Skip to content

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 sendSettingsexternalLexpad 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 and TOPIC 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 LOOPs 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 under greeting 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 values LOOP 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 for TOPICs, IFs or nested LOOPs within this LOOP 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 TOPICs, IFs or nested LOOPs within this LOOP 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 in LOOP 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 subLOOPs. That's why it is possible to use Enjoy {{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.