# Personalize Your Messages Customize messages using user attributes, external data feeds, and other sources to provide the most relevant content for every customer. # About Personalization > Customize messages using user Attributes, external data feeds, and other sources to provide the most relevant content. ## Personalizing content To personalize content in Airship, create a merge field representing a value you want to personalize, surrounded by double curly braces, e.g., `{{firstname}}`. Airship replaces the merge field (and braces) with the data specified by the merge field at send time — just like mail merge in a word processor or mail client. Common uses of personalization: * **Flight info** — *"Your departure gate for flight `{{flight_number}}` has changed to `{{gate_code}}`."* * **Retail purchase updates** — *"`{{first_name}}`, we received your order `{{order_number}}` and will send tracking information when it has shipped."* * **Membership rewards program updates** — *"You reached `{{program_level}}` status and have been awarded `{{points}}` bonus points!"* In the image below you can see `{{program_level}}` and `{{points}}` appear as `Platinum` and `1500` in the message preview. See also [Previewing personalized content](https://www.airship.com/docs/guides/personalization/previewing/). ![Previewing personalized content in a message](https://www.airship.com/docs/images/personalization-example_hu_26c52160d455c1b3.webp) *Previewing personalized content in a message* ### Handlebars syntax Airship's personalization features are implemented using the [Handlebars](https://handlebarsjs.com) templating language. We do not support all features described in handlebarsjs.com, only those described in our [Handlebars reference documentation](https://www.airship.com/docs/guides/personalization/handlebars/). It includes information about complex expressions that use logic operations such as AND, OR, NOT, greater than/less than, loops, math, and more. ## Data sources The data you use to personalize a message may be personal in nature (first name, birth date, city) or based on user behavior (purchase history, pageviews, app opens). That data can come from these sources: * [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) — In addition to stored attributes you can use the `global_attributes` property in the [Push Object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#pushobject) in the API for individual messages. * [Custom Event](https://www.airship.com/docs/reference/glossary/#custom_event) properties * [Create and Send](https://www.airship.com/docs/reference/glossary/#create_and_send) CSVs * [External Data Feeds](https://www.airship.com/docs/reference/glossary/#external_feed) * [Coupons](https://www.airship.com/docs/reference/glossary/#coupons) * [Message namespace properties](https://www.airship.com/docs/guides/personalization/sources/message-namespace/) * [Web URL triggers](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/triggers/#web-url) ### Evaluation order You can use multiple data sources to personalize a single piece of content, which means it is possible for you to have two merge fields with the same name in the same expression, e.g., an Attribute `{{points}}` and a Create and Send value for `{{points}}`. If two fields share the same name, Airship attempts to use the value that is most relevant to your audience. This order isn't guaranteed, but, in general, Airship attempts to use the following order: 1. Custom event properties 1. Create and Send values 1. Attributes 1. External data feeds and Coupons feeds You are advised to make sure your fields have unique names to prevent unintended personalization results. ## What you can personalize When creating a message, you can personalize: * Viewable message elements: * *All message types* — Message text/body * *Message Center* — Title and Preview lines * *In-App Automation* — Header, body, and footer, and button labels * *Email* — Title, subject, body, From name, and the usernames in the From and Reply to email addresses * *SMS* — Summary * *Open channels* — Title * [Actions](https://www.airship.com/docs/reference/glossary/#action) — Take advantage of information specific to your audience to personalize your audience's experience when they interact with your message. * Media URLs You can also personalize reusable content: * [Templates](https://www.airship.com/docs/reference/glossary/#template) * [Snippets](https://www.airship.com/docs/reference/glossary/#snippet) ## Start personalizing First learn personalization formatting: * Learn [Handlebars syntax](https://www.airship.com/docs/guides/personalization/handlebars/). * Learn how to personalize using each data source: * [Attributes](https://www.airship.com/docs/guides/personalization/sources/attributes/) * [Custom event properties](https://www.airship.com/docs/guides/personalization/sources/custom-events/) * [Create and Send messages](https://www.airship.com/docs/guides/personalization/sources/create-and-send/) * [External data feeds](https://www.airship.com/docs/guides/personalization/sources/external-data-feeds/) * [Coupons](https://www.airship.com/docs/guides/personalization/sources/coupons/) * [Message namespace properties](https://www.airship.com/docs/guides/personalization/sources/message-namespace/) * Learn how to personalize [message actions and media URLs](https://www.airship.com/docs/guides/personalization/content/personalize-actions/). Then you can start personalizing your messages, templates, and snippets. # Dashboard tools for Handlebars > Built-in tools that simplify entering Handlebars expressions. Handlebars is Airship's templating language for personalization. Handlebars expressions use double curly braces wrapped around a content template, ranging from a simple variable, e.g., `{{first_name}}`, to complex evaluations of personalization data. ![Dashboard tools for entering Handlebars expressions](https://www.airship.com/docs/images/pers-tool_hu_6ae6bb8dceb13893.webp) *Dashboard tools for entering Handlebars expressions* We provide tools in the dashboard that simplify adding and formatting **merge fields** and **logic statements** that conditionally render content. When entering content in text fields, click **** to start creating merge fields or logic statements for the Text and Number [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) in your project. These options are available for most text fields, such as message body, button labels, URLs, [Custom Keys](https://www.airship.com/docs/reference/glossary/#custom_keys), and pasted or uploaded HTML in the Interactive editor. See also: [Handlebars reference](https://www.airship.com/docs/guides/personalization/handlebars/). ## Merge fields tool A merge field is a variable in your message or template that you want to populate with a personalized value for each member of the audience. Merge fields are the most basic Handlebars expression. Using this tool, your expression is inserted in the text field in the format `{{attribute_id}}` or `{{$def attribute_id "default_value"}}`. ![Creating a merge field using the dashboard tool](https://www.airship.com/docs/images/pers-tool-merge-field_hu_6a05f853f8fce0f1.webp) *Creating a merge field using the dashboard tool* --- When entering content in text fields: 1. Click **** and select *Merge field*. 1. Search for an attribute and select from the list. You can search by attribute name or ID. 1. Do **one** of the following: * Enter a default value that will be used if the merge field is empty. * Leave the default value blank and check *Allow empty values*. 1. Click **Insert**. --- After inserting, you can edit the `"default_value"` or delete the entire handlebars string to remove it. ## Logic statements tool Logic statements include conditional requirements. The first statement you add is an `if`, which means your content is rendered in your message if the statement is true. You can then add `else if` and `else` statements. Each additional statement is a condition that builds on the preceding one, and all are formatted as a single expression. * **Statement 1:** `if` — The specified text is displayed if the statement is true. *Only this initial statement is required.* * **Statement 2:** `else if` — The specified text is displayed if statement 1 is false and statement 2 is true. *You can add multiple `else if` statements.* * **Statement 3:** `else` — The specified text is displayed if all preceding statements are false. *You can add an `else` statement without adding any `else if` statements.* See also: [Handlebars Reference: If/Else Statements](https://www.airship.com/docs/guides/personalization/handlebars/logic-helpers/#ifelse). --- ![Creating a logic statement using the dashboard tool](https://www.airship.com/docs/images/pers-tool-logic-statement_hu_ad7dc948f4a37ca3.webp) *Creating a logic statement using the dashboard tool* When entering content in text fields: 1. Click **** and select *Logic statement*. 1. Search for an attribute and select from the list. You can search by attribute name or ID. 1. Select the operator you want to use, and enter a value. Available operators depend on the attribute type (Text or Number). As you add and edit statements, the code output that will be inserted in your message displays at the bottom of the window. 1. Click **Add content**, enter the content you want to display in the message when the statement is true, then click **Apply**. 1. (Optional) Click the add icon (+) to add `else` and/or `else if` conditions and complete previous steps. 1. Click **Insert**. After inserting, you can edit the display text for each statement or delete the entire handlebars string to remove it. ### Example logic statement In this example, we want to show existing loyalty program members a discount code and non-members a signup message. We will build this expression based on attribute `loyalty_status`. ![Setting attribute operator and value](https://www.airship.com/docs/images/pers-tool-logic_hu_f8a1f4f033513dcb.webp) *Setting attribute operator and value* First set the attribute to equal value `member`. ![if statement ](https://www.airship.com/docs/images/pers-tool-logic-if_hu_d61653f04f17117a.webp) *if statement * Then enter content `Thank you for being a loyalty member. Use code UNDOCK to get an extra 10% off this purchase.`. ![else statement](https://www.airship.com/docs/images/pers-tool-logic-else_hu_c7c1d1813a711faa.webp) *else statement* Next add an `else` statement, for users for whom the first statement is false, and enter the content those users will see instead: `Like great savings? Sign up to become a loyalty member today!`. ![Your coded expression](https://www.airship.com/docs/images/pers-tool-logic-output_hu_4d5d2d73d11087f4.webp) *Your coded expression* The code updates as you add and edit statements. Click **Insert** to add the expression to your message. # Previewing personalized content > See how your personalized content will look when populated with data. When adding or editing content that contains [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars), preview the appearance with actual values from sample data. The sample data can come from a member of a [Preview Group](https://www.airship.com/docs/reference/glossary/#preview_test_groups) or from a random or specific [Contact](https://www.airship.com/docs/reference/glossary/#contact). You can also enter your own JSON sample data and override the data for any selected user. [Message namespace data](https://www.airship.com/docs/guides/personalization/sources/message-namespace/) is available in previews. ![Previewing personalized content in a message](https://www.airship.com/docs/images/personalization-example_hu_26c52160d455c1b3.webp) *Previewing personalized content in a message* Where message, [Template](https://www.airship.com/docs/reference/glossary/#template), and [Snippet](https://www.airship.com/docs/reference/glossary/#snippet) previews appear, select **View** for **Preview Data** to open the configuration drawer. To close it, select **Hide** or select the arrow icon in the **Preview Data** drawer. Select and hold the drag handle icon (dots-six-vertical) to drag the Preview Data element to a new position on the screen. * For [Scenes](https://www.airship.com/docs/reference/glossary/#scene), first enable the Interactive mode [preview tool](https://www.airship.com/docs/guides/messaging/editors/native/about/#preview-tools) to access the **Preview Data** option. * For email, Message Center, and landing page content, previewing is available within the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor) only. When using the Interactive editor, select **Preview** to access the **Preview Data** option. * Previewing personalization is not available for [In-App Automation](https://www.airship.com/docs/reference/glossary/#iaa) messages or templates. After configuring preview data, it persists in other screens within the same message, template, or Snippet. You do not have to reenter it. Within Sequences, preview data persists between the Manage and Performance screens and is independent of the preview data in individual messages. ## Configure a preview data source After entering content that contains Handlebars, select **View** for **Preview Data**, and then configure a data source in the drawer: | Source | Description | Steps | | --- | --- | --- | | **Preview Group** | Populate the personalized content using data associated with a member of a [Preview Group](https://www.airship.com/docs/reference/glossary/#preview_test_groups). Use this source to validate content rendering for known users instead of relying on actual audience data. | Under **Source**, select **Preview Group**, select a group, and then select a member. | | **Random Contact** | Populate the personalized content using data associated with a random user from the configured message audience. Use this source to validate content rendering for a range of actual audience members.

You can select from a list of up to ten users labeled with their channel type, such as `email`, `SMS`, or `iOS`. If the message does not require configuring an audience, or if the audience is All Users, the list is generated from all users in the project. | Under **Source**, select **Random Contact**, and then select a user. Select **Refresh list** for different users. | | **Specific Contact** | Populate the personalized content using data associated with a specific user in your project. Use this source to validate content rendering for known recipients.

