External Lexpad
WARNING
External Content and External Lexpad are complementary features but they serve very different purposes. Please read both documentations very carefully and then choose the right solution for your task. Otherwise you may fall into inefficient design antipatterns that will affect sending speed and may generate excessive network traffic. You should use External Lexpad when you want to populate message content using complex data structures that are specific for every recipient.
Availability
Currently this feature is only available for messages defined through API (excluding RSSes).
To use it provide sendSettings
→ externalLexpad
section in API call such as createNewsletter.
Synopsis
Let's assume that you have online book store.
Every customer in this store:
- Has corresponding contact in GetResponse (for example you synchronize them using API).
- Can choose favorite book categories.
Now you want to send message to your contacts containing section with new books in their categories.
At this point you should notice few things:
- You will apply the same formatting for many elements (books) in array.
- Every element (book) will have many attributes (author, title, cover type, tags, and so on).
- Set of those elements (books) will be quite unique for every customer. For example if there are 16 categories in your store then there are 65536 sets of favorite categories possible.
That is the sign, that you should use External Lexpad, not External Content
WebService
First you have to provide endpoint, that will return data structure with new books for every GR contact.
Let's assume it is located at: https://example.com/new_books.json
Communication in both ways uses JSON and your service will get following POST request:
json
{
"campaign": {
"id": "ABC123"
},
"message": {
"id": "QWE123"
},
"subscriber": {
"id": "RTY456"
}
}
IDs in query are the same as returned by API. Please note that "contact" is called a "subscriber" in request due to backward compatibility.
The authorization token that you've passed in API call will be sent back in X-Auth-Token
header and must be checked by your endpoint.
Now your webservice should find corresponding customer in book store, check which categories are marked by this customer as favorite, find list of new books in those categories and return them in JSON data structure of your choice:
json
{
"month": "May",
"new_books": {
"Science-fiction": [
{
"title": "Attack from Mars",
"author": "John Foo"
},
{
"title": "Robots",
"author": "Anna Bar"
}
],
"Horror": [
{
"title": "Dark forest",
"author": "Peter Baz"
}
]
}
}
This structure is called "Lexpad" and serves the same purpose as lexpads in programming languages - provides data for code execution.
Message
Message content can use special mergewords that allows to iterate over provided data structure.
md
Hi {{CONTACT `subscriber_first_name`}}
Check out new books released in {{TOPIC `month`}} that may interest you:
{{LOOP `new_books` `category` `books`}}
In category {{TOPIC `category`}} we have
{{LOOP `books` `book`}}
- {{TOPIC `book` `title`}} written by {{TOPIC `book` `author`}}
{{ENDLOOP}}
{{ENDLOOP}}
Happy reading!
Result
Before message is sent to subscriber GetResponse will send Lexpad request to your webservice and will use received data to execute mergeword logic, producing following message:
md
Hi Tom
Check out new books released in May that may interest you:
In category Science-fiction we have
- Attack from Mars written by John Foo
- Robots written by Anna Bar
In category Horror we have
- Dark forest written by Peter Baz
Happy reading!
As you can see External Lexpad feature allows to use more sophisticated template logic. And while most Dynamic Content tags operate on data stored in GetResponse (for example contact name) for LOOP
tags you provide data yourself through your webservice. Simple? OK, let's dig into details 😃
General rules
- WebService must respond within 4 seconds.
- Response cannot be larger than 32KB.
- Lexpad must be in JSON format using UTF-8 character set.
- Top level structure must be JSON object. Arrays or bare values are not allowed.
If any of rules above are broken then message won't be sent to contact.
- It is strictly forbidden to use any formatting directives in Lexpad. Values within must never contain any HTML tags, CSS styles, newlines, etc. Whole formatting must be done on message content level exclusively to speed up sending process and minimize lexpad size and network traffic. It may be tempting to return whole piece of text preformated on webservice side without learning those
LOOP
andTOPIC
constructs, but any detected attempt to do so will result in immediate block of this feature. Preformatting on server side would mean pushing over network the same formatting directives again and again, which is very inefficient and therefore we do not allow it. - It is strictly forbidden to clicktrack links personalized in LexPad per subscriber. This causes separate clicktrack entries per every link and will immediaterly clog message statistics. See "Clicktracking" section for more details.
- It is recommended to return JSON in compact form, without pretty formatting.
- We do not enforce how data structure inside top JSON object should look like. Dynamic Content mergeword execution is fast. Like really, really, really fast. So if your data has deep strucure by its nature do not be afraid to use many levels of nested
LOOP
s to format the output.
TOPIC tag
This tag prints value available in current scope under given name (first param). It understands Scalars, Arrays and Hashes.
Lexpad:
json
{
"greeting": "Hello",
"subject": [
"Dog",
"World",
"Kitty"
],
"punctuation": {
"dot": ".",
"exclamation": "!",
"question": "?"
}
}
Message:
md
{{TOPIC `greeting`}} {{TOPIC `subject` `1`}} {{TOPIC `punctuation` `exclamation`}}
Result:
Hello World !
Explanation:
- First param is value name.
- Second param is not present for Scalar values, it is numeric for Array values and it is string for Hash values.
- Accessing value in a way which do not corresponds with its type will not print such value. For example
{{TOPIC `greeting` `foo`}}
will be skipped because value undergreeting
key is a Scalar, not a Hash. The same applies to{{TOPIC `subject`}}
, which is an Array and expects index to be given. - Arrays are enumerated from
0
. Index is numeric but still must be quoted like any other param according to general Dynamic Content rules. - Arrays can only be indexed in positive, integer, ASCII-only numbers. There are no tricks like counting backwards using negative index, known from some programming languages.
- It is impossible to use numbers as hash keys.
- You can not traverse into nested lexpad
{ `foo` : { `bar` : { `baz` : 123 } } }
using single mergeword like{{TOPIC `foo` `bar` `baz`}}
. To access deeper valuesLOOP
must be used.
LOOP tag
This tag iterates over Hash and Array structures and binds values to new names.
Iterating over Array
Lexpad:
json
{
"cars" : [ `Dodge`, `Uaz`, `Fiat` ]
}
Message:
md
{{LOOP `cars` `car`}}
{{TOPIC `car`}}
{{ENDLOOP}}
Result:
md
Dodge
Uaz
Fiat
Explanation:
- First param is value name.
- Value is expected to be an Array. Otherwise whole
LOOP
will be skipped. - Second param describes new name that values within Array will be bound to. This
"car"
name will be available forTOPIC
s,IF
s or nestedLOOP
s within thisLOOP
scope (continue reading to encounter those mythical creatures).
Iterating over Hash
Lexpad:
json
{
"sounds": {
"cat": "meow",
"pig": "oink",
"dog": "woof"
}
}
Message:
md
{{LOOP `sounds` `pet` `does`}}
{{TOPIC `pet`}} - {{TOPIC `does`}}
{{ENDLOOP}}
Result:
md
cat - meow
dog - woof
pig - oink
Explanation:
- First param is value name.
- Value is expected to be a Hash. Otherwise whole
LOOP
will be skipped. - Second and third param describe new key and value name that values within Hash will be bound to. Those values are available for
TOPIC
s,IF
s or nestedLOOP
s within thisLOOP
scope. - Hashes are unordered by nature, therefore order from your JSON file will not be preserved. Iteration will go over keys sorted lexicographically.
Advanced stuff
Let's assume you have diet business and you are sending daily diet for each contact. We'll use the same lexpad for next few cases:
Lexpad:
json
{
"diet": {
"Breakfast": {
"Eggs": {
"preparation": "boiled",
"amount": "2",
"nutrition": {
"fat": "25g",
"proteins": "26g",
"carbs": "4g"
},
"recipe": "https://example.com/boiled_eggs.html"
}
},
"Dinner": {
"Beef": {
"preparation": "grilled",
"amount": "100g",
"nutrition": {
"proteins": "30g",
"fat": "10g"
},
"recipe": "https://example.com/grilled_burger.html"
},
"Potatoes": {
"preparation": "boiled",
"amount": "100g",
"nutrition": {
"proteins": "2g",
"fat": "1g",
"carbs": "20g"
},
"recipe": "https://example.com/boiled_potatoes.html"
}
}
}
}
Nested iteration
It was already shown in Synopsis, but now let's go through the details:
md
{{LOOP `diet` `meal` `ingredients`}}
...
{{ENDLOOP}}
This will bind keys in "diet"
Hash to "meal"
name. So "meal"
will be "Breakfest"
and "Dinner"
. Values in "diet"
hash will be bound to "ingredients"
name and they contain Hash. So to display every ingredient in every meal you have to iterate over "ingredients"
.
md
{{LOOP `diet` `meal` `ingredients`}}
{{LOOP `ingredients` `ingredient_name` `ingredient_attributes`}}
...
{{ENDLOOP}}
{{ENDLOOP}}
You can go as deeply as needed, just be aware of data type (Hash or Array) you are iterating on.
Binding scopes
Now that we have base iteration let's print something useful:
md
{{LOOP `diet` `meal` `ingredients`}}
For {{TOPIC `meal`}} prepare:
{{LOOP `ingredients` `ingredient_name` `ingredient_attributes`}}
- {{TOPIC `ingredient_attributes` `amount`}} {{TOPIC `ingredient_attributes` `preparation`}} {{TOPIC `ingredient_name`}}
Enjoy {{TOPIC `meal`}}!
{{ENDLOOP}}
{{ENDLOOP}}
Which will print:
md
For Breakfast prepare:
- 2 boiled Eggs
Enjoy Breakfast!
For Dinner prepare:
- 100g grilled Beef
- 100g boiled Potatoes
Enjoy Dinner!
In this example you can see, that:
TOPIC
behaves exactly the same way, doesn't matter how deep inLOOP
structures it is burried. If"ingredient_attributes"
in inner most loop is a Hash then you can get value under"amount"
key by using{{TOPIC `ingredient_attributes` `amount`}}
.- Each name assigned in
LOOP
is also available in subLOOP
s. That's why it is possible to useEnjoy {{TOPIC `meal}}
in inner most loop, while"meal"
was bound in outer loop.
Re-binding scopes
You can rebind name in inner LOOP.
md
{{LOOP `diet` `name` `ingredients`}}
{{LOOP `ingredients` `name` `attributes`}}
{{ENDLOOP}}
{{ENDLOOP}}
Try to avoid it unless you know what you are doing. In this case for example you won't be able to use Enjoy {{TOPIC `name`}}
part, because ingredient name in inner LOOP
scope masks meal name from outer LOOP
.
Clicktracking
Any value reachable through TOPIC
tag can be used as URL in LINK
tag.
md
{{LOOP `diet` `meal` `ingredients`}}
{{TOPIC `meal`}}:
{{LOOP `ingredients` `ingredient` `ingredient_attributes`}}
- {{TOPIC `ingredient`}}
Recipe {{LINK `ingredient_attributes` `recipe`}}
{{ENDLOOP}}
{{ENDLOOP}}
This will print:
md
Breakfast:
- Eggs
Recipe http://getresponse.com/click.html?...
Dinner:
- Beef
Recipe http://getresponse.com/click.html?...
- Potatoes
Recipe http://getresponse.com/click.html?...
And all those links will be visible under message statistics.
Explanation:
- Way of accessing values is exactly the same as for
TOPIC
tag. First param is value name. Second param is not present for Scalar values, it is numeric for Array values and it is string for Hash values. - You cannot give custom names to links clicktracked from Lexpad, second param is used exclusively for Lexpad indexing.
Limitations:
- Length of clicktracked link is limited to 2048 characters.
- Clictracking does not interpret Dynamic Content inside link, it is not possible to provide through LexPad links like
http://example.com?email={{CONTACT `subscriber_email`}}
. - Links must not be unique per subscriber. Due to limitation described above it may be tempting to compose links per subscriber like
http://example.com?email=foo@example.com
and pass them in LexPad. However it will cause multiple subscriber-specific links appear in message statistics, each one will be clicktracked individually for single subscriber. This will severly impact statistics performance and is not allowed.
Conditions
Any Scalar value reachable through TOPIC
tag can be used as left value in IF
tag.
md
{{LOOP `diet` `meal` `ingredients`}}
{{TOPIC `meal`}}:
{{IF `(meal STRING_EQ 'Dinner')`}}
Eat before 17:00
{{ENDIF}}
{{LOOP `ingredients` `ingredient` `ingredient_attributes`}}
- {{TOPIC `ingredient_attributes` `preparation`}} {{TOPIC `ingredient`}}
{{LOOP `ingredient_attributes` `ingredient_attribute_name` `ingredient_attribute_value`}}
{{IF `((ingredient_attribute_name STRING_EQ 'preparation') LOGIC_AND (ingredient_attribute_value STRING_EQ 'grilled'))`}}
Don't burn it!
{{ENDIF}}
{{ENDLOOP}}
{{ENDLOOP}}
{{ENDLOOP}}
This will print:
md
Breakfast:
- boiled Eggs
Dinner:
Eat before 17:00
- grilled Beef
Don't burn it!
- boiled Potatoes
Explanation:
- Left side of condition is value name and it always mean Scalar reference. More on that later.
- Lexpad has precedence over custom values -
{{IF `(car ...)`}}
will take value from Lexpad first and fallback to value from custom if Lexpad one is not present. Use distinctive names for your Lexpad keys to avoid confusion!
One shortcoming that you've probably noticed is that IF
works only on Scalar values and cannot reach into Hashes or Arrays like TOPIC
can. So "meal"
is Scalar value, accessible in this scope through {{TOPIC `meal`}}
and also usable in condition directly through {{IF `(meal ...)`}}
. However making condition on "preparation"
method is slightly more complicated. It can be easilly printed by {{TOPIC `ingredient_attributes` `preparation`}}
but condition cannot use the same syntax and left value must be bound as Scalar. You have to make another LOOP
, inside which "ingredient_attribute_name"
and "ingredient_attribute_value"
will be Scalars. Then you should filter out keys which you are not interested in using (ingredient_attribute_name STRING_EQ 'preparation')
portion of the IF
and then compare value in "ingredient_attribute_value"
. If you run into this issue then the best way out is to rethink Lexpad layout and provide data in more direct or de-normalized way.