You can search for a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id), [Named User](https://www.airship.com/docs/reference/glossary/#named_user), email address, or [MSISDN](https://www.airship.com/docs/reference/glossary/#msisdn). Matches are labeled with their channel type, such as `email`, `SMS`, or `iOS`. | Under **Source**, select **Specific Contact**, search for an identifier, and select from the results. | | **Custom JSON** | Modify the data for a selected source or provide your own. Use custom JSON to simulate various personalization scenarios. | Select **JSON**, then **Edit**, enter or modify the current data, then select **Apply JSON** to see the changes in the preview. Select **Reset** to restore the unedited data and refresh the preview. | {class="table-col-1-20 table-col-2-40"} ## JSON sample data for Automations and Sequences If your template or message is for an Automation or Sequence triggered by a [Custom Event](https://www.airship.com/docs/reference/glossary/#custom_event), you will want to use sample data based on that event. Enter Custom Event properties representative of an audience member, mirroring the `properties` object in a [Custom Event object](https://www.airship.com/docs/developer/rest-api/ua/schemas/others/#customeventobject). See also [Personalize messages using Custom Events](https://www.airship.com/docs/guides/personalization/sources/custom-events/). The following are a Custom Event and the JSON you would enter when configuring preview data for that event. **Sample Custom Event** ```JSON { "occurred": "{{event_time}}", "user": { "named_user_id": "user" }, "body": { "name": "purchase", "subscribe": true, "properties": { "customer_name": "user", "total": 48, "cost_units": "USD", "purchase": [ { "qty": 4, "item": "MLB regulation baseball", "per": "$12", "total": "$48" } ] } } } ``` **Example JSON for previewing personalization** ```json { "total": 48, "cost_units": "USD", "purchase": [ { "qty": 4, "item": "MLB regulation baseball", "per": "$12", "total": "$48" } ] } ``` ## Sources The data you use to personalize a message may be personal in nature (first name, birth date, city) or based on user behavior (purchase history, pageviews, app opens). # Personalizing messages using Attributes > Attributes are metadata used for audience segmentation and personalization. You can use Attributes in a message body to personalize content for each user. For example, if you were sending a birthday message to your audience, you could target the audience using an [Attribute](https://www.airship.com/docs/reference/glossary/#attributes) for a specific date and also use relevant Attributes within the body of the message to personalize based on an individual's specific age, location, etc. Use [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) to reference Attributes in message content. The image below shows a push notification with title `It's your birthday, {{name}}!` and text `We heard you're turning {{age}}! Here's a gift to help you celebrate.` Using [preview data](https://www.airship.com/docs/guides/personalization/previewing/), you can populate the message preview to see how it will appear to your users. ![Personalizing a message using age and name Attributes](https://www.airship.com/docs/images/personalize-attributes-birthday_hu_468bdfc4256c8d5.webp) *Personalizing a message using age and name Attributes* Since Attributes can represent different kinds of values, make sure you know the format of your Attributes before using them in a message or template. See [Attribute types](https://www.airship.com/docs/guides/audience/attributes/about/#attribute-types) in *About Attributes*. ### Personalizing content with JSON Attributes JSON Attributes are provided as an object with keys corresponding to instance IDs each with a value of the associated instance. To use JSON Attributes in personalization, you can either reference a specific instance by ID or loop through the instances using the `#each` operator. See the [Looping through objects and arrays](https://www.airship.com/docs/guides/personalization/handlebars/loop/) guide. > **Warning:** Do not include an instance ID in conditionals or other queries. Instance IDs are only for use with the APIs for [setting and removing JSON Attributes](https://www.airship.com/docs/guides/audience/attributes/setting/#setting-and-removing-json-attributes). Use the form `attribute_name.PropertyName` only. In the dashboard device preview, other forms, such as `attribute_name#instance_id.PropertyName`, may render correctly, but sends will fail. > > For example, for an Attribute `acme_ticketing_window_json` with a `Cities` property, use `acme_ticketing_window_json.Cities` inside conditionals: > > ```text > {{#if ($contains acme_ticketing_window_json.Cities "Guadalajara")}} > Guadalajara: February 25 at 11:00 (ET) / 17:00 (CET) > {{else}}{{/if}} > {{#if ($contains acme_ticketing_window_json.Cities "Philadelphia")}} > Philadelphia: February 25 at 11:00 (ET) / 17:00 (CET) > {{else}}{{/if}} > ``` Let's look at an example. This is a JSON schema for an Attribute with ID `players`: ```text { name: string cool_factor: number is_cool: boolean inventory: [ { name: string age: number enchanted: boolean custom_name: string magic_properties: [ { id: string bonus: number } ] } ] } ``` An example of a JSON Attribute using the above schema and having two saved instances might look like this: ```text { "player1": { "name":"Ganthur", "cool_factor":7, "is_cool":true, "inventory":[ { "name":"Broadsword", "age":177, "enchanted":true, "custom_name":"Matilda", "magic_properties":[ { "id":"flaming", "bonus":20 } ] } ] }, "player2": { "name":"Balfor", "cool_factor":1, "is_cool":false, "inventory":[ { "name":"Mighty axe", "age":23, "enchanted":false, "custom_name":"Brunhild", "magic_properties":[ { "id":"indestructible", "bonus":4 } ] } ] } } ``` You can use the `this` expression to refer to the current item in the loop and `@index` for the zero-index number of the item. In this example we will create a numbered list of player names and increment the `@index` operator by one using the `add` helper so the list starts with `1`: ```text {{#each players}} {{add @index 1}} - {{this.name}} {{/each}} ``` You can use the `as |name|` syntax to make your loop more readable. Let's add each player's list of inventory names to our example: ```text {{#each players as |player|}} {{add @index 1}} - {{player.name}}. Inventory: {{#each player.inventory as |item|}} {{item.name}} {{/each}} {{/each}} ``` Add an `if` conditional statement to display a message only for players with a sword in their inventory that has the `flaming` magic property and display its custom name: ```text {{#each players as |player|}} {{#each player.inventory as |item|}} {{#if (eq item.name "sword")}} {{#each item.magic_properties as |property|}} {{#if (eq property.id "flaming")}} Congrats on your flaming sword "{{item.custom_name}}" {{/if}} {{/each}} {{/if}} {{/each}} {{/each}} ``` You can also refer to a specific instance of the JSON Attribute using its instance ID. For example, `{{players.player1}}`. # Personalize your Create and Send messages > Create and Send supports personalization using Handlebars. When using [Create and Send](https://www.airship.com/docs/reference/glossary/#create_and_send), you can personalize values directly in the `create_and_send` array, or you can include in your CSV data additional properties that you want to use to personalize messages — any column name or property that is not prefaced with `ua_`. Use `{{column_name}}` or `{{property_name}}` in the body of the message. All values in CSV uploads, including numbers, are represented as strings and cannot be used with [math helpers](https://www.airship.com/docs/guides/personalization/handlebars/math-helpers/). If you want to personalize your message using both Attributes and information from a CSV, make sure that the properties in the CSV are unique from your Attributes. If an Attribute has the same name as a property in the CSV, Airship will try to use the value in the CSV. See [Object and Array Notation for Create and Send](https://www.airship.com/docs/guides/personalization/handlebars/basics/#object-and-array-notation-for-create-and-send) for using complex arrays and objects. ## Enabling personalization When using bulk audiences (`bulk_id`) with personalization, include `"personalization": true` in the `options` object of your request. Personalization is enabled automatically when using the direct `create_and_send` array approach. **Create and Send request with personalization:** ```http POST /api/create-and-send HTTP/1.1 Authorization: Basic Accept: application/vnd.urbanairship+json; version=3 Content-Type: application/json { "audience": { "create_and_send": [ { "ua_msisdn": "15551234567", "ua_sender": "12345", "ua_opted_in": "2021-04-29T10:34:22", "name": "New Customer, Esq.", "from_city": "City", "from_state": "OR" } ] }, "device_types": [ "SMS" ], "message_type": "commercial", "notification": { "sms": { "template" : { "fields" : { "alert": "Hi {{name}}! I hear you're from {{from_city}}, {{from_state}}!", } } } } } ``` # Personalize messages using Custom Events > Personalize message content with values from the Custom Events used to trigger Automations, Scenes, and Sequences. Custom Events help you track user activities and conversions from your app, website, etc. When you trigger an [Automation](https://www.airship.com/docs/reference/glossary/#automation), [Scene](https://www.airship.com/docs/reference/glossary/#scene), or [Sequence](https://www.airship.com/docs/reference/glossary/#sequence) with a [Custom Event](https://www.airship.com/docs/reference/glossary/#custom_event), you can use properties from the event to personalize the resulting messages. For Sequences, you can reuse Custom Event properties in any message in the Sequence, which can help remind your audience about items they left in their shopping cart, or that an event that they're interested in is happening soon. Automations and Sequences support server-side and client-side Custom Events for personalization. Scenes support client-side Custom Events only. > **Tip:** You can also personalize messages using [Attributes](https://www.airship.com/docs/reference/glossary/#attributes), even when you don't use Custom Event triggers. ## Referencing properties in message content Custom Events are associated with a channel or a [Named User](https://www.airship.com/docs/reference/glossary/#named_user) and carry properties that you can use to trigger and personalize your messages. Start by creating an Automation, Scene, or Sequence using the Custom Event trigger, and select one or more Custom Events or event properties. When writing the message, you can reference properties from the Custom Event trigger in your message or template. For example, you could iterate over the items a user purchased by referencing an `items_purchased` property. ```text {{#each items_purchased}} {{qty}} x {{item_name}} = {{item_total}} {{/each}} {{total}} ``` If you reference a property that is not in an event, is null, or empty, the reference will be empty in your message, and your message may not make sense. Use the [default handler](https://www.airship.com/docs/guides/personalization/handlebars/basics/#def) `$def` to set default values, so that your message still makes sense if variables are empty or absent. For example, `{{$def name "you"}}` would insert `you` in the message if the `name` property in your Custom Event is empty or missing. ## Custom Event personalization example When personalizing messages or templates for a Custom Event-triggered Automation, Scene, or Sequence, you can use anything in the Custom Event's `properties` object. The following is an example Custom Event. We will use that to send a personalized receipt in a Message Center message. **Custom Event:** ```json { "occurred": "{{event_time}}", "user": { "named_user_id": "user" }, "body": { "name": "purchase", "subscribe": true, "properties": { "customer_name": "Dave", "total": 48, "cost_units": "USD", "purchase": [ { "qty": 4, "item": "MLB regulation baseball", "per": "$12", "total": "$48" }, { "qty": 2, "item": "MLB official hat", "per": "$15", "total": "$30" } ] } } } ``` Because Custom Event properties can include arrays, objects, and arrays of objects, we can [loop through](https://www.airship.com/docs/guides/personalization/handlebars/loop/) complex Custom Event properties using `{{#each}}`. Next we put it all together with the default handler to craft the message: ```handlebars Hi, {{$def customer_name "valued customer"}}! Thanks for your purchase of: {{#each purchase}} {{qty}} x {{per}} {{item}} = {{this.total}} {{/each}} Total: ${{total}} {{cost_units}} Your order is being processed. We'll message you again when it ships! ``` The result will look like this: ```handlebars Hi, Dave! Thanks for your purchase of: 4 x $12 MLB regulation baseball = $48 2 x $15 MLB official hat = $30 Total: $78 USD Your order is being processed. We'll message you again when it ships! ``` # External Data Feeds > {{< glossary_definition "external_feed" >}} ## About External Data Feeds Using External Data Feeds can help you tell your audience about things they might be interested in, without having to send custom events to Airship or assign [Attributes](https://www.airship.com/docs/reference/glossary/#attributes). For example, if your audience requests weather updates, you can set up an External Data Feed to fetch weather data when you send your message and personalize messages with relevant weather updates for each member of your audience. The following is the general workflow for using External Data Feeds in messages: 1. [Create an External Data Feed](#create-a-feed) in the dashboard. 1. [Reference a feed when entering content in a message or template](#using-a-feed-in-messages). You'll create a merge field representing a value you want to personalize, surrounded by double curly braces `{{ }}`, also known as [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars). Airship replaces the merge field (and braces) with the data specified by the merge field at send time, just like mail merge in a word processor or mail client. Example feed variables in a message: ```text {{#feed "feed_id"}} Content referencing the feed, like {{first_name}} or {{mtn_weather}}. {{/feed}} ``` 1. [Set failure behavior and variable values when sending the message](#setting-feed-failure-behavior-and-variable-values). ### Throttling and caching Personalization using External Data Feeds can mean a lot of API calls in a short period of time. With throttling, you can set a frequency rate for your requests. The minimum rate is 100 requests per second. You can also cache responses from your feeds and use them for multiple recipients. You may want to cache a response if you don't expect your feed to return a different response when using the same feed URL and parameters. You can enable throttling or caching for a feed but not both. ### Object locations You can enter names and paths for objects in your feed so they are available in the code copying helper in the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor). See [Code helper](#code-helper). Use JSON dot notation to enter the paths to the variables you want to reference. For example, if you have a user object, you might want to access the first name of your user. You would enter `firstName` as the *Name* for the data and `user.firstName` as the *Object location*. See also [Object and array notation](https://www.airship.com/docs/guides/personalization/handlebars/basics/#dot-notation). ### External API requirements Your external API must operate in the following ways to work with External Data Feeds: * Return the correct `Content-Type` header for the response. We support `application/json`. * Return the correct `Content-Encoding` header if you are compressing your responses. We support gzip for compression. * Return your content as a JSON object. We do not support top-level JSON arrays. Array content must be nested in an object. ## Create a feed You can create up to 10 feeds per project. [Contact Support](https://support.airship.com/) to request increasing this limit. 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Project settings**, select **External Data Feeds**. 1. Select **Create external data feed**. 1. Configure the feed settings, and then select **Next** to move on: | Setting | Description | | --- | --- | | **Friendly name** | This is the feed name as it appears in the Airship dashboard. | | **Feed ID** | This is how you will reference the feed. The ID must be unique, without spaces or special characters. Use a feed ID that is meaningful to use and relatively easy to type. **You cannot change the feed ID later.** | | **Description** | This is a summary of the data returned by the feed. | | **Request URL** and **Default values** | This is the URL to retrieve data from, also called the *feed URL*. The URL is your domain, protocol, path, and any variables you want to use to personalize your messages. For example, `https://api.example.com/products/`. The default value fields appear automatically based on the request URL. Enter a default value for each send time variable used in the request URL. See [Message-level and user-level variables](#message-level-and-user-level-variables). | | **Confirm access rights for request URL** | Check the box to confirm. | | **Headers** | Optional. Specify any HTTP request headers that may be required to successfully communicate with the external feed. For example, `authorization` headers.

Header values support Handlebars as well, so you can populate header values from attributes and other personalization sources. Header information is not a part of the request URL. Select **Add header**, then enter a *Key* and *Value*. Repeat for additional headers. | 1. (Optional) Configure [throttling or caching](#throttling-and-caching), and then select **Next** to move on: | Setting | Description | | --- | --- | | **Throttle requests** | Enable to set a frequency rate for your requests. Enter the number of API requests per second. The minimum rate is 100 requests per second. | | **Cache response** | Check box to enable. | 1. (Optional) Configure [object locations](#object-locations). Select **Add object location** and enter a name and object location. 1. Select **Save**. ### Message-level and user-level variables The [request URL](#create-a-feed) supports two types of variables. You can use both in the same URL: | Variable type | Syntax | Resolved from | Scope | |---|---|---|---| | **Message-level (send time)** | `[[var]]` | Default value set in the feed, overridden per message | Same value for all recipients | | **User-level** | `{{var}}` (standard [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars)) | Audience data such as [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) and custom event properties | Different value per recipient | Use **message-level variables** to reuse a single feed for different purposes by changing the variable value at send time. You set a default value when creating the feed and can override it when [sending the message](#setting-feed-failure-behavior-and-variable-values). Use **user-level variables** to personalize the feed request per recipient. Airship resolves each variable from the recipient's data, so the feed request URL can differ for each member of your audience. This example uses two message-level variables (`[[program]]` and `[[contentID]]`) to fetch different content per campaign without creating separate feeds: `https://api.example.com/[[program]]/[[contentID]]` This example combines a message-level variable (`[[msg_purpose]]`) with user-level variables (`{{first_name}}` and `{{last_name}}`), so each recipient gets a personalized request for the same campaign: `https://api.example.com/[[msg_purpose]]?user={{first_name}}%20{{last_name}}` If your users have [Named User](https://www.airship.com/docs/reference/glossary/#named_user) IDs set, you can use the `ua_named_user_id` [default attribute](https://www.airship.com/docs/reference/data-collection/attributes/#default-attributes) to fetch feed data per user: `https://api.example.com/[[msg_purpose]]?user={{ua_named_user_id}}` ## Using a feed in messages You add your data feed to messages in two parts: 1. Formatting the message content 1. Entering default values for the variables you referenced in your message and determining how to handle the message if the feed fails ### Formatting content Formatting your message content is identical in the dashboard and the API. In the dashboard you add content when configuring a template or in the Content step in a composer. **Content format for External Data Feeds** ```text {{#feed "feed_id"}} Content referencing the feed, like {{first_name}} or {{mtn_weather}}. {{/feed}} ``` If your feed URL includes send time variables, you can override their default values in the `#feed` block as `{{#feed "feed_id" var="value"}}`. Or you can [set a value in the *Delivery* step](#setting-feed-failure-behavior-and-variable-values) that applies to all of the channels selected for the message. ### Code helper When using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor) to create content for a template, email, landing page, or Message Center message, we provide a helper for inserting feed code. Available code: * Object locations — Paths defined in the feed settings. * `#feed` blocks — Available for all feeds in your project. A preformatted block that includes the feed ID: ```text {{#feed "feed_id"}} {{/feed}} ``` When adding content: 1. Select *Data Feed* in the left sidebar. 1. Select ▼ for a feed. 1. Hover over a feed name or object location and select the duplicate icon ( ) to copy the code. 1. Select a field in your message content and paste the copied data. ![Copied and pasted feed code in an App template](https://www.airship.com/docs/images/feed-in-template_hu_47c7a83c3fd90937.webp) *Copied and pasted feed code in an App template* #### Differentiating data sources You can use reference variables (or [Merge Fields](https://www.airship.com/docs/reference/glossary/#merge_field)) that aren't a part of a feed and variables that are part of a feed in the same `#feed` block. For instance, if your content uses project attribute `first_name` and the feed also has a property `first_name`, if you don't namespace it, the feed data will take precedence over the project attribute. To differentiate the data sources, set the feed namespace using `as |alias|`. This ensures that Airship uses the proper data for the variables in your messages. In the example below, we can differentiate between attributes (`first_name` and `user_id`) and feed properties based on the parent namespace set within the `feed` and `each` blocks. ```text {{#feed "featured_products" region="us" as |result|}} Hi, {{first_name}}. Take a look at {{#each (limit result.products 2) as |product|}} {{product.name}}: {{product.price}} https://example.com/us/{{user_id}}/featured/{{urlEncode product.slug}}/ {{else}} Products that might interest you at https://example.com/us/products/featured {{/each}} {{else}} Couldn't fetch featured products! {{/feed}} ``` ### Setting feed failure behavior and variable values In the *Delivery* step of a composer, you must configure each feed listed in *External data feed options*. * **Failure behavior** — Determine how your message is handled if the feed fails. If your message still makes sense without a feed, you may want to send the message even if your data feed fails. Also consider how ongoing (automated and recurring) messages will be affected if you later [delete the feed](#managing-external-data-feeds). Select *Abort sending the message* or *Send message without this data*. See also: [Success, Retry, and Error Conditions](#success-retry-and-error-conditions). * **Send time variables** — If your feed URL contains send time variables, each one will be listed as `Default value for [var]` with its default value. You can change the value here to override the default. Changes apply to the current message only. ![External data feed settings in a composer's Delivery step](https://www.airship.com/docs/images/feed-options_hu_e93b42f76f4de163.webp) *External data feed settings in a composer's Delivery step* With the API, you set the failure (error) behavior in the [External Data Feed References Object](https://www.airship.com/docs/developer/rest-api/ua/schemas/external-data-feeds-references/). When using a data feed you must include a [`templates` object](https://www.airship.com/docs/developer/rest-api/ua/operations/personalization/) in the payload or set the `personalization` option to `true`. * The value for `name` is the Feed ID you set when [creating your feed](#create-a-feed). * The possible values for `on_error` are `cancel` and `continue`. These are equivalent to the dashboard options *Abort sending the message* and *Send message without this data*. `continue` will result in sending the message despite an error. Otherwise, a feed failure will prevent Airship from sending your message. **Example push request using an External Data Feed** ```json { "feed_references": { "feeds": [ { "name": "featured_products", "params": { "region": "us" }, "on_error": "continue" } ] } } ``` ## Managing External Data Feeds To see a list of all your feeds: 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Project settings**, select **External Data Feeds**. To edit all fields except the Feed ID, select the more menu icon (⋯) for a feed, select **Edit**, make your changes, and then select **Save**. If you make changes to the Request URL, you will need to reconfirm the request URL domain. You must confirm access rights for request URL every time you edit a feed. To delete a feed, select the more menu icon (⋯) for a feed, and then select **Delete**. > **Warning:** * If you delete an External Data Feed, any messages content referencing its data will be handled according to the message's [feed failure behavior settings](#setting-feed-failure-behavior-and-variable-values). > * Your External Data Feed is deleted immediately. There is no confirmation and your External Data Feed cannot be restored once deleted. ## Success, Retry, and Error Conditions

Airship considers any 2xx response from a feed to be a success and will continue a message using the feed response. Airship considers any 3xx, 4xx, and 5xx codes (except 502/503) to be error conditions.

HTTP status Result Behavior
2xx Success Airship uses the feed response, even if empty.
3xx Error Error behavior determined by user.
4xx Error Error behavior determined by user.
5xx Error Error behavior determined by user.
timeout (60 sec) Retry Up to 4 retries, 60 sec between attempts.
502/503 Retry Up to 4 retries, 60 sec between attempts.
## Send Aborted event The [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds) `SEND_ABORTED` event occurs when a push is dropped from our system before delivery is attempted. Device information for the device that did not receive the push is included with `SEND_ABORTED` events, and the `reason` property gives more information about the error. For additional information and a sample event, see the [Send Aborted event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#send-aborted) in the *Real-Time Data Streaming API* reference. # Personalization using message namespace properties > Message namespace properties are dynamic values that are automatically populated based on the details of a messaging campaign. These properties are the message name, platforms, send date, [Push ID](https://www.airship.com/docs/reference/glossary/#push_id), and [Campaign Categories](https://www.airship.com/docs/reference/glossary/#campaign_categories). They serve a crucial role in personalizing messages and tailoring content by allowing you to dynamically adjust [URL Parameters](https://www.airship.com/docs/reference/glossary/#url_parameters) and implement conditional logic using [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars). Using these properties, you can create more relevant and targeted messaging experiences for your audience. You can also use [Text](https://www.airship.com/docs/guides/personalization/handlebars/text-helpers/) and [Date and time](https://www.airship.com/docs/guides/personalization/handlebars/date-time-helpers/) helpers to transform content in your Handlebars expressions. The properties are available for personalization for all channels and message types except for [In-App Automations](https://www.airship.com/docs/reference/glossary/#iaa) and [Scenes](https://www.airship.com/docs/reference/glossary/#scene). Message namespace properties: | Namespace property | Description | Format | Example value | | --- | --- | --- | --- | | {{$message.push_id}} | The Airship [Push ID](https://www.airship.com/docs/reference/glossary/#push_id) assigned to the message | String | 50b521d9-4cd1-4c08-99c1-cabcf0bf5a2b | | {{$message.platforms}} | The platforms enabled for this message: iOS, Android, Email, SMS, etc. | Comma separated text | ios,email | | {{$message.date}} | The date the message was sent, in UTC | [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) | 1983-01-31T12:36:35 | | {{$message.name}} | The message name set in a [Composer](https://www.airship.com/docs/reference/glossary/#composer) | String | Autumn promo | | {{$message.campaign_categories}} | The [Campaign Categories](https://www.airship.com/docs/reference/glossary/#campaign_categories) defined in the message | String, 64-character maximum per category | spring_newsletter 2024 | # Coupons > Add promotional codes to your messages. {{< badge "axp" >}} ## About Coupons ![A landing page with a promotional code — Barcode requires JavaScript](https://www.airship.com/docs/images/coupons-landing-page_hu_9f85950f7b0c85f1.webp) *A landing page with a promotional code — Barcode requires JavaScript* Like other forms of personalization in Airship, with Coupons you create a merge field representing a value you want to personalize, surrounded by double curly braces `{{ }}`, also known as [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars). Airship replaces the merge field (and braces) with the data specified by the merge field at send time — just like mail merge in a word processor or mail client. The source of this data (including your promotional codes) is a CSV file you upload to Airship and the campaign you define for the codes. We refer to this source as a *feed* for the campaign, and you'll see the term in the dashboard and in the required formatting for your message content. Example uses: * Provide exclusive discounts or rewards to retain or transition your customers and build loyalty * Clear out excess or slow-moving inventory * Re-engage existing customers with your business Coupons are supported for messages in both the dashboard and API. --- Promotional codes are rendered in plain text in your messages. You can use the JsBarcode library to [render the codes as barcodes instead](#rendering-as-a-barcode). Codes are limited to 100 requests per second and are best used with automation. Consider this rate if you intend to distribute codes via broadcasts to large audiences. ### Supported message types You can add codes to any message content you create using the Message, A/B Test, Automation, and Sequence composers: * Push notification * In-app message (standard format) * Message Center (Not supported for A/B Tests) * Landing page * Web push notification * Email * SMS * Open channel You can also add codes to [Templates](https://www.airship.com/docs/reference/glossary/#template) for: * Push notification * Message Center (can also be used for landing pages) * Web push notification * Email * SMS * Open channel The same message types and template content are supported in the API. Coupons are not supported for In-App Automations or Scenes, as these are ephemeral and not a good place to show the promotional code. We suggest using a link in these formats to other messaging formats that are persistent. ### Remaining code notifications When you upload your CSV file, a value equal to 10% of the number of codes is stored and checked when a code is used in a message. We'll notify you by email when 90% of codes have been used in messages and again at 100%. You can specify up to five email addresses for notification. Add `coupons@partner-alerts.airship.com` as a contact in your email client to ensure these messages are not marked as spam. We repeat the calculation when you [add more codes to a campaign](#managing-campaigns-and-adding-more-codes). ### Coupons workflow The following is the general workflow for using promotional codes in messages. For illustration, we are using retail loyalty as an example. 1. [Create your CSV file.](#formatting-your-csv-file) The first row must contain headers `coupon_code` and `campaign`. Each additional row must contain a unique alphanumeric code and the same ID for the campaign. (You can add more information in your file, but we're keeping it simple for this example). **Example CSV** ```text coupon_code,campaign RS8E3589,2023loyalty RS8E3590,2023loyalty RS8E3591,2023loyalty ``` 1. [Create a campaign](#creating-a-campaign) in the dashboard. You will upload your CSV file and define the following: * *Campaign name* — This is used for identification in the list of all campaigns in your project. * *Campaign ID* — This must match what you put in your CSV. You will use the ID when creating your messages so Airship can use codes from the right campaign. * *Validity period* — The effective and expiration dates and times for the campaign and its codes. In our examples the campaign ID is `2023loyalty`. 1. [Create your content](#adding-codes-to-messages) using the following format. In all content, use the feed ID `couponing`. ```text {{#feed "couponing"}} Content referencing the feed, like a {{coupon_code}}. {{/feed}} ``` Using our loyalty example, your message would look like this in the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor): ![Coupons](https://www.airship.com/docs/images/interactive-coupon_hu_5ef052723c1b0ec3.webp) 1. [Specify the campaign ID and determine failure behavior](#specifying-the-campaign-id-and-feed-failure-behavior) when sending the message. * In the dashboard, you'll do this in the Delivery step of a composer. * With the API, you'll make these specifications in the External Data Feed References object. > **Important:** As a best practice, verify the campaign ID, its validity period, and that it has available codes prior to sending the message. The feed will fail if the expiration date-time has passed or does not have remaining codes. > > You can view the number of available codes for each campaign: > > 1\. Next to your project name, select the dropdown menu (▼), then **Settings**.
> 2\. Under **Project settings**, select **Coupons**. --- Boom! We just sent codes to our audience. Now what happens? This is what Airship does next: * Populates your message content at send time, using one code from your CSV file per message * Checks the number of remaining codes so we can process your [remaining code notifications](#remaining-code-notifications) * Generates an event every time a code is used in a message (See next section) > **Note:** Codes do not populate messages as ordered in your CSV file. For example, if your CSV file has rows with codes in order `1001, 1002, 1003`, they may be sent to users in order `1003, 1001, 1002`. ### Reporting Codes are associated with the [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id) of the message recipient at send time. This association is recorded as a [Custom Event](https://www.airship.com/docs/reference/glossary/#custom_event) with name `coupon_assigned`. The event properties use keys `coupon_code` and `campaign`. `coupon_code_alt` will also appear if included in your CSV file. The property values are the actual code used in the message and the campaign ID. The event is recorded the first time a code for a given campaign is assigned to a Channel ID. Additional events are not recorded if you send messages to the same Channel ID that references the same code. **Example Coupons custom event** ```json { "name": "coupon_assigned", "properties": { "coupon_code": "RS8E3589", "campaign": "2023loyalty" } } ``` ## Formatting your CSV file When uploading in the dashboard, maximum file size 1.5 GB. There is no file size limit when using SFTP [to add more codes](#managing-campaigns-and-adding-more-codes) to a campaign. To ensure the support of special characters and accents, the file must be encoded to UTF-8 without BOM. * `campaign` — Required. The campaign ID. Used to determine which campaign to use for populating message content. Ignored when uploaded in the dashboard. Must match an existing campaign when uploaded via SFTP. * `coupon_code` — Required. A unique alphanumeric value to render in a message. Duplicate values are ignored and not added to the campaign as available codes. * `coupon_code_alt` Optional. An alternative version of the coupon code (such as an online code) that can be rendered in a message. Must be a unique alphanumeric value. * `starts_time` — Optional. The effective date-time for the campaign. Must be in ISO 8601 date-time format `YYYY-MM-DDThh:mm:ss`. Defaults to current date-time if not provided. **Ignored when file is uploaded in the dashboard.** Updates the campaign effective date-time in the dashboard when uploaded via SFTP. * `ends_time` — Optional. The expiration date-time for the campaign. Must be in ISO 8601 date-time format `YYYY-MM-DDThh:mm:ss`. **Ignored when uploaded in the dashboard.** Updates or sets the campaign expiration date-time in the dashboard when uploaded via SFTP. ### Pre-assigning coupon codes By default, coupon codes are randomly assigned to recipients at send time. You can pre-assign coupon codes to specific contacts by adding user IDs to your CSV file. Pre-assigned coupon codes are not added to the campaign as available codes or included in the [remaining code notifications](#remaining-code-notifications). To pre-assign codes to [Channel IDs](https://www.airship.com/docs/reference/glossary/#channel_id), add these headers and values to your CSV: * `identifier_type` — Set to `channel_id`. * `identifier` — The Channel ID of the contact to pre-assign to the coupon code. To pre-assign codes using [Named User](https://www.airship.com/docs/reference/glossary/#named_user) IDs, you must first contact your technical account manager or [Airship Support](https://support.airship.com/) to enable the feature. Once enabled, use the same headers as above, but with these values: * `identifier_type` — Set to `named_user`. * `identifier` — The Named User ID of the contact to pre-assign to the coupon code. ### Sample CSV for Coupons Use the selector to see sample files containing all headers and only the required headers. #### Only required headers ```text coupon_code,campaign myCouponCode0,myCampaign myCouponCode1,myCampaign ``` #### All headers ```text coupon_code,coupon_code_alt,campaign,starts_time,ends_time,identifier,identifier_type myCouponCode0,myOnlineCode0,myCampaign,2023-11-20T21:43:44,2024-11-20T21:43:44 myCouponCode1,myOnlinceCode1,myCampaign,2023-11-20T21:43:44,2024-11-20T21:43:44,9c36e8c7-5a73-47c0-9716-99fd3d4197d5,channel_id ``` ## Creating a campaign 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Project settings**, select **Coupons**. 1. Click **Create coupon campaign**. 1. Configure fields: * *Campaign name* — Used for identification in the list of all coupon campaigns in your project. * *Campaign ID* — Used to determine which campaign to use for populating message content. * Format supports alphanumeric characters, dashes, and underscores. * The ID must be unique for a project, without spaces or special characters. * The `campaign` value in your CSV file will be ignored. * **You cannot change the campaign ID later.** * *Effective/Expiration dates* — Optional. The beginning and end dates and times for the campaign and its codes. * If the effective date and time are not specified, they default to the date-time of saving the campaign. * **If you set a start date and time, you cannot edit them later**. * *Remaining codes contact* — The email address used to contact you with [remaining code notifications](#remaining-code-notifications). You can enter up to five comma-separated addresses. Addresses entered here will replace those entered for every campaign in the project. 1. Upload your CSV file. 1. Click **Save campaign**. You will return to the [list of all campaigns in the project](#managing-campaigns-and-adding-more-codes). ## Adding codes to messages You add codes to messages in two parts: 1. Formatting the message content 1. Specifying the campaign to use as the data feed and determining how to handle the message if the feed fails ### Formatting content Formatting your message content is identical in the dashboard and the API. In the dashboard you add content when configuring a template or in the Content step in a composer. #### Feed without campaign ```text {{#feed "couponing"}} Content referencing the feed, like a {{coupon_code}}. {{/feed}} ``` #### Feed with campaign ```text {{#feed "couponing" campaign="FallSavings"}} Content referencing the feed, like a {{coupon_code}}. {{/feed}} ``` The feed ID is always `couponing`. To render coupon codes, the campaign ID, or effective and expiration dates, use `{{coupon_code}}`, `{{campaign}}`, `{{campaign_start_date}}`, and `{{campaign_end_date}}`. Dates are rendered in ISO 8601 date-time format: `YYYY-MM-DDThh:mm:ss`. If you want dates to appear differently in your message, use [Date and time helpers](https://www.airship.com/docs/guides/personalization/handlebars/date-time-helpers/). Examples for a campaign with expiration date 2023-10-15: * To render the date as `10/15/2023`, you would enter it like so in your message content: `{{dateFormat campaign_end_date locale="en-US" format="SHORT"}}`. * To render the date as `Oct 15, 2023`, you would use: `{{dateFormat campaign_end_date locale="en-US" format="MEDIUM"}}`. You can use reference variables (or [Merge Fields](https://www.airship.com/docs/reference/glossary/#merge_field)) that aren't a part of a feed and variables that are part of a feed in the same `#feed` block. See [Differentiating data sources](https://www.airship.com/docs/guides/personalization/sources/external-data-feeds/#differentiating-data-sources) in *External Data Feeds*. > **Tip:** When using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor) to create content for a template, email, landing page, or Message Center message, we provide a helper for inserting the feed formatting. > > 1. Click *Data Feed* in the left sidebar. > 1. Click ▼ next to *Coupon Service*. The feed ID `couponing` will be exposed. > 1. Hover over the feed ID and select the duplicate icon ( > ). > 1. Select a field in your message content and paste the copied data. You should see: > ```text > {{#feed "couponing"}} > {{/feed}} > ``` ### Rendering as a barcode Use the [JsBarcode library](https://github.com/lindell/JsBarcode) to render your alphanumeric codes as barcodes. Make sure that the location where you are using a barcode supports HTML and JavaScript. For example, you cannot use barcodes in push notifications since they only support plain text. **Sample content for a barcode** ```html
``` ### Displaying an alternative coupon code If your use case requires two versions of a coupon code such as an in-store and an online version, you can include both versions in your CSV file and display them in your message. **Content format for alternative code** ```text {{#feed "couponing"}} In-store code {{coupon_code}} or an online code {{coupon_code_alt}} {{/feed}} ``` ### Specifying the campaign ID and feed failure behavior Specifying the campaign ID and determining feed failure handling differs between the dashboard and API. In the dashboard, you set both in the Delivery step of a composer in *External data feed options*: * **Default value for campaign** — Enter the campaign ID (not the campaign name) for the campaign you want to use for the message. The default value is the first campaign created for the project. * **Failure behavior** — Select *Abort sending the message* or *Send message without this data*. For most Coupons scenarios, you likely want to use *Abort...* since the message won't make sense without a code and, potentially, its validity period.. Also consider how ongoing (automated and recurring) messages will be affected if you later [delete their campaigns](#deleting-a-campaign). See also: [Success, Retry, and Error Conditions](#success-retry-and-error-conditions). ![Configuring a Coupons feed in the composer](https://www.airship.com/docs/images/coupons-delivery_hu_f6cfb36382f70974.webp) *Configuring a Coupons feed in the composer* --- With the API, you set the campaign ID and failure (error) behavior in the [External Data Feed References Object](https://www.airship.com/docs/developer/rest-api/ua/schemas/external-data-feeds-references/). When using a Coupons feed you must include a [`templates` object](https://www.airship.com/docs/developer/rest-api/ua/operations/personalization/) in the payload or set the `personalization` option to `true`. * The value for `name` is `couponing` for all Coupons campaigns. * The possible values for `on_error` are `cancel` and `continue`. These are equivalent to the dashboard options *Abort sending the message* and *Send message without this data*. `continue` will result in sending the message despite an error. Otherwise, a feed failure will prevent Airship from sending your message. * To specify the campaign ID, you must include a `defaults` object `couponing` with property `campaign` and the campaign ID as its value. **Example push request using a Coupons feed** ```http POST /api/push HTTP/1.1 Authorization: Basic Accept: application/vnd.urbanairship+json; version=3 Content-Type: application/json { "device_types":[ "ios" ], "audience":{ "tag": "earlyBirds" }, "notification":{ "alert":"{{#feed \"couponing\"}} As a token of our appreciation for being a loyal Fan, please enjoy 15% off your next in-app order. Offer expires {{dateFormat campaign_end_date locale="en-US" format="SHORT"}} Use coupon code: {{coupon_code}}{{/feed}}" }, "options": { "personalization": true }, "feed_references": { "feeds": [ { "name": "couponing", "params": { }, "on_error": "cancel" } ], "defaults": { "couponing": { "campaign": "2023loyalty" } } } } ``` ## Managing campaigns and adding more codes To see a list of all your campaigns and their current number of available coupons.: 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Project settings**, select **Coupons**. Campaigns are sorted by effective date and time, latest first. Select the edit icon ( ) to edit the following for a campaign: * Name * Expiration date and time * Remaining codes contact — Addresses entered here will replace those entered for every campaign in the project. * [CSV file](#formatting-your-csv-file) — You can upload a new file only. You cannot download previously uploaded data. Generally, you only need to upload a new CSV file when you want to **add more codes** to a campaign. You can either append rows to the original CSV file used to create the campaign and reupload it or create a new CSV file containing only rows with new codes. If creating a new file, the only required headers are `coupon_code` and `campaign`, even if a previously uploaded file contained additional headers. You also have the option to provide a new CSV file via SFTP. Understand the differences in handling before deciding on a method: * If you upload a new CSV file using the dashboard, only `coupon_code` values will be processed. All other columns will be ignored. * If you upload a new CSV file via SFTP, all values (other than for `campaign`) will update the current campaign settings in the dashboard. For this reason, always verify that your CSV file has the correct `campaign` value for the campaign you intend to update. To upload a new CSV file via SFTP, see: [SFTP upload for CSV files](https://www.airship.com/docs/guides/audience/segmentation/sftp-upload/). > **Note:** Since message content is populated at send time, if you upload a new CSV file for a campaign, scheduled messages will use the most recent campaign data. They won't necessarily use the values available when the message was created. "Scheduled" includes recurring messages. ### Deleting a campaign > **Warning:** If you delete a campaign, any messages content referencing its data will be handled according to the messages's [feed failure behavior setting](#specifying-the-campaign-id-and-feed-failure-behavior). 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Project settings**, select **Coupons**. 1. Select the delete icon (trash) for a campaign. ## Success, Retry, and Error conditions

Airship considers any 2xx response from a feed to be a success and will continue a message using the feed response. Airship considers any 3xx, 4xx, and 5xx codes (except 502/503) to be error conditions.

HTTP status Result Behavior
2xx Success Airship uses the feed response, even if empty.
3xx Error Error behavior determined by user.
4xx Error Error behavior determined by user.
5xx Error Error behavior determined by user.
timeout (60 sec) Retry Up to 4 retries, 60 sec between attempts.
502/503 Retry Up to 4 retries, 60 sec between attempts.
## Content Wen creating a message, you can personalize viewable message elements, actions, media URLs, templates, and snippets. # Content templates > Use content templates to personalize messages with information specific to each member of your audience, like first name, flight number, or order status. You can even pass complicated variables to your template. ## About content templates You can create content templates for all messaging channels: * App — For [Push Notification](https://www.airship.com/docs/reference/glossary/#push_notification), [Message Center](https://www.airship.com/docs/reference/glossary/#message_center), and [In-App Automation](https://www.airship.com/docs/reference/glossary/#iaa), follow the steps on this page. Templates for In-App Automation can be used for [modal and fullscreen styles](https://www.airship.com/docs/guides/features/messaging/in-app-automation/#styles) only. * Web * SMS/MMS * Email * Open channel After creating content templates, you can select them in the Content step in a composer or reference them with the `template` property in the API. In-App Automation is not supported for API use. In the composers, for App, Web, SMS/MMS, and Open channels, the template content replaces your message content as is and cannot be edited. For Message Center, In-App Automation, and Email, you can edit your message content after selecting the template. You can assign keywords to your templates so they are easily found in search. > **Tip:** * Name your templates according to the sources they use for personalization, so it's clear when and where you can reuse your template. > * For reusable message designs for [Scenes](https://www.airship.com/docs/reference/glossary/#scene), see [Custom content layouts](https://www.airship.com/docs/guides/messaging/editors/native/custom-layouts/). ### Content template format A content template has a name, optional description, and fields that make up the viewable content in a message. | Template | Fields | | --- | --- | | App (Push notification) | - **Message text** — The text that will display in your push notification.
- **Title** — A heading that appears above the notification text when applicable.
- **Summary** — Supplemental text displayed with the notification. **iOS:** The summary appears below the push notification title. **Android and Fire OS:** The summary appears below the main notification text in most cases. This is the only visible text other than the title when Android Picture is visible in expanded mode, as the main notification text is suppressed. | | Message Center | - **Title** — A heading that appears above the message and in the Message Center inbox.
- **Message body** — The HTML body of your message, created using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor). | | In-App Automation | - **Message body** — The HTML body of your message, created using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor). | | Web | - **Alert** — The text that will display in your web push notification.
- **Title** — A heading that appears above the message. | | SMS | - **Message text** — The text that will display in your SMS message. | | MMS | - **Subject** — A meaningful subject to summarize your MMS message.
- **Message text** — The text that will display in your MMS message.
- **Fallback SMS text** — Fallback text version of your message, for use when MMS is unavailable. | | Email | - **Subject** — A meaningful subject to summarize your email.
- **HTML body** — The HTML body of your email, created using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor).
- **Plain text body** — The plain-text version of your message, for use when HTML is unavailable. | | Open channel | - **Alert** — The text that will display in your open channel message.
- **Summary** — Supplemental text displayed with the notification.
- **Title** — A heading that appears above the notification text when applicable.
- **Media attachment URL** — The URL for media you want to include in your message. | {class="table-col-1-20"} #### Using the Interactive editor When adding the email *HTML body*, or In-App Automation or Message Center *Message body*, you can use the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor) to: * Paste or upload your own HTML. * Design using drag-and-drop. You can start from a blank layout or select an Airship default layout or a [layout you saved](https://www.airship.com/docs/guides/messaging/editors/interactive/saving-layouts/). The Interactive editor supports [[Merge Fields](https://www.airship.com/docs/reference/glossary/#merge_field)](https://www.airship.com/docs/guides/messaging/editors/interactive/merge-fields/), so you can personalize your message for your audience. ![Designing a Message Center template using the Interactive Editor](https://www.airship.com/docs/images/editor-interactive_hu_f3164c9db0a14a24.webp) *Designing a Message Center template using the Interactive Editor* ### Personalizing content templates You can use our built-in tool to insert merge fields and logic statements for the [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) in your project. [Simplifying Handlebars expressions](https://www.airship.com/docs/guides/personalization/simplifying-handlebars/). You can also manually enter [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars). Both methods are available for all text fields in a template, including when you paste or upload HTML using the Interactive editor. > **Important:** When using Handlebars to reference variables, you should define default values for your fields with `{{ $def field_name "default value" }}`. The template preview renders variables without default values as blank spaces, mimicking the behavior your audience will experience if they receive messages with unpopulated variables. When personalizing messages in the Interactive editor, you must define merge fields before you can use them to personalize blocks of content unless you write your own custom HTML blocks. You can personalize custom HTML blocks using [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) like you would any other message. See [Merge fields in the Interactive editor](https://www.airship.com/docs/guides/messaging/editors/interactive/merge-fields/). ![Selecting the Merge Tags option for a content template](https://www.airship.com/docs/images/merge-tag_hu_6cef51c5d87a680d.webp) *Selecting the Merge Tags option for a content template* > **Tip:** You can also take advantage of looping (`#each`), conditional if/else statements, and other advanced [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) inside custom HTML blocks. However, you cannot apply conditional or looping logic to block-level elements in your template without defining merge fields in the layout first. ## Create a content template in the dashboard Follow these steps to create a template in the dashboard: 1. Go to **Content**, then **Templates** 1. Select **Create template**. 1. Enter a name and a brief description to help you identify the template in lists (for example, "Holiday 2020"), and select the channel: * App — For [Push Notification](https://www.airship.com/docs/reference/glossary/#push_notification) content only * Message Center * In-App Automation — For use with modal and fullscreen [styles](https://www.airship.com/docs/guides/features/messaging/in-app-automation/#styles) only * Web * SMS/MMS * Email * Open channel 1. (Optional) Add keywords to help organize your templates. Enter a term in the search field and select from results, or select **Add keyword: **. You can add up to 10 keywords. 1. Select **Save and continue**. 1. Enter your content. The preview updates as you type. Open channel templates do not have a preview. Select **Add +** and add content for each field, then select **Done**. For email *HTML body*, and Message Center and In-App Automation *Message body*, provide your HTML or design using drag-and-drop. For email, in the Interactive editor: * To [reduce the HTML size](https://www.airship.com/docs/guides/messaging/messages/content/email/email/#size-limit-and-html-minification), select **Settings** in the sidebar, and then enable **Minify HTML**. * After selecting **Done**, choose whether to save the HTML body only or [also generate the plain text body](https://www.airship.com/docs/guides/features/messaging/email/#plain-text-generation). 1. (Optional, for email only) [Preview your email](https://www.airship.com/docs/guides/messaging/messages/content/email/email/#inbox-previews) in different clients:
  1. Click Inbox preview.
  2. Select from the lists of browser, desktop, and mobile clients, then click Generate previews.
  3. Click a thumbnail to see the full version. Click the close icon (×) to close and choose another preview.
  4. (Optional) To add/remove clients, click Reselect and generate previews and start over.
  5. When you are finished with inbox previews, select the close icon (×) to close the modal.
1. Select **Save template** when you are done adding content. ## Creating content templates using the API You can use the [Content API](https://www.airship.com/docs/developer/rest-api/ua/operations/content/) to create, list, retrieve, update, and delete content templates. For supported template `type` values and fields, see the [Content template object](https://www.airship.com/docs/developer/rest-api/ua/schemas/content-objects/#contenttemplateobject) schema in the API reference. OAuth client credentials can include the Content scope for these endpoints. See [Content](https://www.airship.com/docs/developer/rest-api/ua/api-auth-reference/#content) in the *Airship API authorization reference*. When you create or update a template, Airship validates it using the same rules as the dashboard, including [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) syntax and reference checks. Invalid templates are not saved. You can set an optional `external_id` on a template. The ID is a customer-defined string that is unique within a project. You can use it to look up or update a template using dedicated endpoints and to reference the template in send and push payloads where supported, as an alternative to the template UUID. ## Using content templates In the dashboard, you can select a template in a composer's Content step: * For In-App Automation, you must select Modal or Fullscreen in the Style step, then you will see the option to use a template in the Content step. * For Message Center, In-App Automation, and Email, you can edit the HTML or drag-and-drop design in the Interactive editor after selecting a template. For email templates, you can create a new message directly from your templates list. Go to **Content** and select **Templates**, then select the more menu icon (⋯) for an email template and then **Use template**. This will open the [Message composer](https://www.airship.com/docs/guides/messaging/messages/create/) with the Email channel enabled and the template preselected. In the API, include a `template` object to use a content template or to personalize your message with [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars). See [Platform Overrides with Templates](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides-with-templates/). Reference a content template by its ID, a UUID returned from Content API responses. You can also reference by external ID when your payload and endpoint support it. In the dashboard, you can get IDs from [your list of templates](#managing-content-templates). Exact property names and formats appear in the schema for your channel and operation. For push, specify the template on the [Notification object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#notificationobject). [In-App Automation](https://www.airship.com/docs/reference/glossary/#iaa) is not supported for API use. ## Managing content templates Go to **Content** and select **Templates** to view the list of templates in your project. The list displays templates created in the dashboard or with the [Content API](https://www.airship.com/docs/developer/rest-api/ua/operations/content/). You can sort the list by name or date modified, filter by channel or keyword, and search by name, ID, or keyword. Templates without message content are labeled "Empty" and cannot be used until you add content. Select a template name to open a drawer where you can do the following: * For the ID, select the copy icon (clipboard) to copy it to your clipboard. * Edit the name, description, and keywords. Select **Save** after making your changes. * Select **Edit** or **Duplicate**, which are the same as the actions available from the more menu icon (⋯), as described in the table below. * View the names of [Snippets](https://www.airship.com/docs/reference/glossary/#snippet) or [External Data Feeds](https://www.airship.com/docs/reference/glossary/#external_feed) used in the template. * View the date and time when the template was created and last modified. The following actions are available from the more menu icon (⋯) in the template list: | Action | Description | Steps | | --- | --- | --- | | **Edit** | Open the template for editing. You can change the content, name, description, and keywords. | Select the more menu icon (⋯), then **Edit**, make your changes, and then select **Save template**. | | **Duplicate** | Make a copy of the template in the current project or in a different project. You can only duplicate to projects that are configured for the template's channel. MMS templates are not supported.

If duplicating to a different project and the template has dependencies, they are listed along with whether Airship copies each dependency to the destination project. Airship does not copy Segments, Attributes, Custom Events, Deep Links, Subscription Lists, Preference Centers, brand guidelines, or Scene settings. Configure those resources independently in each project. | First, choose to copy to the current project or a different one, and update the name, description, and keywords. Then, select **Duplicate**. | | **View information and access additional actions** | Open the same drawer as when you select a template name. For what you can do in the drawer, see the list above this table. | Select the more menu icon (⋯), then **View detail**. | | **Delete** | Delete the template from your project. Deleting a template that is in use may impact messaging. | Select the more menu icon (⋯), then **Delete**. | | **Copy ID to clipboard** | Copy the template ID to your clipboard. | Select the more menu icon (⋯), then **Copy ID to clipboard**. | | **Create an email** | Create a new message in the [Message composer](https://www.airship.com/docs/guides/messaging/messages/create/) with the template preselected in the Content step. | Select the more menu icon (⋯) for an email template, then **Use template**. | {class="table-col-1-20 table-col-2-40"} # Snippets > A snippet is a reusable piece of content that you can define in Airship for later use in your messages and templates. A single snippet can be used in multiple channels. Snippets support text or HTML content and can be used for commonly used elements such as a copyright, header image, or custom CSS. ## About snippets When you edit a snippet, the changes automatically update anywhere that snippet is in use. For scheduled and recurring messages, resave the message to update the message with the latest version of the snippet. Example uses: * **Company branding** — Create an HTML snippet with your company's colors. When the colors need to be updated, edit the hex values in the snippet. * **Header and footer blocks** — Create HTML snippets for the headers and footers you use for messages across different channels. * **Streamline inserting** [External Data Feeds](https://www.airship.com/docs/reference/glossary/#external_feed) — Create a snippet that references an external data feed. Instead of using the longer format and process used to insert a feed, use the shorter snippet format: `{{>snippet_ID}}`. You create snippets in the dashboard, and you can insert them into your messages and templates using both the dashboard and API. You can assign keywords to your snippets so they are easily found in search. > **Tip:** Add plain text and HTML in the same snippet for email messages so the plain text version appears for clients that do not support HTML, or for content that will be used across multiple channels. > **Note:** Snippets are not supported for Scenes or In-App Automations. ### Snippet format A snippet has three parts: * **Name** — Used for identification in the list of all snippets in your project. * **ID** — Used to reference the snippet in your message. The snippet ID is generated automatically based on the snippet name, though you have the option to change it. * **Content** — The text or HTML that is inserted in your message where the snippet is referenced. ### Personalizing snippets You can use [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) and [External Data Feeds](https://www.airship.com/docs/reference/glossary/#external_feed) to insert [Merge Fields](https://www.airship.com/docs/reference/glossary/#merge_field) and [Dynamic Content](https://www.airship.com/docs/reference/glossary/#dynamic_content) to personalize messages for each individual member of your audience. The dashboard supports previewing content that contains Handlebars. See [Previewing personalized content](https://www.airship.com/docs/guides/personalization/previewing/). You can also pass template variables into a snippet to customize it for a specific message. For example, you might set up a "footer" snippet with a dynamic URL as its content like this: ```html ``` When including the snippet in your message, you can pass in one or more of the variables. You would add the snippet to your message text like this: ```text {{>footer utm_source="email" utm_campaign="newsletter"}} ``` ## Create a snippet You can create a maximum of 1,000 snippets per project. 1. Go to **Content** and select **Snippets**. 1. Select **Create snippet**. 1. Enter a name and description for the snippet. The snippet ID is automatically generated based on the name. * A snippet ID will not generate for a snippet name that contains only numbers and/or special characters. * If the name starts with a number and/or special characters, the generated ID omits the leading numbers and/or special characters. * Uppercase letters in the name are converted to lowercase in the ID. * Special characters in the name are converted to underscores in the ID and only appear if followed by numbers or letters. 1. (Optional) Edit the snippet ID. Letters, numbers, and underscores only, and must start with a letter and end with a letter or number. **You cannot change the snippet ID later.** 1. (Optional) Add keywords to help organize your snippets. Enter a term in the search field and select from results, or select *Add keyword: [term]*. You can add up to 10 keywords. 1. Select **Continue**. 1. Add your plain text and/or HTML content. 1. Select **Add +**. 1. Enter your content. The preview updates as you type. > **Important:** Do not insert a snippet into another snippet. 1. Select **Done**. 1. Select **Save snippet** when you are done adding content. ## Insert a snippet You can insert a snippet anywhere that supports [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) by entering its ID in the format `{{>snippet_ID}}`. > **Note:** Make sure that the location where you are using a snippet supports the content defined in the snippet. For example, an image URL or HTML will not render in a push notification since push notifications only support plain text. When using the [Interactive Editor](https://www.airship.com/docs/reference/glossary/#interactive_editor) and pasting or uploading HTML, a snippet is immediately rendered in the preview. If using layouts, you can insert snippets into text or HTML content, but they will not render in the editor. You must select **Done** to see the preview. To include a snippet in an API request, reference the snippet by name in the `snippet_references` object, and invoke the snippet in your notification text. The example below loads a "signature" snippet and adds it to the end of the message. ```http POST /attachments HTTP/1.1 Authorization: Bearer Content-Type: application/json Accept: application/vnd.urbanairship+json; version=3 { "notification": { "alert": "Hi {{ name }}: Thanks for your purchase! {{> signature }}" }, "snippet_references": { "snippets": [ { "name": "signature" } ] } } ``` See [Snippet references object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#snippetreferences) in our API reference for details. ## Managing snippets Go to **Content** and select **Snippets** to view the list of snippets in your project. Your last modified snippet is listed first. You can sort the list by name, snippet ID, or date modified. You can search by name, snippet ID, or keyword. Snippets that are used in template content are labeled "In use". Select a snippet name to open a drawer where you can do the following: * For the ID, select the copy icon (clipboard) to copy it to your clipboard. * Edit the name, description, and keywords. Select **Save** after making your changes. * Select **Edit** or **Duplicate**, which are the same as the actions available from the more menu icon (⋯), as described in the table below. * View the names of [Templates](https://www.airship.com/docs/reference/glossary/#template) that reference the snippet. Select a template name to open it. * View the date and time when the snippet was created and last modified. The following actions are available from the more menu icon (⋯) in the snippet list: | Action | Description | Steps | | --- | --- | --- | | **Edit** | Open the snippet for editing. You can change the content, name, description, and keywords.

Snippets are evaluated at send time, so if you schedule a message that contains a snippet and then edit that snippet, the scheduled message automatically uses the updated snippet content. Scheduled includes recurring messages. | Select the more menu icon (⋯), then **Edit**, make your changes, and then select **Save snippet**. | | **Duplicate** | Make a copy of the snippet in the current project or in a different project.

If duplicating to a different project and the snippet has dependencies, they are listed along with whether Airship copies each dependency to the destination project. Airship does not copy Segments, Attributes, Custom Events, Deep Links, Subscription Lists, Preference Centers, brand guidelines, or Scene settings. Configure those resources independently in each project. | First, choose to copy to the current project or a different one, and update the name, ID, description, and keywords. Then, select **Duplicate**. | | **View information and access additional actions** | Open the same drawer as when you select a snippet name. For what you can do in the drawer, see the list above this table. | Select the more menu icon (⋯), then **View detail**. | | **Delete** | Delete the snippet from your project. Deleting a snippet that is in use may impact messaging. | Select the more menu icon (⋯), then **Delete**. | | **Copy ID to clipboard** | Copy the snippet ID to your clipboard. | Select the more menu icon (⋯), then **Copy ID to clipboard**. | {class="table-col-1-20 table-col-2-40"} # Personalize actions and media URLs > Use Airship Handlebars to show personalized media in messages and to send your audience to personalized URLs when they interact with your messages. You can personalize [Actions](https://www.airship.com/docs/reference/glossary/#action) using [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars), taking advantage of information specific to your audience to personalize your audience's experience when they interact with your message. Use properties from a custom event, attributes, or an uploaded list to personalize [adaptive links](https://www.airship.com/docs/reference/glossary/#adaptive_link), send your audience to their own, specific deep link, or send users to their accounts with web links. You should set a default value when personalizing actions to ensure that personalized actions resolve correctly, even if the value you use to personalize the action does not exist. ## Deep links A deep link sends a user to a specific location in your app. If you've configured deep link templates — deep links with merge fields that you can personalize for your audience – you can fill templated fields in the path of your deep link with personalizable properties. For example, if your deep link template is: ```text https://example.com/products/{{category}}/{{product_id}} ``` You would see the `category` and `product_id` fields when adding a deep link action. You could personalize the deep link with values from the event or data that you use to personalize your message. ![Personalizing a deep link action with Handlebars](https://www.airship.com/docs/images/deep-link-personalized_hu_dc46d077ec6fc0a8.webp) *Personalizing a deep link action with Handlebars* ## Adaptive links [Adaptive links](https://www.airship.com/docs/reference/glossary/#adaptive_link) take query parameters that personalize information in the pass that each member of your audience installs. For example, if you want to add a user's name, captured in a custom event, to a pass, you might add `?name={{name}}` to the adaptive link URL. When sending a message supporting personalization, you can reuse personalizable information in the adaptive link to personalize the passes that your audience receives. In general, you'll need to know the field names in your Wallet templates, and then use Handlebars to produce the values that you want to add to the pass that your audience installs. > **Note:** Personalization in adaptive links uses merge fields only and does not allow for personalization features such as logic, default values, or snippets. Click *Add another field* to add additional parameters to your adaptive link. ![Personalizing an adaptive link with merge fields](https://www.airship.com/docs/images/adaptive-link-personalized_hu_f650d03ec0e95cf0.webp) *Personalizing an adaptive link with merge fields* ## Web Page and Landing Page actions The Web Page and [Landing Page](https://www.airship.com/docs/guides/features/messaging/landing-pages/) actions require entering a URL for the web page you want to open or the content to display in a landing page. You can personalize these URLs to send your audience to a relevant page or display specific content when they interact with your message. Insert handlebars in your URL to personalize the URL for each individual recipient. You might want to personalize URLs to send a user to their account login or the page of a product they visited. **Example URL**: `https://www.example.com/user?id={{name}}&cart={{cart_id}}` > **Note:** Standard Handlebars syntax (using double curly braces) is URL encoded, so you can personalize individual parameters in a URL. Use triple braces to escape URL encoding and personalize a complete URL — `{{{url}}}`. ## Share action text You can personalize the Share Text that users will see when they share your message on social networks. This can help you shape the way your audience talks about you, and make it easier for your audience to share their experiences with their groups of friends. ```text I highly recommend {{product.name}} from Cool Company! Go to {{product.url}} to get one! ``` ## Message Center actions Message Center actions support the same personalization sources as a message itself. For example, if you set up an automation based on a custom event trigger, such as an abandoned cart, to send a push notification with a Message Center action, you can personalize both the push notification and the Message Center message from the same information in your custom event. When you select a Message Center action, you can select a template or create a new page in our Interactive Editor. See [Content Templates](https://www.airship.com/docs/guides/personalization/content/templates/) to learn how to create Message Center templates. When using the Interactive editor layouts for email, landing page, Message Center, and in-app automation content, you can assign an action that occurs when a user taps a button, link, or image in the message. You can use [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) to personalize the actions. See [Actions in the Interactive editor](https://www.airship.com/docs/guides/messaging/editors/interactive/actions/). ## Media URLs If you host your own media, you can use [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) to personalize media URLs, helping you maximize the impact of your messages for each member of your audience. You can personalize media URLs using custom event properties, [Attributes](https://www.airship.com/docs/reference/glossary/#attributes), and custom properties in [Create and Send](https://www.airship.com/docs/reference/glossary/#create_and_send) CSV files. You should use the default handler (`$def`) when personalizing media URLs to make sure the URL resolves appropriately if variables in the URL are empty or don't exist. **Example personalized media URL** ```text https://example.com/{{$def user_profile_image "default.png"}} ``` See also: [Media guidelines](https://www.airship.com/docs/reference/messages/media-guidelines/). ## Handlebars Reference Airship uses the Handlebars language to support personalization of messaging content. # Handlebars basics > Airship uses the [Handlebars](https://handlebarsjs.com/) templating language to support personalization of messaging content. > **Note:** This is a reference for using Handlebars as implemented in Airship's features, not for the Handlebars project. We do not support all features described in handlebarsjs.com, only those included in this documentation. > **Tip:** Airship has built-in tools that simplify entering Handlebars expressions. See: [Dashboard tools for Handlebars](https://www.airship.com/docs/guides/personalization/simplifying-handlebars/). ## Syntax Handlebars syntax works by wrapping a variable in curly braces. Merge fields are the most basic Handlebars expression. Insert your merge field into content by wrapping the name of your field in curly braces, e.g., `{{firstname}}`. Airship replaces the merge field (and braces) with the data specified by the merge field at send time. > **Note:** Variables that start with a number require a special syntax, using either brackets `[]` or the `lookup` helper. For example, to use a variable named `42_answer`, use `{{ [42_answer] }}` or `{{lookup . "42_answer"}}`. ### Block expressions Block expressions are boolean-returning helpers that conditionally render blocks of content if their evaluation is true. These helpers use a `#` preceding the helper name and require a matching closing bracket of the same name with a `/` in front of it. See examples in [Logic/conditional statements](https://www.airship.com/docs/guides/personalization/handlebars/logic-helpers/). ## URL encoding In HTML for email, Message Center, In-App Automation, and landing pages, values in Handlebars statements are not automatically URL-encoded. For example, if an email link includes characters that are not URL-safe and you haven't used the `urlEncode` helper, the invalid link may be replaced with a default URL, such as `https://example.com`. When writing your own custom HTML or in any other situation where you might want to URL encode variables, you can use the `urlEncode` helper: `{{urlEncode var_name}}`. Image and "link" properties in the [Interactive editor](https://www.airship.com/docs/guides/messaging/editors/interactive/) and message actions expect URLs. For those fields, Airship automatically URL-encodes values in Handlebars expressions to ensure valid URLs. **When to use double braces `{{var}}`:** Use for variables that need URL encoding, such as user-generated content that might contain spaces or special characters. - Example: `https://www.example.com/user/{{user_name}}/{{cart_id}}` - If `user_name` is `John Smith`, this becomes `https://www.example.com/user/John%20Smith/123` **When to use triple braces `{{{var}}}`:** Use for variables that contain complete URLs or are already URL-safe to avoid double-encoding. - Example: `{{{redirect_url}}}` - If `redirect_url` is already `https://example.com/page%20one`, it remains unchanged **Avoiding double-encoding:** Be careful not to URL-encode values that are already encoded, as this creates invalid URLs. For example, `{{"my%20value"}}` becomes the double-encoded "my%2520value" instead of the correct "my%20value". **URL encoding push example** ```json { "audience": { "named_user": "one_user" }, "notification": { "android": { "alert": "Hi {{value_nospace}} go to https://www.example.com/{{urlEncode value_with_space}}!" }, "actions": { "open": { "type": "url", "content": "{{{value_full_url}}}" } } }, "device_types": [ "android" ] } ``` ## Default handler {#def} Use `$def` to set a default value for one or more merge field variables, so that your message will still make sense if the field is empty or doesn't exist. When using more than one variable, the handler will use the first variable value that isn't empty. When using a default, provide both: * The parameters you want to use in your message * The default value if your variables are empty In addition to a variable or value such as a string or a number, expressions that produce a value can also be used in the handler. For example, `($capitalize first_name)` will return the string value of `first_name` with the first letter capitalized. **Properties**: * `parameter` — The value or expression/variable name whose value will be used if it exists and isn't empty. * `value` — A string to be used as a fallback default if all preceding variables or expressions were empty or did not exist. | Format | Example | Output | | --- | --- | --- | | `{{$def parameter1 parameter2 parameter3... "value if keys are empty"}}` | `{{$def name "you"}}` | Welcome you! | ## Object and array notation {#dot-notation} The user data and merge fields you use to populate a template are often nested in objects or arrays — like when using [Custom Events](https://www.airship.com/docs/developer/rest-api/ua/operations/custom-events/#addcustomevents) to populate a template in an automation rule. Use standard dot notation to access properties in objects or an item/index in an array. For example, you could access a `productName` property in a `suggestedProduct` object using `suggestedProduct.productName`. Use the array index to access an item at a particular location in an array. For example, if your message includes an array of objects called `suggestedProducts` for each member of your audience, you can access the first suggested product with `suggestedProducts.[0]` (array index 0). > **Note:** Array indexes start at 0. If you want to reference a key called `image` belonging to the first suggested product in the array, you would use `suggestedProducts.[0].image`. ### Object and array notation for Create and Send For [Create and Send](https://www.airship.com/docs/reference/glossary/#create_and_send) CSVs, you can include complex arrays and objects in your CSV using object notation to represent object properties in the header. All values in CSV uploads, including numbers, are represented as strings and cannot be used with [math helpers](https://www.airship.com/docs/guides/personalization/handlebars/math-helpers/). When referencing an array index, **you must wrap the array index in brackets**. Your CSV should include headers for each item in the array and each property in the object that you want to reference in your message. For example, if you want to use an array of objects called `items` for each audience member in your CSV, your CSV will include `items.[#]` for each item in the array. If each object in the array had `name`, `image`, and `url` properties, you would add `items.[0].name, items.[0].image, items.[0].url` to your CSV, and reference additional objects in the array by incrementing the index (e.g. `items.[1]` and so on). > **Note:** Array indexes start at 0. If you wanted to personalize an email to members of your audience based on their addresses and the names of items they're interested in, you might format your CSV as follows: ```text ua_address,ua_commercial_opted_in,name,address.city,address.state,items.[0].name,items.[1].name someone@example.com,2018-04-01T18:45:30,Joe Someone,Portland,OR,Rubber Gloves,Bleach Alternative else@example.com,2018-04-21T16:13:01,Sir Else,Seattle,WA,Flashlight,Shovel ``` # Text helpers > Text helpers transform text content in Handlebars expressions. ## Limit string length (Truncate) {#truncate} `truncate` — Limits string length. **Properties** * `string` — The string, string variable, or expression that produces a string whose length you want to limit. * `max_length` — An integer representing the maximum string length. * `replacement_suffix` — An optional string you want to append to the end of the truncated string if, and only if, Airship truncates the string. | Format | Example | Output | |---|---|---| | `{{truncate [replacement_suffix]}}` | How did you like your`{{truncate product_name 5 "..."}}`? | How did you like your produ...? | > **Note:** The `max_length` includes the replacement suffix, so if you set a `max_length` of 8, and a `replacement_suffix` with 3 characters (e.g. `...`), Airship will only display 5 characters from a truncated string; the remaining 3 characters of any truncated string are consumed by the `replacement_suffix`. > > For example, if you have an attribute ID `name` with value `yourname`, `{{truncate name 7 "---"}}` would resolve to `your---`. Without the suffix, the same property would truncate to `yournam`. ## Contains `$contains` — Checks whether a string `target` contains string `searchFor`. **Properties** * `target` — The string or user-specific variable to search. * `searchFor` — The string to find within `target`. | Format | Example | Output | |---|---|---| | `{{#if ($contains target searchFor)}}...{{else}}...{{/if}}` | `{{#if ($contains "some string context" "str")}}true{{else}}false{{/if}}` | `true` | ## Replace `$replace` — Replaces all occurrences of `searchFor` with `replaceWith` in string `target`. **Properties** * `target` — The string or user-specific variable to be searched. * `searchFor` — A string within the `target` to be changed. * `replaceWith` — The intended change to `target` if `searchFor` returns true. * `limit` — Optional. Replaces only the first `X` occurrences. | Format | Example | Output | |---|---|---| | `{{$replace target searchFor replaceWith [limit=]}}` | `{{$replace "Some terrible jawn." "terrible" "cool"}}` | Some cool jawn. | ## Split `$split` — Splits a string `target` on a given string `delimiter`, returning an array of strings split on the characters in `delimiter`. You may also specify a `limit` to only split a string a maximum number of times. **Properties** * `target` — The string to be split. * `delimiter` — The characters on which to split. * `limit` — The maximum number of splits to perform. | Format | Example | Output | |---|---|---| | `{{$split target delimiter index}}` | `{{$split "a,b,c" ","}}`
`{{$split "a,b,c" "," 1}}` | `["a", "b", "c"]`
`["a", "b,c"]` | ## Join `$join` — Joins an array `target` using a given string `delimeter`, outputting a single string. **Properties** * `target` — The array to be joined. An error will be thrown if this parameter is any type other than an array. * `delimeter` — A string to insert between the elements in the `target` array. | Format | Example | Output | |---|---|---| | `{{$join target delimeter}}` | `{{$join ($array "red" "green" "blue") "-"}}`
`{{$join ($array 1 2 3) ", "}}` | `red-green-blue`
`1, 2, 3` | ## Trim `$trim` — Trims whitespace from both ends of a string. **Properties** * `target` — The string to be trimmed. | Format | Example | Output | |---|---|---| | `{{$trim target}}` | `{{$trim " abc "}}` | `abc` | ## Splice `$splice` — Inserts string `insertContent` into string target at index `index`. **Properties** * `target` — The string or user-specific variable to be searched. * `index` — The count from start of `target` to point of replacement. Note `index` lengths are 0-indexed. * `insertContent` — The string or content to be inserted at the `index` point. * `deleteCount` — Optional. Deletes `X` characters from `index`. | Format | Example | Output | |---|---|---| | `{{$splice target index insertContent deleteCount=X}}` | `{{$splice "xxxzzz" 3 "yyy"}}`
`{{$splice "xxxzzz" 1 "y" deleteCount=4}}` | xxxyyyzzz
xyz | ## Capitalize `$capitalize` — Capitalizes the first letter of a given string. **Properties** * `string` — The string or user-specific variable to capitalize. * `locale` — Optional. An IETF BCP 47 language tag. | Format | Example | Output | |---|---|---| | `{{$capitalize string locale=DEFAULT}}` | `{{$capitalize "word"}}` | Word | ## Uppercase `$uppercase ` — Converts a given string to uppercase. **Properties** * `string` — The string or user-specific variable to uppercase. * `locale` — Optional. An IETF BCP 47 language tag. | Format | Example | Output | |---|---|---| | `{{$uppercase string locale=DEFAULT}}` | `{{$uppercase "word"}}` | WORD | ## Lowercase `$lowercase` — Converts a given string to lowercase. **Properties** * `string` — The string or user-specific variable to lowercase. * `locale` — Optional. An IETF BCP 47 language tag. | Format | Example | Output | |---|---|---| | `{{$lowercase string locale=DEFAULT}}` | `{{$lowercase "Word"}}` | word | ## Format number `$numberFormat` — Converts a number to a given `locale` and `precision`. **Properties** * `number` — The number to be formatted. * `locale` — An IETF BCP 47 language tag, e.g., `en-US`. * `precision` — Optional. An integer specifying the maximum number of decimal places that should be shown. | Format | Example | Output | |---|---|---| | `{{$numberFormat number locale=locale precision=precision}}` | `{{$numberFormat 100000 locale="en-US" }}`
`{{$numberFormat 100000.012 locale="en-US" precision=2}}`
`{{$numberFormat 1000 locale="da-DK" precision=2}}` | 100,000
100,000.01
1.000,00 | ## Format currency `$currencyformat` — Converts a number into a currency representation for the given `locale` and `precision`. **Properties** * `number` — The number to be formatted. * `locale` — An IETF BCP 47 language tag, e.g., `en-US`. * `precision` — Optional. An integer specifying the maximum number of decimal places that should be shown. | Format | Example | Output | |---|---|---| | `{{$currencyFormat number locale=locale precision=precision}}` | `{{$currencyFormat 100.129 locale="en-US"}}`
`{{$currencyFormat 120.999 locale="da-DK"}}` | $ 100.13
€ 120,99 | # Math helpers > Math helpers transform number or integer content in Handlebars expressions. > **Important:** * These helpers work only with numbers and properties that resolve to numbers or integers. All values in CSV uploads, including numbers, are represented as strings and cannot be used with math helpers. This is especially relevant when using [Bulk sending](https://www.airship.com/docs/guides/audience/segmentation/bulk-sending/) methods. > > * You cannot nest math operations or use other helpers inside a math helper. For example, you cannot use `$def` to set a default value for a property in a math helper. > > * Take care to use properties that you know exist and have number/integer values, so that your message makes sense to your audience. ### Add {#add-helper} `add` — Adds `number`. Add helpers take an unlimited number of arguments. For example, `{{add 5 2 2}}` resolves to `9`. **Properties** * `number` — Variable that contains a number value, no limit on values that can be added for a sum total. You can add as many numbers as fits your message. | Format | Example | Output | |---|---|---| | `{{add number1 number2...}}` | You can have `{{add slices_per_person left_overs}}` slices of pizza! | You can have 3 slices of pizza! | ### Subtract `sub` — Subtracts `number`. Subtract helpers take an unlimited number of arguments. For example, `{{sub 5 2 2}}` resolves to `1`. You can subtract as many numbers as fits your message. **Properties** * `number` — Variable that contains a number value or an integer. | Format | Example | Output | |---|---|---| | `{{sub number1 number2...}}` | You can have `{{sub slices_per_person left_overs}}` more slice of pizza! | You can have 1 more slice of pizza! | ### Multiply `mul` — Multiplies `number`. Multiplication helpers take an unlimited number of arguments. For example, `{{mul 4 2 2}}` resolves to `16`. **Properties** * `number` — Variable that contains a number value or an integer. | Format | Example | Output | |---|---|---| | `{{mul number1 number2...}}` | You need `{{mul slices_per_person people}}` total pizza slices for the party. | You need 36 total pizza slices for the party. | ### Divide `div` — Divides `number`. Division helpers take an unlimited number of arguments. For example, `{{div 4 2 2}}` resolves to `1`. **Properties** * `number` — Variable that contains a number value or an integer. | Format | Example | Output | |---|---|---| | `{{div number1 number2...}}` | Pizza partiers can each have `{{div total_slices people}} ` slices. | Pizza partiers can each have 3 slices. | ### Modulo `mod` — Returns the remainder when dividing two `number`. This handler takes exactly two arguments. **Properties** * `number` — Variable that contains a number value or an integer. | Format | Example | Output | |---|---|---| | `{{mod number1 number2}}` | Pizza slices up for grabs: `{{mod total_slices slices_per_person}}` | Pizza slices up for grabs: 2 | ### Rounding numbers `round` — Rounds `number` to the nearest integer. You can round to the nearest decimal place by adding second argument determining the decimal place you want to round to. **Properties** * `number` — Variable that contains a number value or an integer. * `place` — Optional. Numerical value to round to a decimal place. **Round to the nearest integer** | Format | Example | Output | |---|---|---| | `{{round number}}` | Pi is pretty close to `{{round pi}}` | Pi is pretty close to 3 | **Round to a decimal place** | Format | Example | Output | |---|---|---| | `{{mod number place}}` | Pi is pretty close to `{{round pi 2}}` | Pi is pretty close to 3.14 | ### Ceiling `$ceil` — Rounds up to the nearest integer. **Properties** * `number` — The number to be rounded. | Format | Example | Output | |---|---|---| | `{{$ceil number}}` | `{{$ceil 3.001}}` | 4 | ### Floor `$floor` — Rounds down to the nearest integer. **Properties** * `number` — The number to be rounded. | Format | Example | Output | |---|---|---| | `{{$floor number}}` | `{{$floor 3.999}}` | 3 | # Date and time helpers > Date and time helpers transform content in Handlebars expressions. ## Formatting dates and times You can format dates and times using helpers `dateFormat`, `timeFormat`, and `datetimeformat`. Each helper takes the arguments [`date-time`](#date-time), [`locale`](#locale), [`format`](#format), and [`pattern`](#pattern). Following the helper, you must include a `date-time` and `locale`. For example, `{{dateFormat "1983-01-31T12:36:35" locale="en-US"}}`. The `format` and `pattern` arguments are optional. Examples of each helper: | Helper | Rendered as | Example | Output | | --- | --- | --- | --- | | dateFormat | Date | `{{dateFormat "1983-01-31T12:36:35" locale="en-US"}}` | Jan 31, 1983 | | timeFormat | Time | `{{timeFormat "1983-01-31T12:36:35" locale="en-US"}}` | 12:36:35 PM | | datetimeFormat | Date and time | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US"}}` | Jan 31, 1983, 12:36:35 PM | You can also provide date-times using Date [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) instead of a date-time string. When using Attributes, do not enclose them in quotes. These examples show how the `locale`, `format`, and `pattern` arguments can transform a date-time, using a Date [Attribute](https://www.airship.com/docs/reference/glossary/#attributes) `birthdate` with value January 31, 1983, for the date-time: | Argument | Example | Output | | --- | --- | --- | | Format using the `locale` argument only. | `{{dateFormat birthdate locale="en-US"}}` | Jan 31, 1983 | | Add the `format` argument to specify a shorter, numeric-only output. | `{{dateFormat birthdate locale="en-US" format="SHORT"}}` | 1/31/1983 | | Change the `format` value to `FULL` to expand "Jan" to "January" and include the day of the week. | `{{dateFormat birthdate locale="en-US" format="FULL"}}` | Monday, January 31, 1983 | | Use the `pattern` argument to specify custom formatting. | `{{dateFormat birthdate locale="en-US" pattern="d MMMM yyyy"}}` | 31 January 1983 | | Change the `locale` value to change the language. The example here changes the output French. | `{{dateFormat birthdate locale="fr-FR" pattern="d MMMM yyyy"}}` | 31 janvier 1983 | ### Date-time An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)-formatted string — Required. In general, we recommend that you use uniform date-time values since they work for all three helpers, but the actual date-time string requirements vary per helper. You can add `[Offset]` to specify the time zone offset from UTC. Use `Z` for UTC, or one of the following formats `±00:00`, `±0000`, `±00`. For example, `-08:00`, `-0800`, or `-08` for PST. Date-time format per helper, with optional parts of the string in brackets: | Helper | Optional data | Date-time format | | --- | --- | --- | | dateFormat | Time | `yyyy-MM-dd[' ']['T']HH:mm[:ss[.SSS][Offset]]` | | timeFormat | Date | `[yyyy-MM-dd[' ']['T']]HH:mm[:ss[.SSS]][Offset]` | | datetimeFormat | n/a | `yyyy-MM-dd[' ']['T']HH:mm[:ss[.SSS]][Offset]` | ### Locale `locale` — Required. Date and time helper argument that renders the date and time according to the user's 2-letter language and country codes, joined by a dash. Format example: `locale="en-US"`. The language is an [ISO 639](https://en.wikipedia.org/wiki/ISO_639) code. The country is an [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code. | Example | Output | | --- | --- | | `{{dateFormat "1983-01-31T12:36:35" locale="en-US"}}` | Jan 31, 1983 | | `{{dateFormat "1983-01-31T12:36:35" locale="fr-FR"}}` | 31 janv. 1983 | | `{{dateFormat "1983-01-31T12:36:35" locale="de-DE"}}` | 31.01.1983 | | `{{timeFormat "1983-01-31T12:36:35" locale="en-US"}}` | 12:36:35 PM | | `{{timeFormat "1983-01-31T12:36:35" locale="fr-FR"}}` | 12:36:35 | | `{{timeFormat "1983-01-31T12:36:35" locale="de-DE"}}` | 12:36:35 | | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US"}}` | Jan 31, 1983, 12:36:35 PM | | `{{datetimeFormat "1983-01-31T12:36:35" locale="fr-FR"}}` | 31 janv. 1983 à 12:36:35 | | `{{datetimeFormat "1983-01-31T12:36:35" locale="de-DE"}}` | 31.01.1983, 12:36:35 | Locale values can also be provided using Date [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) instead of an actual date-time string. > **Tip:** The Airship SDK gathers `ua_language` and `ua_country` and stores them as Default [Attributes](https://www.airship.com/docs/reference/glossary/#attributes) for your app audience. You can use the Attributes as `locale` values. ### Format `format` — Optional. Date and time helper argument that determines how to display the date-time value. One of `FULL`, `LONG`, `MEDIUM`, or `SHORT`. Format example: `format="SHORT"`. `FULL` and `LONG` formats include time zone information. If you don't set a time zone value in your date-time string, Airship assumes `Z` (UTC/GMT). `format` cannot be used in combination with `pattern`. The following tables show examples and output of `format` for each date and time helper. `dateFormat` examples: | Format | Example | Output | | --- | --- | --- | | SHORT | `{{dateFormat "1983-01-31T12:36:35" locale="en-US" format="SHORT"}}` | 1/31/1983 | | MEDIUM | `{{dateFormat "1983-01-31T12:36:35" locale="en-US" format="MEDIUM"}}` | Jan 31, 1983 | | LONG | `{{dateFormat "1983-01-31T12:36:35" locale="en-US" format="LONG"}}` | January 31, 1983 | | FULL | `{{dateFormat "1983-01-31T12:36:35" locale="en-US" format="FULL"}}` | Tuesday, January 31, 1983 | `timeFormat` examples: | Format | Example | Output | | --- | --- | --- | | SHORT | `{{timeFormat "1983-01-31T12:36:35" locale="en-US" format="SHORT"}}` | 12:36 PM | | MEDIUM | `{{timeFormat "1983-01-31T12:36:35" locale="en-US" format="MEDIUM"}}` | 12:36:35 PM | | LONG | `{{timeFormat "1983-01-31T12:36:35" locale="en-US" format="LONG"}}` | 12:36:35 PM Z | | FULL | `{{timeFormat "1983-01-31T12:36:35" locale="en-US" format="FULL"}}` | 12:36:35 PM Z | `datetimeFormat` examples: | Format | Example | Output | | --- | --- | --- | | SHORT | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US" format="SHORT"}}` | 1/31/1983 12:36 PM | | MEDIUM | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US" format="MEDIUM"}}` | Jan 31, 1983 12:36:35 PM | | LONG | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US" format="LONG"}}` | January 31, 1983 12:36:35 PM Z | | FULL | `{{datetimeFormat "1983-01-31T12:36:35" locale="en-US" format="FULL"}}` | Tuesday, January 31, 1983 12:36:35 PM Z | ### Pattern `pattern` — Optional. Date and time helper argument that specifies a custom formatting pattern. Format example: `pattern="MMM d, yyyy"`. Only the parts that are relevant to the type of formatting are allowed. For example, date parts are not allowed within `timeFormat`. Cannot be used in combination with `format`. The most common `pattern` specifiers: | Specifier | Output | Example | |-----------|----------------------------|-------------------------------------| | y | Year | `yyyy` for `2023`, `yy` for `23` | | M | Month | `M` for `4`, `MMMM` for `April` | | d | Day of Month | `d` for `5`, `dd` for `05` | | e | Day of Week | `e` for `2`, `eeee` for `Wednesday` | | a | AM/PM | `a` for `PM` | | h | Hour (1-12) | `hh` for `12` | | K | Hour (0-11) | `KK` for `11` | | k | Hour (1-24) | `kk` for `12` | | H | Hour (0-23) | `HH` for `11` | | m | Minute | `mm` for `30` | | s | Second | `ss` for `55` | | V | Time Zone ID | `VV` for `America/Los_Angeles` | | z | Time Zone Name | `z` for `PDT` | | O | Localized Zone Offset | `O` for `GMT+8` | | X | Offset 'Z' for zero | `X` for `Z`, `XXX` for `-07:00` | | Z | Offset | `Z` for `-0700` | The above specifiers adhere to the following rules: 1. When specifying numbers that may include a leading zero, a single character will omit the leading zero and double characters will include the leading zero. For example, for the fourth hour: * `h` will output `4` * `hh` will output `04` 1. When specifying text output, 1 to 3 characters specify shortened forms, four characters specifies the full form, five characters specifies the narrow form. For example, for the month of April with `en-US` locale: * `M` will output `4` * `MM` will output `04` * `MMM` will output `Apr` * `MMMM` will output `April` * `MMMMM` will output `A` 1. Literal values may be specified in square brackets. For example, `['T']` will yield `T`. Additional formatting options are available and follow the [Java `DateTimeFormatter` rules](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html). Example use and output of `pattern` for each date and time helper: | Helper | Example | Output | | --- | --- | --- | | dateFormat | `{{dateFormat "2023-03-29T13:30:10" locale="en-US" pattern="MMM d, yyyy"}}` | Mar 29, 2023 | | dateFormat | `{{dateFormat "2023-03-29T13:30:10" locale="de-DE" pattern="d MMMM yyyy"}}` | 29 März 2023 | | timeFormat | `{{timeFormat "13:30:10" locale="en-US" pattern="h:mma"}}` | 1:30PM | | datetimeFormat | `{{datetimeFormat "2023-03-29T13:30:10" locale="en-US" pattern="yyyy-MM-dd['T']HH:mm:ss"}}` | 2023-03-29T13:30:10 | ## Determining today, yesterday, or tomorrow Return `true` or `false` based on whether a given date is today, yesterday, or tomorrow relative to the current time in UTC: * `{{$isToday ""}}` * `{{$isYesterday ""}}` * `{{$isTomorrow ""}}` Dates must be formatted as `yyyy-mm-dd` or as a [date-time](#date-time). These helpers are generally used within an evaluation only rather than displaying the result. You can combine them with conditional operators to return true or false in a helper block. You can also use them with a preceding `#` and a closing `/` to create a separate block: ```text {{#if ($isToday birthdate)}} Happy birthday, {{name}} {{/if}} ``` ```text {{#$isToday birthdate}} Happy birthday, {{name}} {{/$isToday}} ``` ## Getting date and time Return the current date, or date and time, in UTC: * `{{$now}}` — Returns the current date and time in UTC in [datetimeFormat](#formatting-dates-and-times) (`yyyy-MM-ddTHH:mm:ssZ`). * `{{$today}}` — Returns the current date in [dateFormat](#formatting-dates-and-times) (`yyyy-MM-dd`). Example use in a purchase order: ```text Purchase Date: {{$today}} // Using `timeFormat` to remove the date to get only the time: Your order was received at {{timeFormat ($now) format="SHORT" locale="en-US"}}. ``` ## Calculating date and time Return an age, future date, or time between dates: | Helper and format | Description | | --- | --- | | `{{$age ""}}` | Returns an age as of today for a given date. | | `{{$addTo "" timeUnit=""}}` | Returns a future date for a given date by adding an amount of time in `days`, `weeks`, `months`, or `years` to a given date-time. `timeUnit` is optional and defaults to days when omitted. Time units are not case-sensitive. | | `{{$daysBetween "" ""}}` | Returns the number of days between two dates. | | `{{$weekBetween "" ""}}` | Returns the number of weeks between two dates. | | `{{$timeBetween "" "" timeUnit="}}` | Returns the number of `days`, `weeks`, `months`, or `years` between two dates. `timeUnit` is optional and defaults to days when omitted. Time units are not case-sensitive. | Dates must be formatted as `yyyy-mm-dd` or as a [date-time](#date-time). The following examples assume today's date is March 25, 2024, and some also use the [`$today`](#getting-date-and-time) and [`pattern`](#pattern) helpers: | Example | Output | | --- | --- | | `{{$age "1970-06-01"}}` | 53 | | `{{dateFormat ($addTo ($today) 7 timeUnit="days") locale="en-US"}}` | Apr 1, 2024 | | `{{dateFormat ($addTo ($today) 7 timeUnit="days") locale="en-US" pattern="dd-MM-yy"}}` | 01-04-24 | | `{{$daysBetween "2024-12-19" "2025-12-19"}}` | 365 | | `{{$weeksBetween "2024-12-19" "2025-12-19"}}` | 52 | | `{{$timeBetween "2024-12-19" "2025-12-19"}}` | 365 | | `{{$timeBetween "2024-12-19" "2025-12-19" timeUnit="days"}}` | 365 | | `{{$timeBetween "2024-12-19" "2025-12-19" timeUnit="weeks"}}` | 52 | | `{{$timeBetween "2024-12-19" "2025-12-19" timeUnit="months"}}` | 12 | | `{{$timeBetween "2024-12-19" "2025-12-19" timeUnit="years"}}` | 1 | For `$daysBetween`, `$weekBetween`, and `$timeBetween`: * If the second date is earlier than the first date, the result will be a negative number. * The calculation returns a whole number, representing the number of complete time units between the two dates. For example, if the time unit is `days` and there are 30 hours between the two dates, then the output will be "1". Example usage in messages: | Example | Output | | --- | --- | | `Your membership expires in three days, on {{dateFormat ($addTo ($today) 3 timeUnit="days") format="SHORT" locale="en-US"}}.` | Your membership expires in three days, on 3/28/24. | | `There are {{$daysBetween ($today) "2021-12-25"}} days left until Christmas!` | There are -821 days left until Christmas! | | `There are {{$daysBetween ($today) "2035-12-25"}} days left until Christmas 2035!` | There are 4292 days left until Christmas 2035! | | `There are {{$timeBetween "2024-01-29T12:36:35" "2024-01-31T12:36:35" springSaleEnds}} days left in our Spring Sale!` | There are 2 days left in our Spring Sale! | ## Transforming a date to another time zone Transform a date to another time zone using `{{$toTimezone "" ""}}`. Dates must be formatted as `yyyy-mm-dd` or as a [date-time](#date-time). The `timezone_id` is the text identifier for the time zone. For example, `America/Los_Angeles`. See the [TZ column](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) in the list of Time Zone abbreviations. The first example in each pair uses standard formatting, and the second adds the [`datetimeFormat` helper](#formatting-dates-and-times) and [`format` argument](#format) to give a more localized time: | Example | Output | | --- | --- | | `{{$toTimezone "2023-04-05T15:24:40Z" "America/Los_Angeles"}}` | 2023-04-05T08:24:40-07:00[America/Los_Angeles] | | `{{datetimeFormat ($toTimezone "2023-04-05T15:24:40Z" "America/Los_Angeles") locale="en-US" format="LONG"}}` | April 5, 2023 at 8:24:40 AM PDT | | `{{$toTimezone "2023-04-05T15:24:40Z" "America/Jamaica"}}` | 2023-04-05T10:24:40-05:00[America/Jamaica] | | `{{datetimeFormat ($toTimezone "2023-04-05T15:24:40Z" "America/Jamaica") locale="en-US" format="LONG"}}` | April 5, 2023 at 10:24:40 AM EST | # Logic/conditional statements > Logic helpers allow you to place conditional requirements in Handlebars expressions. Logic statements can use one or more of these properties to evaluate a condition: * `value` — A literal string or number (e.g. "Hello" or 3) * `attribute` — The string ID/Name of a user-specific data * `expression` — A helper that can use other helpers, attributes, and values to return a value ## If/Else `if`, `else`, `else if` — If/else statements can conditionally render blocks of content. *If* and *else if* statements render content when the condition is true, and *else* renders content when all previous conditions in a [block expression](https://www.airship.com/docs/guides/personalization/handlebars/basics/#block-expressions) are false. Use `if` explicitly to test that a variable exists, is not empty, or is not 0. Otherwise, any condition that evaluates a variable acts as an implicit *if* statement. For example, you must explicitly declare `if` when testing whether or not the `fav_movie` variable exists, is not empty, and is not 0. ```text {{#if fav_movie}} Your favorite movie is {{fav_movie}}. {{else}} Maybe you'd enjoy a book instead. {{/if}} ``` When testing the value of the `fav_movie` variable, you can use other logic helpers such as [Equal](#equal) and combine them with else statements. ```text {{#eq fav_movie "Indiana Jones"}}Did you like The Last Crusade or Temple of Doom? {{else eq fav_movie "Star Wars"}}Are you into the OG trilogy or are you a prequel person? {{else}}What's your favorite book?{{/eq}} ``` You can chain multiple `else` statements — similar to *else if* statements in other languages — to test a series of variables. Generally, the last `else` statement contains no parameters and determines what happens if all previous conditions fail. ## And/Or `and`, `or` — These operators help you string together complex conditions to show the right content to the right members of your audience. > **Note:** `and` renders a block of content if **all** conditions are true. > > `or` renders a block of content if **any** of the conditions are true. | Format | Example | Output | |---|---|---| | `{{#and expression expression}}` | `{{#and (eq genre "action") (eq era "80s") }}
You will definitely like Indiana Jones!
{{/and}}` | If **all** conditions are true, "You will definitely like Indiana Jones!" will display | | `{{#or expression expression}}` | `{{#or (eq genre "action") (eq era "80s") }}
You might be interested in Indiana Jones.
{{/or}}` | If **any** conditions are true, "You might be interested in Indiana Jones. will display | ## Equal `eq` — Equality operators let you test the value of a merge field or the result of an expression — whether it is equal to, or not equal to, specific value. > **Tip:** Equality operators work well with both numeric or string (text) values. But cannot compare between the two value types. In this example, the message is different if the `category` merge field contains the value `Accessories`: ```text {{#eq category "Accessories"}} Enjoy up to $25 off! {{else}} Get up to $75 off! {{/eq}} ``` | Format | Example | Output | |---|---|---| | `{{#eq ## Equal `eq` — Equality operators let you test the value of a merge field or the result of an expression — whether it is equal to, or not equal to, specific value. > **Tip:** Equality operators work well with both numeric or string (text) values. But cannot compare between the two value types. In this example, the message is different if the `category` merge field contains the value `Accessories`: ```text {{#eq category "Accessories"}} Enjoy up to $25 off! {{else}} Get up to $75 off! {{/eq}} ``` | Format | Example | Output | |---|---|---| | `{{#eq expression expression}}` | `{{#eq quantity 2}}You have 2 items in your cart{{/eq}}` | Displays text if `quantity` is equal to `2` | ## Not equal `neq` — Renders content when a merge field or the result of an expression is *not* equal to a value. | Format | Example | Output | |---|---|---| | `{{#neq expression expression}}` | `{{#neq movie "Looper"}}You answered incorrectly. The correct answer is Looper{{/neq}}` | Display text if `movie` does not equal `Looper` | ## Not `not` — Renders content if a merge field or the result of an expression does not exist, is empty, is false, or is 0. This is a direct way to test for the availability of a merge field. | Format | Example | Output | |---|---|---| | `{{#not expression}}` | `{{#not watch_queue}}Your queue is empty, add a show to watch.{{/not}}` | If `watch_queue` is empty or 0, "Your queue is empty, add a show to watch." will display | ## Unless `unless` — Renders content in a template if a condition is false — like a reverse `if` condition. You might want to use `unless` when you only really need the `else` statement for a condition. | Format | Example | Output | |---|---|---| | `{{#unless expression}}` | `{{#unless dislikes_indiana_jones}}You'll probably like Indiana Jones{{/unless}}` | Airship will render message content if the condition `dislikes_indiana_jones` is empty or false. | ## Greater than and Less than `gt`, `lt`, `gte`, `lte` — Use the following operators to show content if the value of a variable — or the length of an array — is greater than, less than, greater than or equals, or less than or equals a particular value. > **Tip:** These operators are for numerical values only. | Format | Example | Output | |---|---|---| | `{{#gt expression}}` | `{{#gt quantity 2}}You have more than 2 items in your cart{{/gt}}` | Displays text if `quantity` is greater than `2` | | `{{#lt expression}}` | `{{#lt quantity 2}}You have less than 2 items in your cart{{/lt}}` | Displays text if `quantity` is less than `2` | | `{{#gte expression}}` | `{{#gte quantity 2}}You have enough items in your cart to unlock our discount{{/gte}}` | Displays text if `quantity` is greater than or equal to `2` | | `{{#lte expression}}` | `{{#lte quantity 2}}You don't have enough items in your cart to unlock our discount{{/lte}}` | Displays text if `quantity` is less than or equal to `2` | > **Note:** Array lengths are 0-indexed. For example, if an array has 3 entries, `{{#gte field.length 2}}` will be true. # Looping through objects and arrays > Loop through and repeat content from an array or object in your personalized message content. You can use the `#each` operator to loop through and repeat content from an array or object. For example, you can show a user all the items in their shopping cart, including quantity, description, and price. Specify the key of the array or object that you want to start your loop from using `{{#each }}`. You can access properties within the loop with the [Handlebars](https://www.airship.com/docs/reference/glossary/#handlebars) expressions on this page. > **Note:** You cannot loop using Handlebars without an array or object data to loop through. ## Setting a loop limit {#each} When you use `#each`, you can limit the number of loops to perform, making sure that your message is a reasonable length. Set a limit using `#each (limit )`. ```text We think you might like: {{#each (limit array_of_objects 2) as |obj|}} {{obj.name}}: {{obj.desc}} {{/each}} ``` ## Setting context using #with {#with} Use `{{#with}}` to set the context within your template. This can help you access nested keys without having to repeat the parent object's path. For example, if your audience's shipping information is nested in an object called `shippingAddress`, your code might look like this: ```text {{shippingAddress.street}} {{shippingAddress.state}}, {{shippingAddress.zipCode}} ``` You then might use the `with` helper to limit context and simplify your template like this ```text We'll ship your package to: {{#with shippingAddress}} {{street}} {{state}}, {{zipCode}} {{/with}} ``` ## Creating and concatenating arrays When constructing new arrays or concatenating existing arrays, the following operators are available: * `{{$array }}` — Creates a new array from the given items. Any number of items may be passed as parameters, e.g., `{{$array "a" "b" "c"}}` creates a new array containing `["a", "b", "c"]`. * `{{$concat }}` — Creates a new array from a given list of arrays. The [`with`](#with) helper can be useful here to assign these created objects to new variables: ```text {{#with ($array "a" "b" "c") as |arr|}} {{#each arr}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} {{/with}} ``` ...which will yield `a, b, c`. ## Checking for presence The `$contains` operator can be used to check if a value is present in an array. * `{{$contains }}` — Returns a boolean value representing if `search_item` exists in `array`. ```text {{#if ($contains favorite_movies "Brazil")}} You must be a Terry Gilliam Fan. {{/if}} ``` ## Working with key/value objects The `$merge` and `$pick` operators can help you construct new key/value objects from existing ones. * `{{$pick }}` — Constructs a new object from `keys` picked from `object`. If a key is not found in the object, it will be omitted. You may specify as many keys as you like, e.g., `{{$pick obj "key1" "key2"}}`. * `{{$merge }}` — Constructs a new object by merging the given objects. They are merged only on their top-level keys, and the right-most object wins if the same key is present in multiple objects. ```text {{#with ($pick ($merge obj1 obj2) "key1" "key2") as |newObj|}} {{#each newObj}} * {{@key}}: {{.}} {{/each}} {{/with}} ``` ## Filtering empty values You can use the `$filterEmpty` operator to filter any empty values from an array or key/value object. When filtering a key/value object, values are evaluated for emptiness: ```text {{#with ($filterEmpty ($array "a" "b" "" "c")) as |arr|}} {{#each arr}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} {{/with}} ``` ...which will yield `a, b, c`. ## Getting the number of items in objects and arrays Using the `length` property on an array, or the `size` property on a map or object, you can get the number of items or number of keys, respectively. ```text {{#gt array.length 3}} Array length was greater than 3! {{/gt}} {{#gt object.size 3}} Object had more than 3 keys! {{/gt}} ``` ## Referencing properties in objects and arrays Because your loop reference begins from the object or array specified by `#each`, you don't need to provide the full path of the property that you want to reference, just the path to the key within the current iteration of the loop. For example, if looping through an array of objects called `suggestedProducts`, you could specify the keys within each object — `qty` and `productName`. ```text {{#each suggestedProducts}} {{qty}}x {{productName}} {{/each}} ``` You can also reference properties of the array itself within the context of the loop. * `{{.}}` — Accesses the current item in the array, generally for simple arrays of strings or integers. * `{{@index}}` — References the current array index in the loop. * `{{@key}}` — Provides the key name or index location of the current loop iteration. * `{{@root}}` — Accesses the root element properties while you iterate in the context of any nested or child elements. * `{{@first}}` — Returns true for the first element in the loop and can be used with If/Else statements. For example, `{{#each array}} {{#if @first}} Hello! {{/if}} {{/each}}` will include "Hello!" in front of the first object in your array. * `{{@last}}` — Returns true for the last element in the loop and can be used with If/Else statements. For example, `{{#each array}} {{#if @last}} Goodbye! {{/if}} {{/each}}` will include "Goodbye!" in front of the last object in your array. * `{{this}}` — Limits the context to the current object iteration of the loop. Use dot notation to reference a key in the object, i.e., `{{this.key}}`. In general, you don't need to use `this` unless you have a duplicate key outside the loop and the key in your loop is optional. In such a case, `this` prevents the template from finding and using a duplicate key outside the current iteration of the loop if the key in the loop is missing. **Reference names in a simple array:** ```text Everybody coming to the pizza party: {{#each pizza_partiers}} {{@index}}: {{.}} {{/each}} ``` **Reference items in a user's shopping cart as an array of objects:** ```text Are you still interested in the following items? {{#each cart}} * {{qty}}x {{product}} for {{price}} {{/each}} ``` ## Using a feed An *External Data Feed* is a connection to an external API. When you send a message, Airship uses a response from that API to personalize messages. See: [External data feeds](https://www.airship.com/docs/guides/personalization/sources/external-data-feeds/). You reference an external feed as a block, and you can use properties from your feed within the block — `{{#feed "feed_name" var_in_url="value" as |alias|}}`. For example, imagine you have a feed at `https://www.example.com/[[region]]/featured-products` that returns an array of products that you want to display to your audience. You may want to set the region at send time (rather than using the default value from your feed), and you may want to provide an alias so that your feed properties don't collide with attributes or custom event properties. Your message text and Handlebars might look something like this: ```text {{#feed "featured_products" region="us" as |result|}} {{#each (limit result.products 2) as |product|}} {{product.name}}: {{product.price}} https://cool-store.tld/us/products/{{urlEncode product.slug}}/ {{else}} Check back later for deals! {{/each}} {{else}} Couldn't fetch featured products! {{/feed}} ``` > **Note:** While the feed block grants access to variables from the feed response, you can still reference variables (or [merge fields](https://www.airship.com/docs/reference/glossary/#merge_field)) that aren't a part of the feed — attributes, custom event properties, etc. If this is something you may do, set the feed namespace using `as |alias|` to differentiate between properties from the feed and other personalization variables. # Encoding and hashing helpers > Encoding, hashing, and secret hashing functions transform content in Handlebars expressions. The helpers described in this document can hash and otherwise transform strings using standard hashing functions, such as MD5 and SHA256. In many of these cases, you will need to transform the output to various encodings, such as Base64 or Hex. All functions required to perform these transforms are described in this document. In all cases where strings can be provided, they are converted to UTF-8 bytes. ## Encoding The following helpers can be used to change the encoding of a variable. They are commonly used when transforming a byte array, e.g., as output from a hash function, into readable characters. ### Base64 encode {#base64-encode} `$base64Encode` — Given a particular string or byte array, encode to Base64. **Properties** * `content` — The string or byte array to encode. * `pad` — Optional. A boolean specifying if the output should be padded. Default is `true` when unspecified. | Format | Example | Output | |---|---|---| | `{{$base64Encode content pad=true}}` | `{{$base64Encode "wut"}}` | `d3V0` | ### Base64 decode {#base64-decode} `$base64Decode` — Given a Base64-encoded string, decode to an array of bytes. **Properties** * `content` — The Base64 encoded string to decode. | Format | Example | Output | |---|---|---| | `{{$base64Decode content}}` | `{{$utf8Decode ($base64Decode "d3V0")}}` | `wut` | ### UTF-8 decode {#utf-8-decode} `$utf8Decode` — Given an array of bytes, decode as UTF-8 text. **Properties** * `content` — The bytes to decode. For an example, see [Base64 decode](#base64-decode) above. ### UTF-8 encode {#utf-8-encode} `$utf8Encode` — Given a string, encode as a UTF-8 byte array. **Properties** * `content` — The string to encode. This function is provided as an inverse of [UTF-8 decode](#utf-8-decode). ### Hex encode {#hex-encode} `$hexEncode` — Given a string or bytes, encode to a hex representation string. **Properties** * `content` — The string or bytes to encode. | Format | Example | Output | |---|---|---| | `{{$hexEncode content}}` | `{{$hexEncode "wut"}}` | `777574` | ## Hashing Hashing helpers are provided for the following algorithms: * MD5: `$md5` * SHA1: `$sha1` * SHA256: `$sha256` * SHA512: `$sha512` All have the same usage but use their specified algorithm to perform the hash. These functions return byte arrays, which should then be [Hex](#hex-encode) or [Base64](#base64-encode) encoded. **Properties** * `content` — The string or byte array to hash. | Format | Example | Output | |---|---|---| | `{{$ content}}` | `{{$hexEncode ($md5 "wut")}}` | `c268120ce3918b1264fe2c05143b5c4b` | ## HMAC [HMAC](https://en.wikipedia.org/wiki/HMAC) hash functions are provided for the following common algorithms: * MD5: `$hmacMd5` * SHA1: `$hmacSha1` * SHA256: `$hmacSha256` * SHA512: `$hmacSha512` All have the same usage but use their specified algorithm to perform the hash. These functions return byte arrays, which should then be [Hex](#hex-encode) or [Base64](#base64-encode) encoded. **Properties** * `content` — The string or byte array to hash. * `secret` — The secret key used to sign the hash. | Format | Example | Output | |---|---|---| | `{{$hmac content secret=secret}}` | `{{$hexEncode ($hmacSha1 "value" secret="key")}}` | `57443a4c052350a44638835d64fd66822f813319` | ## Encryption Symmetrical AES Encryption is provided by the `$aes` helper using AES in CBC mode with PKC55 padding. Output is Base64 encoded automatically. **Properties** * `content` - The string or byte array to encrypt * `secret` - A 256-bit (32 character) string used to encrypt the content. * `iv` - A 128-bit (16 character) initialization vector. To avoid compromising security, the initialization vector should be unique for each encryption. | Format | Example | Output | |---|---|---| | `{{$aes content secret=secret iv=iv}}` | `{{$aes 'This string is encrypted' secret='7C45C93E-5216-48ED-88D4-6EB7C413' iv='myinitialization' }}` | `bwNBr+/yfYYPoE2aq/zqfzp6kxctfFC+MCgacwwkJSQ=` | Below is a reference implementation for decryption of the above example using NodeJS. ```javascript const crypto = require('node:crypto'); const decipherIV = crypto.createDecipheriv( 'AES-256-CBC', Buffer.from('7C45C93E-5216-48ED-88D4-6EB7C413'), Buffer.from('myinitialization') ); let decrypted = decipherIV.update( 'bwNBr+/yfYYPoE2aq/zqfzp6kxctfFC+MCgacwwkJSQ=', 'base64', 'utf8' ); decrypted += decipherIV.final('utf8'); console.log(decrypted); // Will output "This string is encrypted" ```