# General

Welcome to Stencil.

Here you will find all documentations related to using Stencil.

If you are a developer working on integrations, API section outlines all the details regarding public endpoints that are available to consume.

The rest of the sections provide guides and manuals for using Stencil.


# Authentication

### Generate API Key

To generate your API key, go to the project settings page and scroll to API Key section.

API Key is automatically generated, or you can also click on Generate New button to create a new one.

![You can generate new key at any point and the old key will be invalidated immediately.](/files/-MavFwgRqEzawqI9cTJ7)

### Scope

Each API is key is scoped to the project. Thus, each project will have different API keys.

### Making request

Include the following header in each of your request to be authenticated properly.

```
Authorization: Bearer API_KEY
```

Replace `API_KEY` with the API key from the previous step.


# Pagination

Retrieving large result

All results returned are paginated by default.&#x20;

```javascript
{
    "meta": {
        "next": "xqErdcgltHGs",
        "previous": null,
        "total_count": 39
    },
    "results": [
        {
            "created_at": "2021-05-06T07:00:21Z",
            "description": "Test",
            "favorite": false,
            "id": "f2eb2dab-a861-4960-b5f4-d54c61552f7d",
            "name": "Test project",
            "self": "http://localhost:4000/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d",
            "templates": "http://localhost:4000/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d/templates",
            "updated_at": "2021-05-06T07:00:21Z"
        },
        ...
    ]
}
```

`meta` field contains the pagination metadata and `result` field contains the limited result set.

### Querying for next set of result

Append `after` query string with the value from `next` metadata field.

```javascript
https://api.usestencil.com/v1/projects?after=xqErdcgltHGs
```

### Querying for previous set of result

Append `before` query string with the value from `previous` metadata field.

```javascript
https://api.usestencil.com/v1/projects?before=xqErdcgltHGs
```

### Limiting result

Append `limit=<integer>` to limit the results set

```javascript
https://api.usestencil.com/v1/projects?limit=10
```

{% hint style="info" %}
You can combine `limit` with either `after` and `before`
{% endhint %}


# Status Code and Throttling

## HTTP Status Code

<table data-header-hidden><thead><tr><th width="152.0859375">Status Code</th><th>Description</th></tr></thead><tbody><tr><td><code>200</code></td><td>Successful.</td></tr><tr><td><code>201</code></td><td>The resource has been created.</td></tr><tr><td><code>202</code></td><td>Accepted. Your image generation request has been accepted for processing.</td></tr><tr><td><code>400</code></td><td>Check your request, it is invalid.</td></tr><tr><td><code>401</code></td><td>Not authorized. Make sure the provided API key is correct.</td></tr><tr><td><code>404</code></td><td>The resource is not found.</td></tr><tr><td><code>405</code></td><td>Method not allowed. Ensure you're using the correct HTTP verb for the endpoint.</td></tr><tr><td><code>429</code></td><td>Too many requests, slow down. See <a href="/pages/-MavJA6IYmXiG6AmI0w7#throttling">throttling</a>. </td></tr><tr><td><code>500</code></td><td>Something is wrong with our server. Let us know!</td></tr></tbody></table>

## Throttling

Throttle rate is set at 10 requests per 10 seconds. You request will return with `429` status if your request has been throttled.

The following headers will also be sent,

<table data-header-hidden><thead><tr><th width="233.78729248046875">Header</th><th>Descripton</th></tr></thead><tbody><tr><td><code>X-RATELIMIT-LIMIT</code></td><td>Current limit</td></tr><tr><td><code>X-RATELIMIT-REMAINING</code></td><td>Remaining request that can be sent within limit</td></tr><tr><td><code>X-RATELIMIT-RESET</code></td><td>Epoch time (seconds) for when the limit is reset</td></tr></tbody></table>


# Endpoints

API Endpoints


# Account

Getting your account information

For convenience, we also provide API endpoint to retrieve your account information

## Account information

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/account`

#### Headers

<table><thead><tr><th width="132.76300048828125">Name</th><th width="137.0738525390625">Type</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Must contain `Bearer &#x3C;API-Secret-Key>`.</td></tr></tbody></table>

{% tabs %}
{% tab title="200 Successful response upon a valid API key. current\_projectdescribes the scope of the API key." %}

```javascript
{
    "created_at": "2021-06-17T12:05:31",
    "current_project": {
        "description": "Steal the cake",
        "name": "Project 1"
    },
    "current_usage": 0,
    "email": "one@stencil.com",
    "id": "76baa621-283a-4b55-8236-3938e1cbf771",
    "limit_usage": 2000,
    "renewal_date": "2021-07-17T12:27:08Z"
}
```

{% endtab %}

{% tab title="401 Invalid API key will return 401 status" %}

```javascript
"Unauthorized"
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
This API endpoint is also useful when integrating with Integromat to check for a valid account. `200` status response is only returned when API key is valid.
{% endhint %}


# Projects

List available projects

## List available projects

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/projects`

#### Query Parameters

| Name   | Type   | Description               |
| ------ | ------ | ------------------------- |
| after  | string | Query next result set     |
| before | string | Query previous result set |

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "meta": {
        "next": null,
        "previous": null,
        "total_count": 1
    },
    "results": [
        {
            "created_at": "2021-05-06T07:00:21Z",
            "description": "Test",
            "favorite": false,
            "id": "f2eb2dab-a861-4960-b5f4-d54c61552f7d",
            "name": "Test project",
            "self": "https://api.usestencil.com/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d",
            "templates": "https://api.usestencil.com/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d/templates",
            "updated_at": "2021-05-06T07:00:21Z"
        }
    ]
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
`after` and `before` are related to pagination. Please see [pagination](/api/pagination) page for more information&#x20;
{% endhint %}

The response usually include link to other related endpoints.&#x20;

`self` links to detail regarding the resource itself.

`templates` links to endpoint to list templates related to the project.

## Get specific project

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/projects/:id`

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Project ID  |

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "created_at": "2021-05-06T07:00:21Z",
    "description": "Test",
    "favorite": false,
    "id": "f2eb2dab-a861-4960-b5f4-d54c61552f7d",
    "name": "Test project",
    "self": "https://api.usestencil.com/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d",
    "templates": "https://api.usestencil.com/api/v1/projects/f2eb2dab-a861-4960-b5f4-d54c61552f7d/templates",
    "updated_at": "2021-05-06T07:00:21Z"
}
```

{% endtab %}
{% endtabs %}


# Templates

List available templates

## List templates

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/projects/:project_id/templates`

#### Path Parameters

| Name        | Type   | Description |
| ----------- | ------ | ----------- |
| project\_id | string | Project ID  |

#### Query Parameters

| Name   | Type   | Description                   |
| ------ | ------ | ----------------------------- |
| before | string | Query the previous result set |
| after  | string | Query the next result set     |

{% tabs %}
{% tab title="200 " %}

```javascript
{
  "meta":{
    "next":null,
    "previous":null,
    "total_count":2
  },
  "results":[
    {
      "available_modifications":[
        [
          {
            "description":"URL of the image",
            "field":"src",
            "primary":true,
            "required":true,
            "type":"string",
            "value":"https//example.com/image.png"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"image_3"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ],
        [
          {
            "description":"Value for the textbox",
            "field":"text",
            "primary":true,
            "required":true,
            "type":"string",
            "value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
          },
          {
            "description":"Text color",
            "field":"fill",
            "primary":false,
            "required":false,
            "type":"string",
            "value":"rgba(15, 15, 15, 1)"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"circular_text_2"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ],
        [
          {
            "description":"Value for the textbox",
            "field":"text",
            "primary":true,
            "required":true,
            "type":"string",
            "value":"ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور"
          },
          {
            "description":"Text color",
            "field":"fill",
            "primary":false,
            "required":false,
            "type":"string",
            "value":"rgba(0, 0, 0, 1)"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"text_5"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ]
      ],
      "created_at":"2021-05-22T05:00:44Z",
      "id":"10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
      "name":"Test2",
      "project_id":"f2eb2dab-a861-4960-b5f4-d54c61552f7d",
      "self":"https://api.usestencil.com/v1/templates/10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
      "signed_image_base":"Hts6NWUuvtXbmGaQbM2kGM",
      "starred":false,
      "updated_at":"2021-05-22T05:00:59Z"
    },
    {
      "available_modifications":[
        [
          {
            "description":"URL of the image",
            "field":"src",
            "primary":true,
            "required":true,
            "type":"string",
            "value":"https//example.com/image.png"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"image_3"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ],
        [
          {
            "description":"Value for the textbox",
            "field":"text",
            "primary":true,
            "required":true,
            "type":"string",
            "value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
          },
          {
            "description":"Text color",
            "field":"fill",
            "primary":false,
            "required":false,
            "type":"string",
            "value":"rgba(15, 15, 15, 1)"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"circular_text_2"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ],
        [
          {
            "description":"Text color",
            "field":"fill",
            "primary":false,
            "required":false,
            "type":"string",
            "value":"rgba(0, 0, 0, 1)"
          },
          {
            "description":"Object identifier",
            "field":"name",
            "primary":false,
            "required":true,
            "type":"string",
            "value":"text_5"
          },
          {
            "description":"Angle of the object",
            "field":"angle",
            "primary":false,
            "required":false,
            "type":"integer",
            "value":0
          }
        ]
      ],
      "created_at":"2021-05-22T04:59:03Z",
      "id":"70176253-3bcb-4592-913d-a6c5df83a258",
      "name":"Untitled",
      "project_id":"f2eb2dab-a861-4960-b5f4-d54c61552f7d",
      "self":"http://api.usestencil.com/v1/templates/70176253-3bcb-4592-913d-a6c5df83a258",
      "signed_image_base":"Hts6NWUuvtXbmGaQbM2kGM",
      "starred":true,
      "updated_at":"2021-05-22T07:43:02Z"
    }
  ]
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
`after` and `before` are related to pagination. Please see [pagination](/api/pagination) page for more information&#x20;
{% endhint %}

## Get specific template

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/templates/:id`

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Template ID |

#### Query Parameters

| Name   | Type   | Description                   |
| ------ | ------ | ----------------------------- |
| before | string | Query the previous result set |
| after  | string | Query the next result set     |

{% tabs %}
{% tab title="200 " %}

```javascript
{
  "available_modifications":[
    [
      {
        "description":"URL of the image",
        "field":"src",
        "primary":true,
        "required":true,
        "type":"string",
        "value":"https://example.com/image.png"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"image_3"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ],
    [
      {
        "description":"Value for the textbox",
        "field":"text",
        "primary":true,
        "required":true,
        "type":"string",
        "value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
      },
      {
        "description":"Text color",
        "field":"fill",
        "primary":false,
        "required":false,
        "type":"string",
        "value":"rgba(15, 15, 15, 1)"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"circular_text_2"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ],
    [
      {
        "description":"Text color",
        "field":"fill",
        "primary":false,
        "required":false,
        "type":"string",
        "value":"rgba(0, 0, 0, 1)"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"text_5"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ]
  ],
  "created_at":"2021-05-22T05:00:44Z",
  "id":"10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
  "name":"Test2",
  "project_id":"f2eb2dab-a861-4960-b5f4-d54c61552f7d",
  "self":"https://api.usestencil.com/v1/templates/10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
  "signed_image_base":"Hts6NWUuvtXbmGaQbM2kGM",
  "starred":false,
  "updated_at":"2021-05-22T05:00:59Z"
}
```

{% endtab %}
{% endtabs %}

### Response

#### `available_modification` object

| Property      | Description                                                    |
| ------------- | -------------------------------------------------------------- |
| `description` | Description of the property                                    |
| `field`       | Field unique identifier                                        |
| `value`       | Value of the field                                             |
| `required`    | Indicate that the value must be specified                      |
| `type`        | The type of value i.e. Integer requires number to be specified |

#### `signed_image_base`

Base ID for secure signed image. See [Secure Signed Image](/integrations/secure-signed-image/signed-image) for more information.

The rests of the fields are self explanatory.&#x20;

## Copy a template

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/templates/:id/copy`&#x20;

Duplicate a template

**Headers**

| Name          | Value              |
| ------------- | ------------------ |
| Content-Type  | `application/json` |
| Authorization | `Bearer <token>`   |

**Query string**

| Name | Value       |
| ---- | ----------- |
| id   | Template ID |

**Body**

| Name         | Type   | Description                                                                   |
| ------------ | ------ | ----------------------------------------------------------------------------- |
| `name`       | string | Optional. New name for the template. Default to existing name.                |
| `project_id` | string | Optional. Project ID to duplicate the project to. Default to current project. |

**Response**

{% tabs %}
{% tab title="200" %}

```json
{
  "available_modifications":[
    [
      {
        "description":"URL of the image",
        "field":"src",
        "primary":true,
        "required":true,
        "type":"string",
        "value":"https://example.com/image.png"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"image_3"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ],
    [
      {
        "description":"Value for the textbox",
        "field":"text",
        "primary":true,
        "required":true,
        "type":"string",
        "value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
      },
      {
        "description":"Text color",
        "field":"fill",
        "primary":false,
        "required":false,
        "type":"string",
        "value":"rgba(15, 15, 15, 1)"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"circular_text_2"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ],
    [
      {
        "description":"Text color",
        "field":"fill",
        "primary":false,
        "required":false,
        "type":"string",
        "value":"rgba(0, 0, 0, 1)"
      },
      {
        "description":"Object identifier",
        "field":"name",
        "primary":false,
        "required":true,
        "type":"string",
        "value":"text_5"
      },
      {
        "description":"Angle of the object",
        "field":"angle",
        "primary":false,
        "required":false,
        "type":"integer",
        "value":0
      }
    ]
  ],
  "created_at":"2021-05-22T05:00:44Z",
  "id":"10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
  "name":"Test2",
  "project_id":"f2eb2dab-a861-4960-b5f4-d54c61552f7d",
  "self":"https://api.usestencil.com/v1/templates/10dee897-cc28-4ae1-bf8e-f1bc1c551fe3",
  "signed_image_base":"Hts6NWUuvtXbmGaQbM2kGM",
  "starred":false,
  "updated_at":"2021-05-22T05:00:59Z"
}
```

{% endtab %}
{% endtabs %}


# Images

Generate image with POST request

## Create image synchronously

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/images/sync`

Create image synchronously and get the result in within the same request.

#### Request Body

| Name            | Type    | Description                                                                                                                       |
| --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| modifications   | array   | Array of modification objects                                                                                                     |
| metadata        | object  | Extra metadata to be included together with the webhook                                                                           |
| webhook\_url    | string  | URL of the webhook to be called when the image is done processing                                                                 |
| template        | string  | The ID of the template                                                                                                            |
| transparent     | boolean | Set image background transparency                                                                                                 |
| png\_multiplier | integer | Set the quality of PNG image. Must be greater or equal to 1. See [Image Quality](#undefined) section.                             |
| jpeg\_quality   | float   | Set the quality of JPEG image. Must be between 0 and 1 with 1 being the highest quality. See [Image Quality](#undefined) section. |

{% tabs %}
{% tab title="200 Image generated successfully " %}

```json
{
  "id": "05e15531-b80d-4ca0-a658-203d68cd2216",
  "status": "completed",
  "self": "https://api.usestencil.com/v1/images/05e15531-b80d-4ca0-a658-203d68cd2216",
  "log": "{}",
  "metadata": {},
  "template_id": "1688a5ed-ccdb-477d-a7d5-629a5d4d72ff",
  "webhook_url": null,
  "webhook_response_body": null,
  "webhook_response_code": null,
  "created_at": "2025-09-22T03:52:00.482Z",
  "modifications": [
    {
      "name": "category",
      "text": "1...2...3...Bougeons pour I.R.I.S.E"
    },
    {
      "name": "distance",
      "text": "10 KM"
    },
    {
      "name": "logo",
      "src": "https://i.ibb.co/wF2sybRX/Logo-IRISE-2.png"
    },
    {
      "name": "name",
      "text": "Malo J"
    },
    {
      "name": "number",
      "text": "00035"
    },
    {
      "name": "qrcode",
      "value": "https://usestencil.com",
      "visible": false
    }
  ],
  "image_url": "https://cdn.usestencil.com/images/1688a5ed-ccdb-477d-a7d5-629a5d4d72ff/05e15531-b80d-4ca0-a658-203d68cd2216.png",
  "image_url_jpg": "https://cdn.usestencil.com/images/1688a5ed-ccdb-477d-a7d5-629a5d4d72ff/05e15531-b80d-4ca0-a658-203d68cd2216.jpeg"
}
```

{% endtab %}
{% endtabs %}

## Create image asynchronously

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/images`

Create image asynchronously. Once image is done processing, the specified webhook is called with the generated image.

#### Request Body

| Name            | Type    | Description                                                                                                                       |
| --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| modifications   | array   | Array of modifications objects                                                                                                    |
| metadta         | object  | Extra metadata to be included together with the webhook                                                                           |
| webhook\_url    | string  | URL of the webhook to be called when the image is done processing                                                                 |
| template        | string  | The ID of the template                                                                                                            |
| transparent     | boolean | Set image background transparency                                                                                                 |
| png\_multiplier | integer | Set the quality of PNG image. Must be greater or equal to 1. See [Image Quality](#undefined) section.                             |
| jpeg\_quality   | float   | Set the quality of JPEG image. Must be between 0 and 1 with 1 being the highest quality. See [Image Quality](#undefined) section. |

{% tabs %}
{% tab title="201: Created Image is sent to processing" %}

```json
{
  "id": "05e15531-b80d-4ca0-a658-203d68cd2216",
  "status": "pending",
  "self": "https://api.usestencil.com/v1/images/05e15531-b80d-4ca0-a658-203d68cd2216",
  "log": "{}",
  "metadata": {},
  "template_id": "1688a5ed-ccdb-477d-a7d5-629a5d4d72ff",
  "webhook_url": null,
  "webhook_response_body": null,
  "webhook_response_code": null,
  "created_at": "2025-09-22T03:52:00.482Z",
  "modifications": [
    {
      "name": "category",
      "text": "1...2...3...Bougeons pour I.R.I.S.E"
    },
    {
      "name": "distance",
      "text": "10 KM"
    },
    {
      "name": "logo",
      "src": "https://i.ibb.co/wF2sybRX/Logo-IRISE-2.png"
    },
    {
      "name": "name",
      "text": "Malo J"
    },
    {
      "name": "number",
      "text": "00035"
    },
    {
      "name": "qrcode",
      "value": "https://usestencil.com",
      "visible": false
    }
  ],
  "image_url": null,
  "image_url_jpg": null
}
```

{% endtab %}
{% endtabs %}

### Modification object

Modification object generally follows the following pattern,

```javascript
  "modifications": [
    {
      "name": "text_1",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
    },
    {
      "name": "rating_3",
      "rating": 3.5
    }
  ]
```

`name` is a required property.&#x20;

{% hint style="info" %}
The `name` is the field unique identifier in the template editor
{% endhint %}

The rest of the properties depend on the type of object. If you need full list of supported properties, you can view them in them in API Console.

![Console shows all available modifications for each template](/files/eJ9XmkmL1znVbkpSf9tk)

### Image Quality

#### PNG

For PNG images, you can set `png_multiplier` to adjust the quality of generated images.

For example, if your template size is 800x800 then setting the `png_multiplier` to 2 will scale the image to 1600x1600 which creates a higher resolution image.

Please note that higher quality image will increase the image size and will also increase the image generation time.

#### JPEG

For JPEG images, you can set `jpeg_quality` to adjust the quality of generated images. JPEG is a lossy format, while it offers higher compression rate that could decrease the file size, it also degrades the image quality.&#x20;

You can control this by setting `jpeg_quality` to a number between 0 and 1 with 1 being the highest quality.

Please note that higher quality image will increase the image size and will also increase the image generation time.

#### Default Settings

You can also set a default settings for all templates in your project. Go to your project's settings page and set the values accordingly.

You can always override these values in the API by following the instructions mentioned above.

![](/files/KnBTeFpK9d78Fmk1Rd9r)

## Search generated images

<mark style="color:green;">`POST`</mark> `/v1/images/search?q=<query>`

Search for generated images based on your modification inputs. It searches for given query in your modification inputs.  Query is case insensitive.

**Response**

{% tabs %}
{% tab title="200" %}

```json
{
  "meta": {
    "next": null,
    "total_count": 2,
    "previous": null
  },
  "results": [
    {
      "id": "4034dee3-039a-466e-b4ba-0c2fb721036a",
      "status": "completed",
      "self": "http://api.usestencil.com/v1/images/4034dee3-039a-466e-b4ba-0c2fb721036a",
      "log": "{}",
      "metadata": {},
      "created_at": "2025-09-22T14:39:25.624Z",
      "template_id": "8be62eca-72de-4f81-9edc-a4dbeee6986f",
      "webhook_url": null,
      "modifications": [
        {
          "name": "rect_4"
        },
        {
          "name": "image",
          "src": "https://images.unsplash.com/photo-1503327431567-3ab5e6e79140?q=80&w=3328&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
        },
        {
          "name": "product",
          "text": "JEANS"
        },
        {
          "name": "price",
          "text": "$49.99"
        },
        {
          "name": "qrcode_5",
          "value": "https://usestencil.com"
        }
      ],
      "image_url": "https://usestencil.s3.amazonaws.com/dev/images/8be62eca-72de-4f81-9edc-a4dbeee6986f/4034dee3-039a-466e-b4ba-0c2fb721036a.png",
      "image_url_jpg": "https://usestencil.s3.amazonaws.com/dev/images/8be62eca-72de-4f81-9edc-a4dbeee6986f/4034dee3-039a-466e-b4ba-0c2fb721036a.jpeg",
      "webhook_response_code": null,
      "webhook_response_body": null
    }
  ]
}

```

{% endtab %}
{% endtabs %}


# Collections

Collection allows you to create multiple images from multiple templates within a single request

## Use cases

Generally, there are two use cases of collection,

1. **To create images from multiple similar templates in a collection.**

   Image if you have a similar template but with different dimension. For example, you could have three templates - one for Instagram post, one for Pinterest, and one for open graph.
2. **To create image from randomly selected template in a collection.**

   If you have multiple templates with similar content but with different design, you might want to generate an image randomly from the collection of templates. This is useful if you want to create Instagram post but you don't want the design to be the same.

## How to create template collection

Before you can send a request to collection endpoint, you need to create the collection first.

1. Go to "Collections" tab and click on "+ New Collection"

![](/files/-Miv0eePIqybL5BAI9Y-)

2\. Add the template that you want to add into the collection and save your changes.

![](/files/-Miv0tDivBeetu92XFBd)

### Templates Compatibility

For best compatibility, each template's fields must have similar types. i.e. a field named `text_1` must be of type `textbox` in all the templates.

## Endpoints

{% hint style="info" %}
Collection endpoint only available in asynchronous mode.&#x20;
{% endhint %}

## Create images from a template collection

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/collections`

Create images from a template collection. This endpoint returns immediately. \
\
To get the images, you can specify a `webhook_url` or poll the collection endpoint.

#### Request Body

| Name          | Type   | Description                                                                                                        |
| ------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| select        | number | <p>Select <code>n</code> number of templates randomly from the collection.<br><br>Leave blank to generate all.</p> |
| metadata      | object | Additional metadata that you want to add. This will be returned when the images are ready.                         |
| webhook\_url  | string | Webhook URL to call when the image is ready                                                                        |
| modifications | array  | Array of modifications. Similar to modifications in  Images section.                                               |
| collection    | string | Collection ID                                                                                                      |

{% tabs %}
{% tab title="200 When the images are ready, the status field is set to completed. Otherwise, it is pending." %}

```javascript
{
  "created_at": "2021-09-06T07:10:36.134Z",
  "id": "80b49653-2f71-4c2e-af92-cc861d686993",
  "images": [
    {
      "image_url": null,
      "image_url_jpg": null,
      "template_id": "4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7"
    }
  ],
  "metadata": null,
  "modifications": [
    {
      "name": "text_1",
      "text": "Hello world"
    },
    {
      "name": "author",
      "text": "Shulhi"
    }
  ],
  "self": "http://api.usestencil.test:4000/v1/collections/80b49653-2f71-4c2e-af92-cc861d686993",
  "status": "pending",
  "templates": [
    {
      "template": "Template 1",
      "template_id": "4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7"
    },
    {
      "template": "Template 2",
      "template_id": "1a7625b3-41ef-4838-9919-c684b98b93b4"
    }
  ],
  "updated_at": "2021-09-06T07:10:39.382Z",
  "webhook_url": null
}
```

{% endtab %}
{% endtabs %}

## Retrieve images from template collection

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/collections/:id`

When the images are ready, the `status` field is set to `completed`.

#### Path Parameters

| Name | Type   | Description                                |
| ---- | ------ | ------------------------------------------ |
| id   | string | The ID returned from previous POST request |

{% tabs %}
{% tab title="200 " %}

```javascript
{
  "created_at": "2021-09-06T07:10:36.134Z",
  "id": "80b49653-2f71-4c2e-af92-cc861d686993",
  "images": [
    {
      "image_url": "https://cdn.usestencil.com/images/4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7/ff6a4383-c35e-430f-b5d5-af124cf28a11.png",
      "image_url_jpg": "https://cdn.usestencil.com/images/4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7/ff6a4383-c35e-430f-b5d5-af124cf28a11.jpeg",
      "template_id": "4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7"
    }
  ],
  "metadata": null,
  "modifications": [
    {
      "name": "text_1",
      "text": "Hello world"
    },
    {
      "name": "author",
      "text": "Shulhi"
    }
  ],
  "self": "http://api.usestencil.test:4000/v1/collections/80b49653-2f71-4c2e-af92-cc861d686993",
  "status": "completed",
  "templates": [
    {
      "template": "Template 1",
      "template_id": "4c0a708e-fff0-4256-ad4b-45f4c2f2ebd7"
    },
    {
      "template": "Template 2",
      "template_id": "1a7625b3-41ef-4838-9919-c684b98b93b4"
    }
  ],
  "updated_at": "2021-09-06T07:10:39.382Z",
  "webhook_url": null
}
```

{% endtab %}
{% endtabs %}


# PDFs

Besides images, Stencil supports generating PDFs from the same template.

## Create a PDF asynchronously

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/pdfs`

#### Request Body

| Name                                       | Type   | Description                                               |
| ------------------------------------------ | ------ | --------------------------------------------------------- |
| modfications                               | array  | Array of modification objects                             |
| metadata                                   | object | Extra metadata to be included together with the webhook   |
| webhook\_url                               | string | URL of the webhook to be called when the pdf is generated |
| template<mark style="color:red;">\*</mark> | string | The ID of the template                                    |

{% tabs %}
{% tab title="202: Accepted PDF is accepted for processing" %}

```json
{
  "created_at": "2023-04-06T12:04:58.877Z",
  "id": "e45e9dc7-8eee-476c-9031-0d1040881d1a",
  "log": null,
  "metadata": {},
  "modifications": [
    {
      "name": "image_2",
      "src": "https://usestencil.s3.amazonaws.com/dev/uploads/814bb298-a5c9-4df8-b367-cb81be9e8839/efaa4261-94d7-4ceb-b2ec-cf60990afb07/photo-1613569973485-c6ecb241be47-311746964.png"
    },
    {
      "name": "text_3",
      "text": "YELLOW WOOL HOODIEZZ"
    },
    {
      "name": "text_5",
      "text": "USD 49.99"
    }
  ],
  "pdf_url": null,
  "self": "http://api.usestencil.test:4000/v1/pdfs/e45e9dc7-8eee-476c-9031-0d1040881d1a",
  "status": "pending",
  "template_id": "6132c433-aa49-4774-8eed-c402898f1437",
  "webhook_response_body": null,
  "webhook_response_code": null,
  "webhook_url": null
}
```

You can send a `GET` request to `self` to check the status of PDF generation. See the next API for details.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
PDF generation uses 2 credits instead of 1.
{% endhint %}

## Get the PDF

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/pdfs/:pdf_id`

#### Path Parameters

| Name                                      | Type   | Description                                                                                 |
| ----------------------------------------- | ------ | ------------------------------------------------------------------------------------------- |
| pdf\_id<mark style="color:red;">\*</mark> | string | <p>The PDF id. </p><p></p><p>You can get the PDF id from the response of creating a PDF</p> |

{% tabs %}
{% tab title="200: OK " %}

```json
{
  "created_at": "2023-04-06T12:04:58.877Z",
  "id": "e45e9dc7-8eee-476c-9031-0d1040881d1a",
  "log": null,
  "metadata": {},
  "modifications": [
    {
      "name": "image_2",
      "src": "https://usestencil.s3.amazonaws.com/dev/uploads/814bb298-a5c9-4df8-b367-cb81be9e8839/efaa4261-94d7-4ceb-b2ec-cf60990afb07/photo-1613569973485-c6ecb241be47-311746964.png"
    },
    {
      "name": "text_3",
      "text": "YELLOW WOOL HOODIEZZ"
    },
    {
      "name": "text_5",
      "text": "USD 49.99"
    }
  ],
  "pdf_url": "https://usestencil.s3.amazonaws.com/dev/pdfs/6132c433-aa49-4774-8eed-c402898f1437/e45e9dc7-8eee-476c-9031-0d1040881d1a.pdf",
  "self": "http://api.usestencil.test:4000/v1/pdfs/e45e9dc7-8eee-476c-9031-0d1040881d1a",
  "status": "completed",
  "template_id": "6132c433-aa49-4774-8eed-c402898f1437",
  "webhook_response_body": null,
  "webhook_response_code": null,
  "webhook_url": null
}
       
```

{% endtab %}
{% endtabs %}

For PDF generation, we only support asynchronous request, as PDF generation usually takes a slightly longer time than image generation.

## Search generated PDFs

<mark style="color:green;">`POST`</mark> `/v1/pdfs/search?q=<query>`

Search for generated PDFs based on your modification inputs. It searches for given query in your modification inputs.  Query is case insensitive.

**Response**

{% tabs %}
{% tab title="200" %}

```json
{
  "meta": {
    "next": null,
    "total_count": 1,
    "previous": null
  },
  "results": [
    {
      "id": "fa032cc8-fa07-4221-8411-4279b46683d5",
      "status": "completed",
      "self": "http://api.usestencil.com/v1/pdfs/fa032cc8-fa07-4221-8411-4279b46683d5",
      "log": "{}",
      "metadata": {},
      "created_at": "2025-09-23T11:56:21.562Z",
      "template_id": "8be62eca-72de-4f81-9edc-a4dbeee6986f",
      "webhook_url": null,
      "modifications": [
        {
          "name": "rect_4"
        },
        {
          "name": "image",
          "src": "https://images.unsplash.com/photo-1503327431567-3ab5e6e79140?q=80&w=3328&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
        },
        {
          "name": "product",
          "text": "JEANS"
        },
        {
          "name": "price",
          "text": "$49.99"
        },
        {
          "name": "qrcode_5",
          "value": "https://usestencil.com"
        }
      ],
      "webhook_response_code": null,
      "webhook_response_body": null,
      "pdf_url": "https://usestencil.s3.amazonaws.com/dev/pdfs/8be62eca-72de-4f81-9edc-a4dbeee6986f/fa032cc8-fa07-4221-8411-4279b46683d5.pdf"
    }
  ]
}

```

{% endtab %}
{% endtabs %}


# Airtable

Get and run Airtable integration from our API

## Get Airtable image generation status

<mark style="color:blue;">`GET`</mark> `https://api.usestencil.com/v1/airtables/:id`

#### Path Parameters

| Name                                 | Type   | Description      |
| ------------------------------------ | ------ | ---------------- |
| id<mark style="color:red;">\*</mark> | string | ID of the action |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    "action_title": "Demo",
    "base_id": "appdxxxxx",
    "status": "Started",
    "table_name": "Demo",
    "view_name": null
}
```

{% endtab %}
{% endtabs %}

## Trigger Airtable image generation from the API

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/airtables/:id`

#### Path Parameters

| Name                                 | Type   | Description      |
| ------------------------------------ | ------ | ---------------- |
| id<mark style="color:red;">\*</mark> | string | ID of the action |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    "action_title": "Demo",
    "base_id": "appdxxxxx",
    "status": "Starting",
    "table_name": "Demo",
    "view_name": null
}
```

{% endtab %}
{% endtabs %}

### `status` field

|          | Description                                                                                      |
| -------- | ------------------------------------------------------------------------------------------------ |
| `null`   | Nothing is being generated. It Indicates that no job is running or the job has finished running. |
| Starting | Image generation is starting.                                                                    |
| Started  | Image generation is already started.                                                             |


# Editor Session

Create a shareable link for editing template without logging in.

## Create a new session

<mark style="color:green;">`POST`</mark> `https://api.usestencil.com/v1/editor/sessions`

### **Request Body**

<table><thead><tr><th width="154.76904296875">Name</th><th width="110.35498046875"></th><th width="146.3775634765625">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>name</code></td><td>Required</td><td>string</td><td>Name of the session</td></tr><tr><td><code>expires</code></td><td>Required</td><td>integer</td><td>Time till expire. In seconds.</td></tr><tr><td><code>template_id</code></td><td>Required</td><td>uuid</td><td>The template to give access to</td></tr><tr><td><code>permissions</code></td><td>Optional</td><td>object</td><td>See <code>permission</code> object, <a data-mention href="#permission-object">#permission-object</a></td></tr></tbody></table>

#### Permission object

<table><thead><tr><th width="156.4305419921875">Name</th><th width="108.14056396484375"></th><th width="111.68829345703125">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>layers</code></td><td>Required</td><td>object</td><td><code>layer</code> object</td></tr></tbody></table>

#### Layer object

<table><thead><tr><th width="106.08941650390625">Name</th><th width="100.7586669921875"></th><th width="194.5399169921875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>actions</code></td><td>Optional</td><td>array of string</td><td><strong>Set the default actions for all layers.</strong> Value can be combination of <code>"create"</code>, <code>"edit"</code>, and <code>"delete"</code></td></tr><tr><td><code>fields</code></td><td>Optional</td><td>array of <code>field</code> object</td><td>Override default action for specific layer. See <code>field</code> object.</td></tr></tbody></table>

#### Field object

<table><thead><tr><th width="107.06768798828125">Name</th><th width="112.21270751953125"></th><th width="189.9765625">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>name</code></td><td>Required</td><td>string</td><td>Name of the layer</td></tr><tr><td><code>actions</code></td><td>Required</td><td>array of string</td><td>Value can be combination of <code>"create"</code>, <code>"edit"</code>, and <code>"delete"</code></td></tr></tbody></table>

#### Permission actions

Action can be combination of  `"create"`, `"edit"`, and `"delete"` .

<table><thead><tr><th width="102.61114501953125">Action</th><th>Effect</th></tr></thead><tbody><tr><td><code>create</code></td><td>When specified, user can create a new layer or duplicate an existing layer.</td></tr><tr><td><code>edit</code></td><td>When specified, user is allowed to make changes to the layer.</td></tr><tr><td><code>delete</code></td><td>When specified, user is allowed to delete the layer.</td></tr></tbody></table>

When you override the field, the permission set by the default permission is ignored.

#### Request body examples

{% tabs %}
{% tab title="Create with default permission" %}

```json
{
    "name": "Session 1",
    "template_id": "<template_id>",
    "expires": 60000
}
```

{% endtab %}

{% tab title="Create with custom permissions" %}

```json
{
    "name": "Session 1",
    "template_id": "<template_id>",
    "expires": 60000,
    "permissions": {
        "layers" {
            "actions": ["edit"],
            "fields": [
                {
                    "name": "image",
                    "actions": ["edit", "delete"]
                },
                {
                    "name": "description_text",
                    "actions": ["edit"]
                }               
            ]
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Get a session

<mark style="color:green;">`GET`</mark> `https://api.usestencil.com/v1/editor/sessions/:session_id`

### Response body

{% tabs %}
{% tab title="200 (No permission set)" %}

```json
{
  "permissions": null,
  "token": "QFhV6HN7hv3nqPcnGhp2xo",
  "session_id": "daf35835-fd26-4503-8027-1bb59417be91",
  "expired_at": "2025-05-18T03:31:34Z",
  "session_url": "http://app.usestencil.com/editor/templates/f636ffa7-8761-4877-960b-cf5b97a41c6b/sessions/daf35835-fd26-4503-8027-1bb59417be91?token=QFhV6HN7hv3nqPcnGhp2xo"
}
```

{% endtab %}

{% tab title="200 (With custom permission)" %}

```json
{
  "permissions": {
    "layers": {
      "fields": [
        {
          "name": "field",
          "actions": [
            "create",
            "edit"
          ]
        }
      ],
      "actions": [
        "create",
        "edit",
        "delete"
      ]
    }
  },
  "token": "QFhV6HN7hv3nqPcnGhp2xo",
  "session_id": "daf35835-fd26-4503-8027-1bb59417be91",
  "expired_at": "2025-05-18T03:31:34Z",
  "session_url": "http://app.usestencil.com/editor/templates/f636ffa7-8761-4877-960b-cf5b97a41c6b/sessions/daf35835-fd26-4503-8027-1bb59417be91?token=QFhV6HN7hv3nqPcnGhp2xo"
}
```

{% endtab %}
{% endtabs %}


# Template Editor

Our editor just got a refresh!

We are thrilled to announce the release of our new editor! After months of hard work, we've completely rewritten the editor from scratch. This new version represents a significant upgrade over the old editor in terms of performance, flexibility, and ease of use.

The decision to rewrite the editor was not taken lightly. We knew that it would be a significant undertaking, but we also knew that it was necessary to give our users the best possible experience. The old editor was starting to show its age, and it was becoming increasingly difficult to add new features and functionality.

> **Early access**
>
> We are currently in the process of gradually rolling out our new editor to users. If you are interested in gaining early access to the updated version, please feel free to reach out to us.

### What is new?

#### Layout

We've made some changes to the layout while still maintaining the overall look of the old editor for familiarity. The new editor offers much better use of the available space.

We've reorganized the toolbars and menus to make them more intuitive and easier to use.

![](https://i.ibb.co/HNDsXKg/Screenshot-from-2023-03-24-15-51-29.png)

The sidebar is collapsible to give you more space to work with. You can freely move the canvas with our infinite viewer editor.

In contrast to our previous editor, the new version offers both light and dark themes that you can switch between.

#### Simplified image layer

We've merged static and dynamic image layers into a single image component, which makes it much easier to work with images in the editor.

#### Better layer control

Previously, when a layer was out of the frame of the canvas, the controls would disappear. If it was moved too far out of the canvas, it became unreachable.

This issue has been fixed in the new editor. Now, you can always access the layer controls, regardless of where the layer is located.

![Controls are gone when outside of canvas area](https://i.ibb.co/rFJPHVY/Screenshot-from-2023-03-21-22-13-53.png)

![Infinite viewer, controls are always visible](https://i.ibb.co/0BKhp23/Screenshot-from-2023-03-21-22-14-26.png)

#### Faster loading time

As we no longer depend on FabricJs, there's a significant improvement in performance. The editor now loads much faster, even when working with large templates or images.

![Before - same image, same fonts, same design](https://i.ibb.co/NSrcxj6/Screenshot-from-2023-03-24-16-18-41.png) ![After - same image, same fonts, same design](https://i.ibb.co/jWK9zjF/Screenshot-from-2023-03-24-16-19-30.png)

We are only scratching the surface of what we can achieve to improve the editor performance. We have several other techniques and approaches that we plan to implement to further enhance the user experience.

#### Better text support

By removing the canvas, we can improve the text support in our editor. Although FabricJS performs well in managing text, we have faced several limitations as we attempt to customize it further. For instance, enabling RTL support in FabricJS requires significant effort, and enhancing the text styling is also challenging. However, with our new editor, we aim to overcome these challenges, and you can expect to see more advanced text features in the upcoming releases.

### What's coming next?

The new editor allows us to do more in the future - we are excited to work on this feature!

#### Improved editor experience

The rewrite puts us back on a solid foundation and will help us develop a better editor experience. We're working on several improvements to the user interface and workflow, which should make it easier and more intuitive to create templates.

#### PDF generation

We're exploring PDF generation with the new editor. Any new templates made with the new editor will have an option to generate a PDF instead of an image.

As we're no longer using a canvas-based template, the generated PDF can include vectorized text, meaning that you can select the text in the PDF. This feature will be especially useful for customers who need to create high-quality print materials, such as brochures, flyers, and business cards.

#### Editor SDK

We have been eager to implement this feature for some time now. With this new capability, you can seamlessly integrate our editor into your backend system or even set up your service using our editor.

#### Feature parity

If you have noticed that certain components, such as graphs, from the previous editor are missing in the new version, please rest assured that we are actively working on integrating them into the updated editor and they will be available soon.

#### More features!

All these features are planned for the editor, but we also have features planned for the rest of Stencil. Stay tuned!

\--

In summary, we're incredibly excited about the release of our new editor. It represents a significant step forward in terms of performance, flexibility, and ease of use, and we believe that our users will be thrilled with the new features and functionality. We look forward to hearing your feedback and continuing to improve the editor in the months and years ahead.


# Template Editor (Legacy)


# Charts

Manual for proper chart usage

## Supported Charts

Currently we support five kinds of charts in your template

1. Bar chart - both horizontal and vertical with ability to stack y-axis
2. Line chart
3. Pie chart
4. Doughnut chart - uses pie chart menu with the ability to set the cutout percentage to create doughnut chart
5. Radar chart

## Data structures

### General structure

Generally, you would have JSON data that looks like this for your modification,

```javascript
{
  "template": "bfba4ccf-7153-4ae4-9e2e-6c736eaffef5",
  "modifications": [
    {
      "name": "radar_chart_4",
      "labels": [
        "Speed",
        "Passing",
        "Dribbling",
        "Defense",
        "Attacking",
        "Heading"
      ],
      "datasets": [
        {
          "backgroundColor": "rgba(0, 112, 0, 0.5)",
          "data": [
            89,
            95,
            70,
            40,
            90,
            80
          ],
          "label": "Player #1"
        }
      ]
    }
  ]
}
```

Chart modification data consists of two main fields,

| Field      | Description                 |
| ---------- | --------------------------- |
| `labels`   | List of labels for the data |
| `datasets` | List of `dataset` object    |

### Dataset object

Dataset object consists of three fields,

| Field             | Description                                                                       |
| ----------------- | --------------------------------------------------------------------------------- |
| `data`            | List of numbers. The index of the number corresponds to the index of the `labels` |
| `label`           | The label or title for the dataset                                                |
| `backgroundColor` | Either a color formatted string or a list of color formatted strings              |

#### `backgroundColor`

The format depends on the type of chart.

**Color formatted string,**

* Bar chart
* Line chart
* Radar chart

**List of color formatted strings,**

* Pie chart
* Doughnut chart

Color must be formatted according to these formats,

* Hex - `#FFFFFF`&#x20;
* RGBA - `rgba(255, 255, 105, 0.8)` which support alpha transparency

{% hint style="info" %}
Pie and doughnut charts needs to take list of colors because each color represent each cut out, while the other type of charts only need a single color to represent the data.
{% endhint %}

### Data validation

Our API will validate your modification and return a user friendly error message. However, it is useful to know what is considered as a valid chart modification because chart can be trickier than the rest of the template's objects.

#### Rules

1. `labels` must be unique
2. `labels` length must match the length of the `data`
3. Dataset's `label` must be unique
4. If `backgroundColor` requires list of colors, then the length must match the length of the `labels`
5. `backgroundColor` must have the right format i.e. either hex or RGBA value

{% hint style="info" %}
Don't worry about the complexity. Our API will return proper error messages when any of the rules are not met.&#x20;
{% endhint %}

#### Using Test API Console for guidance

![Use our built-in Test API Console to know which modification are available](/files/-McX3hshrgOAR-fHZBaT)

{% hint style="info" %}
It is also a good idea to play around in our Test API Console page to get a feel about the correct modification. Only successful image generation will count towards your quota.
{% endhint %}

## Using Data Editor

Template editor comes with an easy to use data editor in case you want *to create a static chart* (not modified through API) or *to create a default chart* which can be overridden through API.

![Data editor user interface](/files/-Mc8AIE9MgMZR1sdzhp7)

Data editor comes in two modes,&#x20;

* User friendly UI that validates the data as you type and will generate the proper JSON.
* JSON editor that allows you write the JSON yourself.

In both mode, data will always be validated so you don't have to worry about making mistake. The error will guide you on what to fix.

### Overriding default chart

As mentioned, you can create a default chart with all the colors and styles using data editor.

When sending the image creation request, you can choose to override the data and use the colors set with by the data editor. To do this, send the request with the same dataset `label` that you want to override.

{% tabs %}
{% tab title="Sample JSON generated by data editor" %}

```javascript
{
  "labels": [
    "Passing",
    "Dribbling"
  ],
  "datasets": [
    {
      "label": "Player #1",
      "data": [
        95,
        70
      ],
      "backgroundColor": "rgba(0, 112, 0, 0.5)"
    }
  ]
}
```

{% endtab %}
{% endtabs %}

You can send a modification request like so to override the `data` and use existing `backgroundColor`

{% tabs %}
{% tab title="Modification Request" %}

```javascript
{
  "template": "bfba4ccf-7153-4ae4-9e2e-6c736eaffef5",
  "modifications": [
    {
      "name": "radar_chart_4",
      "datasets": [
        {
          "data": [
            30,
            40,
          ],
          "label": "Player #1"
        }
      ]
    }
  ]
}
```

{% endtab %}

{% tab title="Response" %}

```javascript
{
  "labels": [
    "Passing",
    "Dribbling"
  ],
  "datasets": [
    {
      "label": "Player #1",
      "data": [
        30,
        40
      ],
      "backgroundColor": "rgba(0, 112, 0, 0.5)"
    }
  ]
}
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
It is important that you **set the same dataset's `label` in the modification request** so it knows which dataset to merge with.
{% endhint %}


# Limited Markdown Supports

Text object now supports limited markdown support so you can add bold, italic, underline, strikethrough and any combination of those options.

To enable this feature, click on your text object and find `Enable Markdown` option in the properties pane.

<figure><img src="/files/U6GMzKuUsDMlgu3F3sSX" alt=""><figcaption><p>Markdown option</p></figcaption></figure>

### Supported options

| Token               |                   |
| ------------------- | ----------------- |
| `**Bold**`          | **Bold**          |
| `***Bold/Italic***` | ***Bold/Italic*** |
| `_Italic_`          | *Italic*          |
| `__Underscore__`    | Underscore        |
| `~~Strikethrough~~` | ~~Strikethrough~~ |

### Integrations

You can use this feature through our API, Test Console, Signed URL, Query String URL, and all other integrations that we have as long as the template has `Enable Markdown` option enabled.

### Example

```json
{
  "template": "8ad9a3b8-cd62-4ac9-9b6f-d019addd8a04",
  "modifications": [
    {
      "name": "text_1",
      "text": "**Bold** _Italic_ ***Bold/Italic*** __Underscore__ ~~Strikethrough~~ **__Combination__**"
    }
  ]
}
```

<div><figure><img src="/files/2pTvO4QxDHHcMxZhrUFl" alt=""><figcaption></figcaption></figure> <figure><img src="/files/yOg0EnaWDuXeLz7KMwW5" alt=""><figcaption></figcaption></figure></div>


# Anchoring Element

Anchor To feature in template editor

## Dynamically Positioning an Element

Sometimes there is a time that you need text to be dynamically positioned next to certain element. For example, in our template below we want the price "$399" to be anchored next to "per pax" text.

Given that the price will change based on input from API, it is hard to position this statically without causing overlap especially if the price text can be very long.

![](/files/-MettjIEW5Oq789iHET5)

## Anchor To feature

To solve this problem, we added Anchor To field where you can select layer that can be set as the anchor.&#x20;

This option can be found in Textbox's settings,

![More options will pop out when anchor is selected](/files/-Metxd5SNIYtrpSJG6HP)

| Field     | Description                                                 |
| --------- | ----------------------------------------------------------- |
| Anchor To | Layer that you want your current text box to be anchored to |
| Anchor X  | Horizontal positioning relative to the anchor               |
| Anchor Y  | Vertical positioning relative to the anchor                 |
| Offset X  | Horizontal offset to be applied                             |
| Offset Y  | Vertical offset to be applied                               |

Offset is useful to fine tune the position to the pixel.

{% hint style="info" %}
Currently this option is only available for Text but you can anchor it to any other elements. It will be available on other components soon.
{% endhint %}

## Understanding Anchor Positioning

### Anchor X - Horizontal Positioning

![](/files/-Meu2V_ZBq5Gtshh0e2Z)

### Anchor Y - Vertical Positioning

![](/files/-Meu3vx2yLAIJGOhd4GB)

## Testing Your Design

To make sure your design will work well with your expected inputs, you can try to generate several images through Console. Alternatively, you could also type in the text to ensure the positions are correct.

{% hint style="success" %}
**Pro tips**

Anchored element position is fixed. If you are trying to anchor anything that is close to the edge, use the element nearest to the edge as the anchor. Otherwise, your text will be out of the canvas area.
{% endhint %}

![ ✅ Use per pax as the anchor of the price. The price can expand while per pax is fixed](/files/-MeuFnyPWwJdMsiOyMzj)

![❌ Bad choice of anchor can cause item to overflow unexpectedly](/files/-MeuGBNE6NvOsJVKT76A)


# Using Custom Font

Using custom font in Stencil is really straight forward. You can manage all custom fonts directly from the template editor.

### Supported Format

* TTF (TrueType)
* WOFF
* WOFF2
* OTF

### Managing Font

Click on any text and under the ***Font family*** section you should see ***Manage custom fonts*** option. Click on it to manage your custom fonts.

![](/files/H6hvh9K3x0xK6Fo1Ad61)

A popup should come out and you should be able to upload a new custom font or remove existing custom fonts.

![](/files/DvJhOEqhEB2gP1TJewCu)

#### Updating Font

By default, Stencil will get the font family name and its format by reading the font metadata. If that is not available, it will use the filename and its extension to detect the font family and the format.

#### Scope

All uploaded fonts are available in all your projects and templates.

### Troubleshooting Issues

#### I uploaded my font but my text box doesn't change, what should I do?

1. You can try to refresh you browser and select the custom font again in the Font family section.
2. Make sure that the font family and its format are correct. For example, although the filename is *MetalsmithRegular.ttf,* the font family is actually *Metalsmith* instead of *Metalsmith Regular*.

#### I deleted my font. What happens to existing templates that are using the font?

They will fallback to the default font. Your image will be generated as usual without issue but without the font.

If you're still having trouble, please contact support.


# Circular Text Positioning

### Understanding Arc Position and Alignment

#### Position

There are four available positions,

1. Top
2. Bottom
3. Left
4. Right

Setting the Arc Position to one of this values will position the text based on this anchor position.

![Arc Position set to Bottom](/files/T5haHM82uv5dJssN957I)

#### Alignment

There are three alignment options,

1. Left
2. Center (default)
3. Right

This option allows you to decide how to align your text based on the anchor position.

![](/files/ZKEmoxjOWjaeub7n7wXs)**Position**: Bottom, **Alignment**: Center

![](/files/PvWwq8W4g0bVHslrmi68)**Position**: Bottom, **Alignment**: Left

![](/files/gXhF44PztkOBy107ARiE)**Position**: Bottom, **Alignment**: Right

### Manual Adjustment

Certain fonts don't align correctly because the length of the text can be inaccurate. This usually happens with custom font which usually doesn't contain the right metadata.

To work around this, you can set an offset to align your text properly.

![](/files/f3lwK5ZDCoHhQ3EQ21s5)


# White Label for Business


# Setting Up Your First Client

Guide to getting started with white labeling Stencil

## What is white labeling?

Stencil offers our business users to personalize Stencil's branding and limit image generation features to only you. Your clients can create projects and templates, and these templates are accessible from your account. This allows you to collaborate directly with your client in term of designing your campaign.

{% hint style="info" %}
This feature is only available to users who subscribe to Business plan with white labeling feature.
{% endhint %}

## Getting started

### Creating your first client

If you are subscribed to the correct plan, you should be able to see `Clients` menu in the left sidebar. Click on it to access the `Clients` section.

<figure><img src="/files/sssefshjFfWaD3bLPbRy" alt=""><figcaption><p>Clients section</p></figcaption></figure>

Click on `Add new client` to create your first client. You will be redirected to a form to set up your client details.

Here you can upload a custom logo to personalize the branding. Name will also be used to customize the browser's title.

Right now, there are two languages supported - English and Brazilian Portuguese. This will be the default language when your client's user first log in into white labelled Stencil. They can change between languages in their user profile if they have other preference.

Next, for the `domain`, key in the domain from where the white labeled Stencil will be accessed. Your client will only have access to white labeled Stencil from this specified domain.&#x20;

<figure><img src="/files/2Im9JZYweuIDdQ44LEOh" alt=""><figcaption><p>Create client form</p></figcaption></figure>

{% hint style="info" %}
Learn how to set up the DNS for the domain you specified here - [DNS Setup with Cloudflare](/using-stencil/white-label-for-business/dns-setup-with-cloudflare)
{% endhint %}

### Testing your setup

Once you have created your first client and have set up the proper DNS settings to point your custom domain to Stencil's server, you can go to the specified domain.

You should be able to see login page to Stencil but with your client's logo.

Right now you can't login yet because you haven't add any user yet. Continue reading to the next section - [#managing-clients-access](#managing-clients-access "mention").

<figure><img src="/files/QsA5ler6wBvV7FDaHmFj" alt=""><figcaption><p>Your own white labeled Stencil</p></figcaption></figure>

### Managing client's access

Access to white labeled Stencil can only be managed by you. You need to add new user to access the white labeled Stencil. To do that, click on `Manage users`.

<figure><img src="/files/SmAptP4uKDaKuhNgDMAX" alt=""><figcaption></figcaption></figure>

You will redirected to a form to add a new user. Specify the email and default password for your new user.

<figure><img src="/files/ILxozx3xqKjsQnNbZPxw" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Your client's users can change the default password by going to their profile page.
{% endhint %}

### What can your client see?

When accessed through the custom domain (i.e. white labeled Stencil), your client can only see projects and templates. Other image generation functionalities and integrations are disabled and they won't be able to see them.

<figure><img src="/files/Xx1T5D7dHJ28paZ5GEv3" alt=""><figcaption><p>Project screen</p></figcaption></figure>

Your client can add new project and template. They can edit templates but not generate images from them.

<figure><img src="/files/DbGuTKvT23ii2BJwA9Lp" alt=""><figcaption><p>Template screen</p></figcaption></figure>

### How do I access their projects?

In order to access their projects and templates, simply go to `Projects` screen like you would normally do.

You will notice that you have an extra tab called `Clients`.

<figure><img src="/files/7mUWN8GLCDB3BTu5DQ4t" alt=""><figcaption><p>Your client's projects are listed in this section</p></figcaption></figure>

From here you can access your client's projects and their templates. You would generate images like you would normally do.

<figure><img src="/files/ed3WjHOwFifBixWp1NJ6" alt=""><figcaption><p>Your client's template but with all the features enabled just for you.</p></figcaption></figure>

<figure><img src="/files/8UEfmdEogZmInRAF3EHK" alt=""><figcaption></figcaption></figure>

Image generation quota is taken from your account as your client's do not have the ability to generate image.


# DNS Setup with Cloudflare

Setting up custom domain

{% hint style="info" %}
We highly recommend that you use your Cloudflare to manage your DNS. It comes with free SSL setup that is easy to use to ensure your site is secure.
{% endhint %}

Although this setup is used with Cloudflare, you can apply the same settings if you're using other services to manage your DNS.

If you're using Cloudflare, go to your domain that you'd like to redirect to Stencil's white label version.

<figure><img src="/files/xxFkq2W3F1fpKTywLVMT" alt=""><figcaption></figcaption></figure>

Then, add a new record.

<figure><img src="/files/vNDuL8zX7IaTIN6o7kTo" alt=""><figcaption></figcaption></figure>

Set a `CNAME` record to point to `app.usestencil.com` and fill in the required subdomain. This must match the subdomain that you have provided when setting up client in Stencil.

If you're using Cloudflare, you can also enable proxy status so that the connection is proxied through Cloudflare before going to Stencil's server. This gives your free SSL out of the box.

<figure><img src="/files/GBDFRdKsVBoLaqQgb9Z4" alt=""><figcaption></figcaption></figure>


# Embeddable Live Preview

This section explains how to improve your Stencil integration within your app by adding live preview to the template based on your changes.

If you're familiar with Stencil's Form integration, you'll notice that the preview tab shows live preview of your template based on the form inputs. This is exactly what Embeddable Live Preview is - it adds live preview of the template to your custom solution.

<figure><img src="/files/J2vO5xXOqUO31VhOH7Oj" alt=""><figcaption><p>Live template preview in Form integration</p></figcaption></figure>

## Better UX

For advanced users seeking to deeply integrate Stencil into their app with maximum flexibility, using our Form integration might be limiting. Combining the Embeddable Live Preview feature with our image generation API is the most effective solution in this case.

### Common use case

Most common use case revolves around having to preprocess the input or limiting the input to the custom form before passing the inputs to Stencil's image generation API.

* You need a custom form that allows user to search from your database to prefill some values.
* You need conditional inputs that may depend on other related inputs.
* You want to limit what users' input based on your own criteria.

If you answer yes to any of these, you'll probably need to build a custom solution that fits you. However, one missing part is having a preview to your template based on users input. Embedding live preview solves this elegantly.&#x20;

* You have full control on the UI and business logics for your inputs.
* Save costs since users have access to the live preview without having to generate image to see the changes.&#x20;
* Improved UX, users get instant feedback and reduce frustration.

{% hint style="success" %}
[Forms Integration](/integrations/forms-integration) is the better option for users looking for drag-and-drop experience in generating image while having the option to integrate image generation service into their app.

If you don't have the above requirements, generally using our form integration with our built-in form builder is sufficient.
{% endhint %}

## Using Embeddable Live Preview

There are two parts to this.

1. Building your own solution for gathering inputs from users - this can be a form that populates data from your database or with additional business logic applied to the inputs.
2. Inserting embeddable live preview and connecting it to your solution.

We won't cover building your solution as this can vary between businesses but the general idea is to gather inputs and use the inputs to feed to image generation API.

We only cover the latter where we can use the same inputs to the API to also build a preview for the template.

#### 1. Embed template live preview

You can copy embed link from your template menu. Then, use `iframe` to display this preview.

```html
<iframe 
  id="template-preview" 
  src="https://app.usestencil.com/live-preview/8be62eca-72de-4f81-9edc-a4dbeee6986f?token=abcdefghijkl" 
  style="width:600px; height:600px; border:1px solid black;">
</iframe>
```

The iframe content will resize automatically to fit the container.

#### 2. Send modification

To update the preview, you can send a message to the `iframe` with your modification.

```javascript
const preview = document.getElementById('template-preview');

button.addEventListener('click', () => {
  const modifications = [{
          "name": "image_1",
          "src": "https://images.unsplash.com/photo-1499678329028-101435549a4e?q=80&w=2400&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
      },
      {
          "name": "text_2",
          "text": "Hello world"
      },
      {
          "name": "discount_tag",
          "visible": "{{show_discount}}"
      }
  ]
  const message = {
      namespace: 'stencil',
      action: 'preview_changes',
      data: modifications,
      variables: {
          show_discount: true
      }
  };

  // Send message to iframe
  preview.contentWindow.postMessage(message, 'https://app.usestencil.com');
});
```

Currently, `namespace` and `action` must be set to `stencil` and `preview_changes` respectively like in the above example.

The example above updates the template preview each time the button is clicked, but ideally you should do this whenever your custom form inputs changes.

## Variables

Variables are also supported by adding the `variables` property. See the above example for usage.

For more details, see [Template Variables](/using-stencil/template-variables)

## Handling Errors

Template data returned directly without wrapper:

```json
{
  "id": "template-id",
  "name": "Template Name",
  "template": { ... },
  // ... other template fields
}
```

### Error Response

Structured error format with `status` field:

```json
{
  "status": "error",
  "error": {
    "type": "build_template_error",
    "message": "Error building template",
    "details": {
      "variables": "Variable 'userName' expected type string but got number"
    }
  }
}
```

### Error Types

| Type                       | Description                | Details Field        |
| -------------------------- | -------------------------- | -------------------- |
| `template_not_found_error` | Template doesn't exist     | `null`               |
| `invalid_request_error`    | Invalid request parameters | Error message string |
| `build_template_error`     | Template rendering failed  | Error details map    |
| `invalid_digest_error`     | Invalid authentication     | `null`               |

### Frontend Implementation

#### Detecting Errors

Check for the presence of `status: "error"` field:

```javascript
socket.addEventListener("receive-preview-image", (event) => {
  const response = event.detail;

  if (response.status === "error") {
    // Handle error
    handlePreviewError(response.error);
  } else {
    // Success - render preview
    renderPreview(response);
  }
});
```

#### Handling Errors from Iframe

The iframe forwards errors to the parent window via `postMessage`:

```javascript
window.addEventListener("message", (event) => {
  if (event.data.namespace === "stencil" && event.data.action === "preview_error") {
    const error = event.data.error;

    // Display error message
    showErrorNotification(error.message);

    // Handle specific error types
    switch (error.type) {
      case "build_template_error":
        if (error.details?.variables) {
          highlightVariableErrors(error.details.variables);
        }
        break;

      case "template_not_found_error":
        redirectToTemplateList();
        break;
    }
  }
});
```

### Sample project

We have developed a simple React application that demonstrates the usage of embeddable live preview. The demo consists of a live preview for products database.

{% hint style="info" %}
Clone the project here <https://github.com/usestencil/stencil-embed-preview-demo>
{% endhint %}


# Template Variables

## What is template variables?

Template variables allow you to connect multiple fields together by sharing the same value across different elements in your template.

**The Problem Without Variables:**

When the same value needs to appear in multiple places, you'd have to specify it repeatedly in your modifications:

```json
{
  "modifications": [
    {"id": "header_name", "text": "John Doe"},
    {"id": "body_name", "text": "John Doe"},
    {"id": "footer_name", "text": "John Doe"},
    {"id": "signature", "text": "John Doe"}
  ]
}
```

This creates several issues:

* Repetitive and error-prone
* Risk of inconsistency (typos, different values)
* Hard to maintain when values change
* Verbose API requests

**The Solution With Variables:**

Define the value once and reference it everywhere:

```json
{
  "modifications": [
    {"id": "header_name", "text": "{{user_name}}"},
    {"id": "body_name", "text": "{{user_name}}"},
    {"id": "footer_name", "text": "{{user_name}}"},
    {"id": "signature", "text": "{{user_name}}"}
  ],
  "variables": {
    "user_name": "John Doe"
  }
}
```

## Creating variables

Variables are defined in your template's configuration. Inside the template editor, you'll see a "Variables" button at the top bar.

<figure><img src="/files/yWyV4Xi7MZHOfVr9PXqm" alt=""><figcaption></figcaption></figure>

### Variable types

Variables support three types:

* `string`
* `number`
* `boolean` (False/True)

### Defining variables

You'll need to specify the variable name and they have to be unique. Each variable also requires default value.

<figure><img src="/files/UPJWnNdfoixXjZxfVJgc" alt=""><figcaption></figcaption></figure>

## Using variables

Currently, variables can only be used with the API. Soon, we are extending this to other integrations.

### With API

Apart from the `modifications` field, you can now specify `variables` field to define the value that should be assigned to the variable. If you don't specify it here, the default value will be used.

#### Example of using variables with image generation API

```json
{
  "template": "a2bdf44a-5fba-467d-a4d2-8685b1c4f8f5",
  "modifications": [
    {
      "name": "image_1",
      "src": "/images/image-placeholder.jpg",
      "visible": "{{visibility}}"
    },
    {
      "name": "text_2",
      "text": "Hello {{name}}!",
      "visible": "{{visibility}}"
    }
  ],
  "variables": {
    "visibility": false,
    "name": {
      "type": "string",
      "value": "John Doe"
    }
  }
}
```

The above example shows that we can control both layers' visibility by setting the variable `visibility` to `false`.&#x20;

Also, note that for variable `name`, we are declaring and defining variables right from the API without using the template editor. In this case, the `name` variable is only temporary for the current request.

Without variables, you'll need to set them individually.

Variables are useful for cases where you want to link some fields together. A common use case is to hide and show certain fields together.

## Variable Resolution

Variables are resolved in two places during generation:

1. **Template Resolution**: Variables in the template  itself are replaced with provided values
2. **Modification Resolution**: Variables in the modifications array are replaced with provided values

### Resolution Order

1. Use value from request's `variables` object if provided
2. Fall back to template's default value if defined
3. Return error if variable is required but not provided and has no default

### Type Validation

The system validates that provided values match the declared variable type:

* Text variables must receive string values
* Number variables must receive numeric values (integers or floats)
* Boolean variables must receive boolean values (true/false)

Type mismatches will result in an error.

You can look at the Console of each template to see the expected type for each field.

<figure><img src="/files/hbBHmm7zJ17RfVOqccER" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
`number` variable type refers to either `integer` and `float` in the console page. `array` type is not supported for variables.
{% endhint %}


# Airtable Integration


# Basic

With Airtable integration, it allows images to be generated from data pulled from Airtable. Generated images are automatically populated back into Airtable.

### Requirements

* Airtable's Base ID
* Airtable's Table name
* Airtable's API Key

{% hint style="info" %}
You can get base ID for your airtable project by going to their documentation site
{% endhint %}

### 1. Set up your project with Airtable API Key

Go to project's setting page and paste your Airtable API Key.

![](/files/-MavUYNWKc0XVJsXR3a4)

### 2. Create Airtable integration action

Go to your project and go to the Console page for the template that you would like to generate the image.

![Select Airtable Integration](/files/-MavVHgsunCMiwRrcaj0)

Then, create New Action.

### 3. Map the column in Airtable to your template&#x20;

Fill in the requested information in Step 1 and Step 2. You need Airtable's Base ID and Table name to complete this step.

Then, match the column name in your Airtable table to your template.

![](/files/-MavWADYU7JMTkgBZF5y)

{% hint style="info" %}
For image modification, we support Airtable's field for URL, Text and Attachments.\
\
For multiple attachments, the first attachment will be used as the source of the image. We only support mime types `image/jpeg` and `image/png`.
{% endhint %}

![The difference between Attachment and URL/Text in Airtable](/files/-MfxOKE_AgjWjM1nxfHo)

![Attachment Type](/files/-MfxO_qWwTGqbzBe8gCz)

![URL Type](/files/-MfxOiCatMK99s_-NUjM)

### 4. Set the output column

Fill in the column name in Airtable that will be populated with your image. This column must be of type `Attachment`.&#x20;

Save your changes when you're done.

{% hint style="warning" %}
**The type of the column must be `Attachment`.**

Both PNG and JPEG images will be attached to the column.
{% endhint %}

### 5. Run your action

![](/files/-MavWuaAmXWklWM_KHxW)

Click on Run Action to start generating images. You can safely close the browser as the process is run in the background.&#x20;

Once they are done, you can view the generated images in Airtable. Both PNG and JPEG are uploaded to Airtable.&#x20;

{% hint style="warning" %}
Images are only generated for empty output column. If they already contain data, no images are generated for those particular rows.
{% endhint %}

### Example process

{% embed url="<https://www.usestencil.com/statics/videos/airtable-integration.mp4>" %}


# Integromat Integration


# Connection

Connection allows you to authenticate Integromat with Stencil

Most modules provided by Stencil in Integromat requires authentication to properly work.

### Connection

Integromat uses Connection object to do this. When you use any module that requires authentication, you need to either add new connection or use existing connection.

The image below shows an example of `Create Image Asynchronously` module that will ask for Connection. Simply provide the project's API key to create new Connection.

![](/files/-MeXdLEfeDAfQn3JT_Kj)

{% hint style="info" %}
Due to the nature of API Key being scoped to a single project, connection object will inherit the behavior too. You need to use the right connection for the right project.
{% endhint %}


# Instant Trigger

Guide for setting up Integromat Instant Trigger

By setting Instant Trigger, it allows Integromat scenario to receive generated image immediately.

Stencil supports this feature out of the box and setting it up requires minimal effort. For now Instant Trigger support is scoped per template.

{% hint style="info" %}
Integromat's Instant Triggers execute the flow immediately after the remote server sends data. It is powered by webhooks.&#x20;
{% endhint %}

## Requirements

1. Instant Trigger's webhook URL
2. A working template in Stencil

## Recommended Approach

This approach scopes the Instant Trigger on template level. That means all images generated from this template will trigger Integromat Instant Trigger.

### 1. Set Trigger in scenario

![](/files/-MeX0Cg9PsVU5d3AZcSe)

In your scenario editor, select Image Generated trigger from the list of available modules. This allows Integromat to listen for data coming from Stencil.

{% hint style="warning" %}
Instant Trigger must be the first module in scenario
{% endhint %}

### 2. Set up Trigger webhook

Add a new webhook or use existing webhook based on your need. Then copy the given webhook URL.

![](/files/-MeX1lDLr5SOZfwx3iEW)

### 3. Set Trigger webhook in Stencil's Console

Go back to Stencil's webapp, select the template that you want to integrate with Integromat.

Then, go to its console and select Integromat under the Integrations tab.&#x20;

Paste the webhook URL from the previous step to the input box. Make sure your save your changes.

![Integromat integration section in Console](/files/-MeX2oaFOg7vpiYU6UZ2)

Congratulations! You have successfully set up your Integromat Instant Trigger. We've told you that it's going to be easy.

### 4. Test your changes (optional)

#### Test scenario

For testing purpose, I've setup my scenario to look like this. The trigger is linked to a `Get multiple variables` module (Integromat's tools module) and will capture two variables from the output of the trigger.

![](/files/-MeX4XQ9UiQGcG56Lw7V)

Once you've got that set up, run the scenario and it will start listening for data.

#### Sending data from Console

Go to your template's Console page that is linked to the trigger. Use the Test API tool to send a request to generate image.

![Sample request sent through Console](/files/-MeXXQ4vH1oQXA9rPBVG)

If image is generated successfully, you should see similar output in your trigger module.

![Instant Trigger module successfully received data being sent from Stencil.](/files/-MeXWxps30CTd8PvcZ9g)

### Alternative approach

The other approach to trigger instant trigger is by setting the `webhook_url` field with Integromat's Dedicated Instant Trigger webhook URL when generating the image.

{% tabs %}
{% tab title="POST <https://api.usestencil.com/images>" %}

```javascript
{
  "template": "68b168a8-17e8-4702-92fd-838c019bc3ec",
  "modifications": [
    {
      "name": "text_1",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do..."
    }
  ],
  "webhook_url": "https://hook.integromat.com/2rxxxxxxxxxxxxxxxxxxxx"
}
```

{% endtab %}
{% endtabs %}

The downside of this approach is that you would have to set this each time you generate the image.


# Zapier Integration

Integrate your projects with 3,000+ services and applications

[Zapier](https://zapier.com/) is a no-code platform that allows you to automate workflow and integrate your Stencil projects to 3,000+ (and growing) available applications and services without having to do custom software development.

We recommend you go through [Zapier University](https://zapier.com/university) to become more familiar with it. It will be helpful for you and not just to work on your Stencil projects. We assume you are familiar with Zapier terms, such as zap, trigger, action, etc.

Alternatively, we also support Zapier integration through our image request's webhook. For an example of usage, please take a look at [`Sending Charts to Twitter`](/integrations/case-studies/sending-charts-to-twitter).


# Authentication

The first step in getting the automation in Zapier is setting up the authentication with your Stencil project. You’ll need the `API Key` from the Project’s settings page.

<figure><img src="/files/BFsfAbeHPV9wBSq0fSjh" alt="API Key in Project Settings"><figcaption><p>API Key from the Project Settings section</p></figcaption></figure>

Keep this API Key handy as you'll need this in the next section.


# Trigger - New Image

{% hint style="info" %}
You can learn more about Zap [here](https://zapier.com/university-101-section-3-understanding-a-zap).
{% endhint %}

Now, let’s head over to Zapier.

* Navigate to the `Zaps` [page](https://zapier.com/app/zaps) and create a new Zap.
* You should see a screen like a screenshot below on the new page. Search and select `Stencil` from the available services.

<figure><img src="/files/0E8TS6maXrBSJT6dlAVx" alt="Zapier trigger"><figcaption><p>A trigger is an event that starts your Zap.</p></figcaption></figure>

* Select the trigger that you want to use and click "Continue"

<figure><img src="/files/2PuujPV3pReKnwtJGPMc" alt=""><figcaption><p>Choose the trigger event.</p></figcaption></figure>

* If this is your first time connecting to Stencil services, then you'll see a similar screen to the screenshot below. Click "Sign in" to begin the authentication process.

<figure><img src="/files/LFdxJOh48aEovxkpRBWZ" alt=""><figcaption><p>First time connecting to Stencil service.</p></figcaption></figure>

* Enter the `API Key` you’ve obtained from the project's settings that you want this Zap to connect to into the text field and click on “Yes, Continue” to proceed.

<figure><img src="/files/XBqK9kAejcJ3glT6IWIf" alt=""><figcaption><p>API Key window</p></figcaption></figure>

* Once connected, you may optionally test the integration if you have previously at least generated an image. That’s all for creating triggers and performing authentication.
* You can then connect the trigger to any desired action, such as email or SMS, or create a row in a Google spreadsheet. Output information from the selected trigger is made available in the selected action. If you’ve selected the “New Image” trigger and “Email” action, you could, for example, attach the newly created image as an attachment to the email.

<figure><img src="/files/WoiSp59G6M2AOKAYYd1Q" alt=""><figcaption><p>Customizing action</p></figcaption></figure>


# Action - Create Image

Right now, we'll assume that you are familiar with creating Zap, especially creating a trigger. If not, you may visit the [Trigger - New Image](/integrations/zapier-integration/trigger-new-image) page.

`Create Image` is the core functionality of Stencil. For this action to work, you must have already created a [Project](https://app.usestencil.com/projects) and a Template. Pay special attention to the available modifications (refer to the samples below).

<figure><img src="/files/PnNU67W3wDucSwOWfyuo" alt=""><figcaption><p>Available modifications</p></figcaption></figure>

<figure><img src="/files/ts4f2kvNLArwl2EGX40S" alt=""><figcaption><p>Available modifications in JSON format</p></figcaption></figure>

Stencil's `Create Image` action allows you to provide template modification on the fly based on the outputs available in the previous components in your Zap's workflow, such as the triggers or search.

{% hint style="info" %}
`Create Image` action is an asynchronous action, which means that the next component in the current Zap will run immediately, and the Stencil service does not yet create the image. You may either put a Wait block or create another Zap with a `New Image` trigger to fetch the completed image and the available URLs.
{% endhint %}

<table><thead><tr><th width="183">Field</th><th>Remarks</th></tr></thead><tbody><tr><td>Template</td><td>The template that you want to generate an image.</td></tr><tr><td>Modifications</td><td>Show either the <code>Required</code> modifications only or together with <code>Optional</code> modifications.</td></tr></tbody></table>

In the example screenshot below, we have a `New Incomplete Task` trigger from the Todoist service, assigning (or binding) the `Notes` value to the Text 1 text's value. Hence, whenever there's a new incomplete task being created in your Todoist service, a new image is created by Stencil based on the information that you've specified in your Zap. Neat, right?

<figure><img src="/files/ItdWhP74QntcV8HAmvPa" alt=""><figcaption><p>Customizing <code>Create Image</code> action</p></figcaption></figure>

Once satisfied with the assignment, you can continue publishing your Zap.


# Secure Signed Image


# Basic

On-demand image creation by secure URL parameters

Your image modifications are signed with your project's API token, thus your image can only be modified by you.

{% tabs %}
{% tab title="Elixir" %}

```elixir
def create_signed_image do
  user_id = ""
  secret_key = ""
  base_id = ""
  
  modifications = 
    Jason.encode!([
      %{"name" => "text_1", "text" => "secure image"},
      %{"name" => "rating_2", "rating" => 5}
    ])
    |> Base.url_encoded64(padding: false)
    
  parameters = "#{user_id}+#{base_id}+#{modifications}"
  
  signature =
    :crypto.hmac(:sha256, secret_key, parameters)
    |> Base.encode16()
    |> String.downcase()
    
  "https://images.usestencil.com/signed-images/#{user_id}/#{base_id}.png?modifications=#{modifications}&s=#{signature}"
end
```

{% endtab %}

{% tab title="Python" %}

```python
import json
import hmac
import hashlib
import base64

def base64_encode(string):
    """
    Removes any `=` used as padding from the encoded string.
    """
    encoded = base64.b64encode(string.encode())
    encoded = encoded.rstrip(b'=')
    return encoded

def generate_url():
  user_id = ""
  secret_key = ""
  base_id = ""

  modifications = [
    { "name": "text_1", "text": "secure image" },
    { "name": "rating_2", "rating": 5 },
  ]

  # ensure no spaces in json output
  encoded = json.dumps(modifications, separators=(',', ':'))
  encoded = base64_encode(encoded).decode()

  parameters = "{user_id}+{base_id}+{modifications}".format(user_id=user_id, base_id=base_id, modifications=encoded)

  signature = hmac.new(secret_key.encode(), parameters.encode(), hashlib.sha256).hexdigest()

  url = "https://images.usestencil.com/signed-images/{user_id}/{base_id}.png?modifications={modifications}&s={signature}".format(
      base_id=base_id,
      user_id=user_id,
      modifications=encoded,
      signature=signature
  )
  print(url)

if __name__ == "__main__":
    generate_url()

```

{% endtab %}
{% endtabs %}

### Changing image format on the fly

We support both JPEG and PNG. To use PNG, change the image in the url to `.png` and to use JPEG, change the image in the url to `.jpg`

### Cache

Image is only generated once (synchronously on the first request) and subsequent images are pulled from cache. Images pulled from cache do not count towards your quota.

{% hint style="warning" %}
**Order matters.**&#x20;

In order for cache to work properly, please specify the signature parameter last in the query string i.e. `modifications=<your-modifications>&s=<your-signature>`.
{% endhint %}

#### Cache invalidation

Sometimes it is useful to invalidate cache, you can add `ts=<random number>` to invalidate the cache and a new image will be generated (this will count towards your quota).


# Query String Integration

Generate image variations through query string URL


# Basic

Guide to generating your first image variation with query string integration

## What is query string?

Query string is a key value pair that you can append to a URL. In this particular use case, we can use it to encode modification that needs to be applied to a template.

{% hint style="success" %}
A good use case for this integration is to create open graph image, product images for your e-commerce store and many more.
{% endhint %}

By creating query string URL for your template, you can create variation of the image by just simply modifying the query string.&#x20;

For example, the link below has two query strings - **title** and **src.** It generates the following image.

```
https://images.usestencil.com/qs/<template_id>/<image_base>.png?title=Pienza%2C%20Italy&src=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1569416078500-3857b00616f8%3Fixid%3DMnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D676%26q%3D80
```

![First variation](/files/-MgTX1YkDb8yvnyYIBEX)

By simply changing the query string **title** and **src** to something different, you could generate a new variation like below,

![Second variation using the same template](/files/-MgTY8bbovEXNezeRPYW)

## Guide

We will walk you through to create your first image variation.

### Requirements

Unlike other integration, this is the simplest form of integration. All you need is your template. If you don't have one, choose from our selection of presets.

### 1. Select template to build query string integration

From the drop down menu, select "Query String URL". Alternatively, you can select "Console" and navigate to "Integrations" > "Query String".

![](/files/-MgTZtIEabPfMiKDF7GW)

### 2. Customize your query strings

You should now see the Query String wizard

![Query String Integration wizard](/files/-MgT_Ty-19DBOnuujum1)

#### Base URL

Our query strings are appended to this base URL. Base URL lets us know which template you want to create variation for.

#### Allowed Origins

This allows us to block any requests that come from unauthorized domains.&#x20;

You can specify multiple origins, they must be separated by comma. Wilcard (\*) character is allowed. Here's few examples.

| Allowed Origins                                 | Description                                                                                              |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `*`                                             | Allow all origins                                                                                        |
| `*.usestencil.com`                              | Allows `app.usestencil.com,` `images.usestencil.com`, and anything with that ends with `.usestencil.com` |
| `usestencil.*`                                  | Allows `usestencil.com`, `usestencil.org`, `usestencil.io`, etc                                          |
| `usestencil.com, *.usestencil.com, example.com` | Only allows `usestencil.com`, `example.com`, `app.usestencil.com`, `api.usestencil.com`, etc             |

{% hint style="warning" %}
We highly **recommend** that you setup this value to only domains you're going to use the integration with.

**Why?**

Unlike [Secure Signed Image](/integrations/secure-signed-image), anyone can create variation from this URL. If your templates are really good looking (which I'm sure they are) and without your branding, then anyone can use the URL to create variation for their own use. This will eat up your image generation quota.
{% endhint %}

#### Allowed Query Strings

Here you would customize which query string belongs to which object to be modified.&#x20;

![](/files/-MgTd6omYQFn4pjeeK5D)

In this example, we create two query strings - `image` which should change the `src` field for `bg_image` object and `title` which should change the `text` field for `text_4` object.

The preview will show you how the URL will look like. Of course you need to replace those values with a real value and it must be properly [URL encoded](https://www.w3schools.com/tags/ref_urlencode.ASP).

Don't forget to save it you're done!

If you're unsure which object belongs to which item on your template, simply go to the "Test API" and consult the table for the right information.

![Test API shows all the available modifications for your specific template](/files/-MgTe7zgZ7xoK_s8TG_G)

### 3. Testing your integration

To test your query string, copy the URL from previous step and modify the query strings properly.

```
https://images.usestencil.com/qs/34aacb1e-a7e4-4b4d-bb64-2bffc3c930a4/CAfyfe8Cicm4bLiuKcTi45.png?image=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1590523277812-c3cc1176dd79%3Fixid%3DMnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D675%26q%3D80&text=Grand%20Canyon%2C%20USA
```

The above links give me,

![Result](/files/-MgTfTFF3ad2POZo5xMS)

The first image will take a few seconds to generate. If the modifications remain the same, the subsequent call to the image will retrieve the image from cache.

Read more about [cache and quota](/integrations/query-string-integration/cache).


# Cache

How query string cache works

## Quota calculation

In other word, you will only be charged when our server generates an image for you i.e. cache miss on our server.

![Network flow between user request, Amazon's Cloudfront and Stencil's server](/files/-MgTjsj5OewOQx8qp34U)

## How it works?

### 1. User sends a request

User's browser send a request to get an image. This image points to Query String URL integration that we have set up previously.

### 2. CloudFront intercepts the request

Images are cached on Amazon CloudFront. This is why Stencil image's load is so fast because we are using Amazon CloudFront to cache in multiple locations throughout the world.

If images are in CloudFront's cache, it will return the cache version (i.e. go to step 4 directly). Otherwise it will send a request to Stencil.

### 3. Stencil's cache

Stencil maintains a smart cache, we won't go into details on how it works but it can figure out whether you're generating the same image as before or it is a new image.

If a new image needs to be generated, your quota will be deducted. Otherwise, your usage remains unchanged.

### 4. Back to Cloudfront's cache

Amazon CloudFront will cache this image returned by our server. Any subsequent requests will hit Amazon CloudFront.&#x20;

## Headers

To assist with issues, image response header contains some useful information to know whether cache is working as intended.

| Header            | Description                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `x-cache`         | This is CloudFront cache header. `HIT` when retrieving from CloudFront's cache, `MISS` when image is not in CloudFront's cache. |
| `x-stencil-cache` | This is Stencil cache header. `HIT` when Stencil returns the image from its cache. `MISS` when Stencil generates new image.     |
| `x-stencil-error` | If image fails to be generated, please check the status code and this header. This header contains the reason of image failure. |

{% hint style="info" %}
Sometimes you will get`x-cache: HIT` and `x-stencil-cache: MISS` within the same response. Don't worry, your usage will remain unchanged.&#x20;

This happens when an image is first generated and the subsequent cache hit from CloudFront also caches this header value.
{% endhint %}


# Forms Integration

Stencil's From allows you to generate image without requiring technical know-how. It provides an intuitive form builder and allows you to embed the form in any website or in your application.

<figure><img src="/files/NByZQeEvI3VNF5ebYGsE" alt=""><figcaption><p>Form Builder</p></figcaption></figure>

## Customization

You start by adding a new block and selecting the field of your layer to allow for users to update.

The generated input field depends on the type of the field selected i.e. field requiring color will generate color picker, field requiring numbers like `x` and `y` position will generate a number input and image will generate a file upload or a link input. All other fields are defaulted to a text input.

You can also customize the label for each field to make it more friendly.

### Layout

We provide two layouts when viewing the form - **Cosy** and **Compact**.

<figure><img src="/files/fCApRgX6oiIPjZdyNf2L" alt=""><figcaption><p>Cosy layout</p></figcaption></figure>

<figure><img src="/files/UZg6qm0AW5n0SGqBec5N" alt=""><figcaption><p>Compact layout</p></figcaption></figure>

#### Cosy layout vs Compact layout

The only difference of the two layouts is that cosy layout shows live preview of the template while you're editing them.

{% hint style="info" %}
While **live preview** of the template changes with your changes, it doesn't consume your image generation credit. Your credit is only consumed when you generate the images or PDFs.
{% endhint %}

## Embedding form

You can copy the embed code by clicking on the `Embed` button.

<figure><img src="/files/y9SGYZgk3AJXcuxGK99J" alt=""><figcaption><p>Embedding form</p></figcaption></figure>

<figure><img src="/files/nBvMNCcCr1tgvYItFKHd" alt=""><figcaption></figcaption></figure>

## Advanced section

### Customize image upload picker

In some scenario, it makes sense to only allow images to be picked from a custom gallery, S3 buckets, or anything that you can think of.

Stencil's Form allows the image upload to be customized according to your need by leveraging on `iframe` communication.&#x20;

Consider your website embedding the form as the `parent` and the embedded form (`iframe`) as the `child`.  Stencil provides a hook to communicate between these two.

#### Setting up the form for custom image upload

When you configure the image layer, set `src` field uploader type to custom picker.

<figure><img src="/files/z4jwL4CwNP4ntjvc5mih" alt=""><figcaption><p>Custom picker option</p></figcaption></figure>

Next, you'll need to set up the communication between the `parent` and the `child`.

#### Events&#x20;

When the image picker button is clicked, the form (`child`) sends an `event` to the `parent`. The `parent` is responsible in listening and reacting to this `event`.

**Event sent from `child` to the `parent`**

```javascript
{
    namespace: "stencil",
    action: "choose_image_start",
    data: {
        layerId: "<layerId>",
        layer: "<layer>"
    }
}
```

**Event sent from `parent` to the `child`**

```javascript
{
    namespace: "stencil",
    action: "choose_image_end",
    data: {
      	url: "<link to the image>",
        layerId: "<layerId>",
        layer: "<layer>"
    }
}
```

### Demo

We have a working example that you can checkout in our GitHub repo.

{% hint style="info" %}
View the demo here, <https://github.com/usestencil/form-custom-image-picker-example>
{% endhint %}


# Webhook Integration


# Introduction

## What are Webhooks?

Webhooks are a lightweight, developer-friendly way to receive real-time notifications when specific events occur in UseStencil. Instead of constantly polling an API to check for updates, webhooks push event data to your specified URL the moment something happens — in this case, when a new image is created.

They enable seamless communication between UseStencil and your external systems, so you can automate workflows, trigger downstream processes, or log activity with minimal effort.

If you are interested in reading and learn more about Webhook, consider visiting this excellent [article](https://zapier.com/blog/what-are-webhooks/?utm_source=google\&utm_medium=cpc\&utm_campaign=gaw-row-nua-evr-search_nb_desktop_blog_prospecting_developing1_developing2-ads\&utm_term=\&utm_content=1010233\&utm_ads_campaign_id=19622168382\&utm_ads_adset_id=156311806284\&utm_ads_ad_id=748161684143\&gad_source=1\&gad_campaignid=19622168382\&gclid=CjwKCAjw9uPCBhATEiwABHN9K4ou6ZHgg9NiB5x1k3EAG1IaxLDN9lKblWhWijuOa_5a_4oNx4FGahoCB4EQAvD_BwE) on Webhook from Zapier. Come back here when you're done!

## How Webhooks Work in UseStencil

When your workspace generates a new image — whether through automated actions, user requests, or scheduled jobs — UseStencil triggers a `image.created` event. A JSON-formatted payload is immediately sent to the URL you’ve configured in your Webhook settings.

This allows your application or third-party service to respond in real-time. Whether you’re sending notifications, updating a database, or syncing with another app, webhooks help bridge that gap efficiently.

**Basic Workflow**

```
Image Created in UseStencil → Webhook Triggered → Payload Delivered to Your Endpoint.
```

> You may have multiple webhooks being set up for one template

## Key Benefits

<table><thead><tr><th width="209.98046875">Feature</th><th>Description</th></tr></thead><tbody><tr><td>⚡ Instant Notifications</td><td>Get real-time updates the moment an image is generated</td></tr><tr><td>🔗 Seamless Integration</td><td>Connect with external tools (Slack, storage APIs, automation platforms)</td></tr><tr><td>📦 Simple Payloads</td><td>Easy-to-parse JSON data ready for immediate use</td></tr><tr><td>🔐 Secure Delivery</td><td>Optional secret token to verify the source of incoming requests</td></tr></tbody></table>

## Real-World Use Cases

Even with a single event type, `image.created` unlocks powerful workflows:

* **Internal Automation**: Automatically upload new images to your image CDN or S3 bucket.
* **Notifications**: Post a generated image to a Slack channel or Discord server with metadata.
* **Processing Pipelines**: Trigger additional tasks like watermarking, resizing, or captioning via serverless functions.
* **Analytics**: Track how often and when images are being generated for reporting or audit purposes.

## Who Should Use This?

This feature is ideal for:

* **Developers** who want to trigger custom actions on image creation
* **Ops & Automation Engineers** needing system-wide image notifications
* **Teams using third-party tools** like Zapier, n8n, or Make to handle image delivery, archival, or routing
* **You**! no, seriously

> ✅ Next Up: [Getting Started](/integrations/webhook-integration/getting-started) with Webhooks
>
> We’ll walk you through how to enable webhooks, configure your endpoint, test it, and start receiving image.created events.


# Getting Started

This section walks you through everything you need to begin using webhooks in UseStencil, from setting up your first webhook endpoint to verifying and testing the `image.created` event.

## Prerequisites

Before configuring your first webhook, make sure you have the following:

* An active UseStencil [paid subscription](https://www.usestencil.com/pricing) with access to the project or workspace where image creation events occur.
* A publicly accessible HTTPS endpoint that can receive POST requests.
* (Optional) A tool like [webhook.site](https://webhook.site) or [RequestBin](https://requestbin.com/) if you're testing without a live backend.
* (Optional) Your own authentication mechanism (e.g., API key, secret token) to embed into the webhook payload or headers


# Setup Guide

Follow these steps to configure a webhook for your workspace:

1. Log in to [UseStencil](https://app.usestencil.com/)
2. Navigate to your Template that you wish to have integration with. This is located under the "Project" tab.
3. Click on the triple-dot menu to bring up the menu options for the template. Choose "Webhooks".\
   ![](/files/dgbju3t1EmEdCq1dFqHL)
4. Click "Add Webhook".
5. Fill in the following fields:

<table><thead><tr><th width="187.3828125">Fields</th><th>Description</th></tr></thead><tbody><tr><td>Webhook name</td><td>A friendly name for your reference.</td></tr><tr><td>Webhook URL</td><td>The HTTPS endpoint where UseStencil should send the payload.</td></tr><tr><td>Custom headers (Optional)</td><td><p>Key-value pairs to be included as HTTP headers in the request. Useful for:</p><ul><li>Authentication tokens (e.g., <code>Authorization: Bearer YOUR_TOKEN</code>)</li><li>Source-identification headers (e.g., <code>X-Origin: stencil</code>)</li></ul></td></tr><tr><td>Custom Body (Optional)</td><td><p>Define a custom JSON payload structure under the <code>user_defined</code> field.</p><p></p><p>You can:</p><ul><li>Include static values (e.g., "source": "usestencil")</li><li>Inject dynamic values using handlebars-style templating like:<br><br><code>{</code><br><code>"image_id": "{{data.id}}",</code><br><code>"url": "{{data.url}}",</code><br><code>"created_at": "{{timestamp}}"</code><br><code>}</code></li></ul></td></tr></tbody></table>

Click **Save** and that's it! Isn't that easy?

## Using Custom Headers for Security

Custom headers are a simple yet powerful way to pass authentication tokens or shared secrets to your server. You can configure:

* `Authorization: Bearer abc123`
* `X-Signature: custom-value`
* `X-App-Key: my-service-key`

These headers will be included in every webhook request and can be validated on your backend.

## Managing Webhooks

From the Webhooks dashboard, you can:

* **Pause** or **edit** existing webhooks
* **Delete** unused ones
* View recent **delivery logs** to inspect response codes, headers, and payloads.

> #### ✅ Next Steps
>
> Now that you’ve set up and tested your webhook with custom headers and payloads, you’re ready to explore the [Payload Structure ](/integrations/webhook-integration/payload-structure)in detail


# Payload Structure

The UseStencil Webhook system delivers a fixed JSON payload whenever an image.created event is triggered. The payload contains standardized information about the image, the modifications applied, and any additional context you optionally provide using the user\_defined field.

Example payload

{% code lineNumbers="true" %}

```json
{
  "image_url": "https://usestencil.s3.amazonaws.com/dev/images/a3b37b73-6876-44b7-bfd9-dfc3b0ad4157/19da5632-feef-4abd-a28a-e3dfa6077429.png",
  "image_url_jpg": "https://usestencil.s3.amazonaws.com/dev/images/a3b37b73-6876-44b7-bfd9-dfc3b0ad4157/19da5632-feef-4abd-a28a-e3dfa6077429.jpeg",
  "metadata": {},
  "modifications": [
    {
      "name": "image_1",
      "src": "https://usestencil.s3.amazonaws.com/dev/uploads/.../w100_product_highlight_v1.webp"
    },
    {
      "name": "model_name",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    },
    {
      "name": "text_3",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    },
    {
      "name": "qrcode_4",
      "value": "https://usestencil.com"
    },
    {
      "name": "barcode_5",
      "value": "1234567890128"
    }
  ],
  "status": "completed",
  "user_defined": {
    "auth": "test"
  }
}
```

{% endcode %}

## Payload Fields Explained

<table><thead><tr><th width="181.98828125">Field</th><th width="90.19921875">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>image_url</code></td><td>string</td><td>URL to the final PNG image</td></tr><tr><td><code>image_url_jpg</code></td><td>string</td><td>URL to the JPEG version of the image</td></tr><tr><td><code>metadata</code></td><td>object</td><td>Metadata from the rendering job (can be empty or enriched in future)</td></tr><tr><td><code>modifications</code></td><td>array</td><td>List of template elements and their applied values (text, image, QR, etc.)</td></tr><tr><td><code>status</code></td><td>string</td><td>Always completed for now (indicating image generation is done)</td></tr><tr><td><code>user_defined</code></td><td>object</td><td>Optional. A dictionary of custom values added by the user when configuring the webhook</td></tr></tbody></table>

### User Defined information

The user\_defined object allows you to attach custom key-value pairs to every webhook request. You can use this to send:

* A shared secret for verification
* Identifiers like client\_id or source
* Any static data your receiving service may need

**Example configuration during webhook setup:**

```
{
"auth": "stencil-secret-xyz",
"environment": "staging",
"source": "usestencil"
}
```

**Resulting payload:**

```
"user_defined": {
"auth": "stencil-secret-xyz",
"environment": "staging",
"source": "usestencil"
}
```

## Headers

Webhook requests include standard headers, and optionally custom headers you define:

> ✅ Next Up: [Security & Verification](/integrations/webhook-integration/security-and-verification)
>
> Learn how to use custom headers and body to protect your webhook endpoints.


# Security and Verification

Webhook endpoints often deal with sensitive or privileged data. To help protect your systems and ensure authenticity, UseStencil provides multiple options to secure webhook deliveries:

1. ✅ Custom Headers
2. ✅ User-Defined Fields

## Customer Header (Simple Auth)

When configuring a webhook, you can include custom headers such as an API key or token for simple verification.

**Example configuration**

```
{
"Authorization": "Bearer abc123",
"X-Origin": "usestencil"
}
```

**Resulting HTTP request:**

```
POST /webhooks/receive HTTP/1.1
Content-Type: application/json
Authorization: Bearer abc123
X-Origin: usestencil
```

**How to Use:**

* Your backend should validate the presence and correctness of the token or header.
* Best for quick or internal-only setups (e.g. test environments).

## `user_defined` data (Optional Field-based auth)

You can supply key-value pairs under user-defined during webhook setup. These will appear inside the payload body — useful if your verification logic depends on data inside the request.

**Example Configuration:**

```
{
"auth_token": "stencil_secret_xyz"
}
```

**Payload Example:**

<pre><code><strong>"user_defined": {
</strong>  "auth_token": "stencil_secret_xyz"
}
</code></pre>

**How to Use:**

* Validate `user_defined.auth_token` on the receiving server.
* Useful for services that require in-body verification (e.g., Lambda triggers or low-code tools).

## Additional Best Practices

* ✅ Always use HTTPS for your receiving endpoint
* ✅ Rotate your webhook secrets periodically
* ✅ Log and monitor webhook activity and failures
* ✅ Reject requests missing the X-UseStencil-Signature header (if secret is defined)


# Error Handling & Retries

Webhooks, by nature, depend on reliable delivery. UseStencil handles failures gracefully and provides an automatic retry mechanism to help ensure webhook events are not lost due to temporary issues on the receiving server.

This section explains:

* What counts as a delivery failure
* How and when retries are triggered
* Best practices for ensuring reliability

## What Is Considered a Failure?

UseStencil considers a webhook delivery failed if:

* The server responds with a non-2xx HTTP status code (e.g., 400, 500, 403)
* The server times out (takes longer than 5 seconds to respond)
* The server drops the connection (e.g., TLS handshake failure, DNS error)

## Retry Behavior

If a webhook delivery fails, UseStencil will automatically retry the request using the following backoff strategy:

<table><thead><tr><th width="116.953125">Retry #</th><th>Delay (approx.)</th></tr></thead><tbody><tr><td>1</td><td>1 seconds</td></tr><tr><td>2</td><td>~4 seconds</td></tr><tr><td>3</td><td>~9 seconds</td></tr><tr><td>10</td><td>~1.7 minutes</td></tr><tr><td>20</td><td>~6.7 minutes</td></tr></tbody></table>

## Logging

Webhook integrations can fail due to temporary server issues, invalid URLs, or network hiccups. UseStencil ensures delivery robustness with a retry strategy and full visibility into delivery logs via your Webhook dashboard.

Every webhook configured in UseStencil includes a Delivery Log Viewer, which shows:

* A timestamped list of deliveries
* Thumbnail preview of the rendered image
* Success status (green tick for success, red icon for failures)
* A “View Details” option for payload and response inspection

Access the list of previous deliveries via this screen.

<figure><img src="/files/etgBm6Fosuzoxqrl7hGU" alt=""><figcaption></figcaption></figure>

**Example delivery log**

<figure><img src="/files/AQ6vg0IV016V8FO2LkHR" alt=""><figcaption></figcaption></figure>

1. Go to your Webhook configuration page
2. Click the ⋮ menu next to your webhook URL
3. Select "View deliveries"
4. You'll see a list of all past attempts, with:
   * Event preview (e.g., generated image)
   * Exact date and time
   * Status icon
   * "View Details" button

## Test Webhook Feature

To quickly validate your endpoint without triggering a real event, use the Send Test Webhook button on the delivery log screen.

UseStencil will immediately fire a test payload to your configured URL, following the same structure and headers as a real webhook.

This is useful for:

* Verifying endpoint availability
* Testing authentication
* Debugging custom handling logic

## Best Practices

| ✅ Respond with 200 OK immediately                      | Avoids timeout and duplicate retries                   |
| ------------------------------------------------------ | ------------------------------------------------------ |
| ✅ Make handler idempotent                              | Retries may cause duplicates                           |
| ✅ Use the user\_defined field for traceability         | Helps identify environments, orgs, or source triggers  |
| ✅ Log incoming requests                                | Enables postmortem debugging                           |
| ✅ Use the “Send Test Webhook” feature before deploying | Ensure payload + auth + headers are handled correctly. |


# Use Cases & Integration Ideas

UseStencil webhooks allow you to **automate downstream workflows** the moment a new image is generated. Whether you're using third-party tools or building internal systems, webhooks help eliminate manual steps, trigger following actions, and keep systems in sync.

## Auto-upload to Cloud Storage

**Use Case**: Automatically push the generated image to another cloud storage bucket (e.g. S3, GCS, Azure Blob).

**How**:

* Use an AWS Lambda or a GCP Cloud Function to receive the webhook.
* Fetch the image\_url from the payload and upload it to your destination bucket.
* Use user\_defined to route by environment or team.

## Send Notifications via Slack, Discord or Email

**Use Case:** Notify your team or clients when a new asset is ready.

**How:**

* Use Zapier, Make (Integromat), or n8n to receive the webhook.
* Format a message including image\_url and modifications.
* Post to a Slack or Discord channel, or email a recipient list.

## Sync Image to Product CMS or Storefront

**Use Case:** When an image is created for a product campaign, push it directly to your CMS (e.g. Contentful, Sanity) or eCommerce system (e.g. Shopify, WooCommerce).

**How:**

* Use the `project_id` or a `user_defined.product_id` to identify the target.
* Update or create a record with the image URLs.
* Optionally trigger cache invalidation or a publish workflow.

## Enrich Metadata or Log to Internal Analytics

**Use Case:** Track when and how assets are used, by whom, or for what campaign.

**How:**

* Log the payload into your internal analytics or data warehouse (e.g. via Kafka, Segment, or BigQuery).
* Parse modifications to understand template usage patterns.
* Store `user_defined` keys for later audit or reporting.

## Trigger AI Post-processing or Optimisation

**Use Case:** Automatically feed new images into a compression engine, thumbnail generator, or an AI pipeline (e.g., background removal, social resizer, object detection).

**How:**

* Receive the webhook, and download the image via image\_url.
* Pass it to a processing tool like TinyPNG API, Remove.bg, or your model.
* Store processed images separately or update the same resource.

## Automatically Generate PDFs or Marketing Collateral

**Use Case:** Combine the image into a PDF, sales sheet, or multi-page collateral automatically.

**How:**

* Use the image\_url as an asset source.
* Feed it into a PDF generation tool (e.g. Puppeteer, PDFKit, or CloudConvert).
* Save and distribute the document or attach it to a CRM record.

## Chain Other Internal Workflows

**Use Case:** Kick off internal approval, ticket creation, or downstream service calls.

**How:**

* Trigger a Jira or Trello card via webhook.
* Open a support ticket via Intercom or Zendesk APIs.
* Trigger another system's API with enriched image metadata.

> #### &#x20;Tip: Leverage `user_defined` for Routing Logic
>
> Add static or dynamic fields to the webhook config so your receiver knows:
>
> * Which app/team/org triggered the event
> * What action should follow ("action": "send-to-salesforce")
> * What template or purpose it was for ("label": "product-campaign", "env": "staging")


# Case Studies


# Generate Instagram Post from WooCommerce

How to generate Instagram post from your products in WooCommerce

Your online store using WooCommerce is doing very well but you still think you could do better. You had built a good number of followers in social media like Instagram and you wonder if you could capitalize on them. Maybe you should cross sell your product in Instagram and drive more traffics from there to your online store.

Then it hits you, it's a lot of work to post *quality* content on Instagram.

If this sounds like a problem you're having, then read on. We will show you how to turn your product listing in WooCommerce store to Instagram posts - complete with description of product, price and everything else you need.&#x20;

The best part is, you only need to set this up once and it runs autonomously after.&#x20;

<div align="center"><img src="/files/-MhMng7CKVyK9cLentVv" alt="Turn your WooCommerce product listing into..."></div>

![attractive Instagram posts to drive more traffic to your online store.](/files/-MhMo-M5bw76NWg5grly)

## Guide

### 1. Create an image template

You can create your own custom template complete with your branding and everything, or you could also choose one from our list of presets. In this tutorial we are going to use our custom template.

![Our custom template that matches our surfing apparel items we are selling](/files/-MhMpQ2sFwQ9x0dU3372)

For this template, we will have four layers that can be overridden - product, description, price and product image.

### 2. Create your product in WooCommerce

To ensure that our template has the correct data to fill in, you need to have appropriate information about your listing. In this case, we need to add product price, short product description and have at least one product image.

View the short clip below to see how we set this up.

{% embed url="<https://youtu.be/1ohhIXSHyEI>" %}

### 3. Connect your WooCommerce store to Integromat

Before you can integrate your store with Integromat, you need to create WooCommerce API Key.&#x20;

Simply go to your WooCommerce Settings > Advanced, and click on the REST API. You should see an option to create an API Key.

Take note on these values. If you're unclear on how to do this, please see the below clip.

{% embed url="<https://youtu.be/Bv8MId3J5Cg>" %}

{% hint style="warning" %}
Your API Key must remains a secret. Anyone can access your store data given the API Key.
{% endhint %}

### 4. Automating the workflow

Once you have set up Integromat connection to your WooCommerce, you can now start building automation workflow to tie everything together.

In general, here are the steps we need to do.

1. Watch for new products being listed in WooCommerce store
2. Generate image variations with Stencil using the information we get from above
3. Post those images to Instagram

Watch the video for full walk-through on how each of these steps are set up. They are very easy to follow.

{% embed url="<https://youtu.be/mjkRnYe1Po4>" %}

Once this is set up properly, whenever a new product is listed it will automatically generate an Instagram post with a caption to the product page. How cool is that?

Now you've got yourself a fully automated marketing bot 🦾

## Final result

![Final result](/files/-MhMvIwBkKR8RahgzrRS)

## More?

Since WooCommerce is built on top of WordPress, you can create similar image variations as preview image. This is called open graph image.

So when you share your product page link on Facebook, Twitter, etc they will see a link with image as preview instead of just text. Creating an appealing image can then help in driving more traffic to your site.

![You can customize this image preview](/files/-MhMx0OD2VXXJ5TPHKUF)

If you're interested to know more about this, read our tutorial on [generating open graph image for WordPress](/integrations/case-studies/generate-open-graph-image-for-wordpress).


# Generate Open Graph Image for WordPress

How to create preview image for Wordpress

WordPress is a powerful and easy to use CMS. Due to its popularity, WordPress is stated to power [65.2%](https://w3techs.com/technologies/details/cm-wordpress) of CMS out there. Creating content has never been more accessible than ever and trying to make your site stands out is becoming a much harder task.

Open graph can help with that. When open graph is set up on your site, an image preview is shown when your link is shared. Having attractive image can help in increasing click rate to your site.

![No open graph setup, no image preview is shown](/files/-MfIRJUBLHHpZ7ni7PD1)

![With open graph setup, a well designed preview image can increase click rate](/files/-MfIRVIvVotgbOwS8N3V)

## Template

We are going to use custom designed template for our open graph image. You can also use preset templates that we have in our gallery and adjust it to your need.

![](/files/-MhJsIb8WCvs0fwR1Vx_)

It only consists of two texts that we are interested in overriding soon - `title` and `author`.

## Guides

### 1. Install Plugin

We are going to use Head, Footer and Post Injections plugin to inject meta field to your template. If you're using any plugins, please make sure that PHP code can be executed. Certain plugins disallow PHP code execution.

![](/files/-MhJneoUv_rEFL4qwpzP)

{% hint style="info" %}
You can definitely do this without using any plugin by modifying your template directly. This plugin helps do this for you so you don't have to redo the whole process when you changed to a new template.
{% endhint %}

### 2. Create Query String URL

Go to your template's Query String integration page. Then select the object and field that you want to override.&#x20;

For this tutorial, we are going to override the title's text and the author's text fields.

![Take note of the base URL](/files/-MhJlcsH0kWhCj-FX58j)

{% hint style="info" %}
If you are only using this template's integration for a specific domain, we recommend updating the **Allowed Origins** setting to your domain.

This prevents unauthorized use of your query string. Learn more about[ Allowed Origins here](https://docs.usestencil.com/integrations/query-string-integration/basic#allowed-origins).
{% endhint %}

### 3. Inject open graph meta tag

Copy the query string URL from the previous step and update the value for the query string into something more appropriate to your use case.

In our case, the template we are using requires the title of the post and the author.

```php
<?php $author_id=$post->post_author; ?>
<meta name="og:image" content="https://images.usestencil.com/qs/34aacb1e-a7e4-4b4d-bb64-2bffc3c930a5/bWFWRh9vdxbvA3rzSrjx4d.png?title=<?php echo urlencode(get_the_title()); ?>&author=<?php echo urlencode(get_the_author_meta('display_name', $author_id)); ?>" />
<meta name="og:image:height" content="630" />
<meta name="og:image:width" content="1200" />
```

{% hint style="success" %}
`<?php echo urlencode(get_the_date()); ?>` to get the published date.

You can view WordPress documentation for more details.
{% endhint %}

Paste the above code (after your modification) to the Head and footer section. You can go to this section by clicking on the "Settings" menu on the left and select "Header and footer".

![](/files/-MhJoQnwFznnvl3Qfqm5)

Save your changes and you're done.

## Testing your open graph image

### Facebook Sharing Debugger

Facebook has sharing debugger that validate your open graph image setup. Simply paste the link to the post that you want to check.

Here's the link to facebok sharing debugger, <https://developers.facebook.com/tools/debug/>

![Here's how it will look like when you share your link. Big preview image.](/files/-MhJq9Ymv5pcByKtVUlo)

### Stencil Image Requests&#x20;

You can also view the request in Image Requests page. Please note that only the initial request is shown here when the image is first generated. Subsequent requests are always served from our cache. This is by design, so your quota is not deducted on each request.

![You can see the modifications being sent to generate the image](/files/-MhJpFdY7MWVb3euyITe)

## When will my image ready?

Image will be generated on-the-fly when the page is *first* accessed - either by actual human or search engine crawler. You don't have to do anything once everything is setup, now go and enjoy your free time.

![Sample generated image](/files/-MhJtiu7zyhUJ1_BOUka)


# Generate Personalized SendGrid Email Campaigns

Use query string URL to create a personalized email header in SendGrid

Personalized email feels more personal and engaging to users. It builds trust between you and the user.&#x20;

In this guide we will utilize query string URL to generate image with our intended recipient's name in the email header image. The link can then be embedded in the email.

## Design

Our design will look like this. You can find this template in our list of preset templates.

![Hana is our intended recipient which we will customize](/files/-Mgpmdo1U4UGP6IsH6Ul)

## Guides

Query string URL is the easiest integrations to use and it should take less than 5 minutes to everything up.

### 1. Setting up our query string

For this template, we are only interested in customizing the recipient name.

![](/files/-Mgpnq_124mfXc1_5wbU)

We create a query string called `name` that maps to our template's object `user` (field `text`).&#x20;

We also set the allowed origins to `*.` This allows the image to be requested from any origins. For more information about query string, please read the [documentation here](/integrations/query-string-integration).&#x20;

### 2. Embedding the URL in SendGrip template

You can either create a new design or edit your existing design. In this example, we are going to create a new simple design.

![Creating a custom design](/files/-MgpsLn7I2L77ZtEgPnU)

Once your template is loaded, then drag the Image module to the top of your template. Leave the image empty for now because we are going to generate the image dynamically with our query string URL.

Add any other additional modules that fits your requirement.

![](/files/-MgpsrY8HBpy5ia29wAG)

Edit the image module's HTML to include our query string URL. Please ensure that the SendGrid template variable is corrrect.

```
<img src="https://images.usestencil.com/qs/34aacb1e-a7e4-4b4d-bb64-2bffc3c930a4/j3D8rXYRGfGhP5CntrkymX.png?name={{First_Name}}">
```

You're set!

### 3. Sending a test email

Send a test email to see this in action.&#x20;

![Our email viewed inside Gmail](/files/-MgpteeQhe5VXGPTPaBc)

The variable name has been changed to our user's first name.

{% hint style="success" %}
Use JPEG to load the image faster due to its smaller size. Stencil also caches the generated image, so any subsequent requests will be faster.
{% endhint %}

It's really easy and the amount of time saved is simply amazing.&#x20;

You can build similar workflow in any other email providers like Mailchimp, Mailgun etc.


# Sending Charts to Twitter

Tutorial on how to use Zapier to integrate with Stencil

{% hint style="success" %}
Stencil's Zapier app is coming soon. For now, we can use Stencil's webhook to do the integration.
{% endhint %}

## Automating Charts Generation Workflow

In this demo we will demonstrate a real use case of workflow automation using Zapier, AWS Lambda and Twitter.&#x20;

The workflow goal is to pull up data from external source, populate our template with the data, generate the image and post it to Twitter.

Despite having no Zapier app (yet, WIP), integration with Zapier is still possible. In fact, this illustrates how integration with other services can be achieved just by utilizing webhook functionality.

## Requirements

If you're following along, you will need&#x20;

1. Premium Zapier account
2. AWS Account
3. Twitter account
4. and of course Stencil

{% hint style="info" %}
Premium Zapier account is required due to the Zaps that we are going to use
{% endhint %}

## Overview

Here's how everything connects to each other.

1. We write Lambda function to pull the data we need and build modifications to send to Stencil.
2. We use Zapier to schedule a Zap that calls AWS Lambda function once a day.
3. Our lambda function then fetches the data, transforms it into modifications that can be applied to the template and then they are sent to Stencil to generate image.
4. We also specify Zapier webhook URL that Stencil will call once the image is generated.
5. The webhook is linked to Zapier Webhook Zap that in turns run another zap to parse the payload and tweet to Twitter.

## Guides

### 1. Designing our template

![The final image that will be tweeted](/files/-MgZZPBywTE-7gMg-GTV)

Our template consists of two bar graphs, three texts, one image and one rectangle that acts as a background.&#x20;

We will leave the designing part as an exercise to the reader 😄

For both bar graphs, you can use the following JSON to create a default chart that we will override from the API.

```javascript
{
  "labels": [
    "Johor",
    "Kedah",
    "Kelantan",
    "Melaka",
    "Negeri Sembilan",
    "Pahang",
    "Perak",
    "Perlis",
    "Pulau Pinang",
    "Sabah",
    "Sarawak",
    "Selangor",
    "Terengganu",
    "W.P. Kuala Lumpur",
    "W.P. Labuan",
    "W.P. Putrajaya"
  ],
  "datasets": [
    {
      "label": "Dose #1",
      "data": [
        100,
        5,
        15,
        100,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "backgroundColor": "#ffcc00"
    },
    {
      "label": "Dose #2",
      "data": [
        10,
        100,
        150,
        200,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "backgroundColor": "rgba(187, 153, 224, 1)"
    }
  ]
}
```

Paste the above JSON into your Dataset Editor (switch to JSON View).

{% hint style="info" %}
If you need help with Charts, please see [Chart's documentation](/using-stencil/template-editor-1/charts)
{% endhint %}

### 2. Preparing the data with AWS Lambda

We are going to use Malaysia's National Covid-​19 Immunisation Programme Open Data that can be found here, <https://github.com/CITF-Malaysia/citf-public>.&#x20;

We are mostly interested in vaccination count by state that is referenced here, <https://github.com/CITF-Malaysia/citf-public/blob/main/vaccination/vax_state.csv>.

{% tabs %}
{% tab title="Lambda Function" %}

```javascript
from datetime import datetime, timedelta
import pytz
import pandas as pd
import requests as req
import json

def make_modification(name, states, dose1, dose2):
    datasets = [ { "data": dose1, "label": "Dose #1" }, { "data": dose2, "label": "Dose #2" } ]

    modification = {
        "name": name,
        "labels": states,
        "datasets": datasets
    }

    return modification

def run():
    url = "https://raw.githubusercontent.com/CITF-Malaysia/citf-public/main/vaccination/vax_state.csv"
    
    # Get yesterday's date because data is only available up to yesterday
    kl_timezone = pytz.timezone('Asia/Kuala_Lumpur')
    kl_time_now = datetime.now(kl_timezone)
    kl_time_yesterday = kl_time_now - timedelta(days=1)
    yesterday = kl_time_yesterday.strftime("%Y-%m-%d")

    # read the CSV from the URL
    df = pd.read_csv(url)
    
    # filter the data only for the date we are interested in
    rows = df.loc[df["date"] == yesterday]

    if not rows.empty:
        # Only get the data that we want to trend
        states = rows["state"].tolist()
        daily_dose1 = rows["dose1_daily"].tolist()
        daily_dose2 = rows["dose2_daily"].tolist()

        cumul_dose1 = rows["dose1_cumul"].tolist()
        cumul_dose2 = rows["dose2_cumul"].tolist()

        # Generate modificatiosn that fit Stencil's API format
        daily_graph = make_modification("bar_graph_daily", states, daily_dose1, daily_dose2)
        cumul_graph = make_modification("bar_graph_cumul", states, cumul_dose1, cumul_dose2)

        text_date = {
            "name": "text_date",
            "text": yesterday
        }
        modifications = [text_date, daily_graph, cumul_graph]
        
        # Set our Zapier webhook to call once image is generatd
        payload = {
            "template": "3bc487ef-33ad-46b3-a18c-0d57c3825723",
            "modifications": modifications,
            "webhook_url": "your-zapier-webhook"
        }

        headers = {
            "Authorization": "Bearer your-project-api-key",
            "Content-Type": "application/json"
        }

        # Call Stencil's API to generate the image asynchronously
        req.post("https://api.usestencil.com/v1/images", json=payload, headers=headers)


def lambda_handler(event, context):
    run()
    
    return {
        'statusCode': 200,
        'body': json.dumps('Request sent')
    }
```

{% endtab %}
{% endtabs %}

Although the code may look daunting for some users, it is actually pretty straight forward. You could also use any other no-code or low-code tool out there to do this, but in this case a simple Python script should handle the job just fine.

We can now deploy our lambda function and set HTTP trigger for the lambda function. We will need this trigger URL in the next step.

![](/files/-MgZ_0p_z-T9GvRt7yFN)

### 3. Schedule Zap to run lambda function

The rest of the steps are straight forward and mostly filling in the details with proper information.

We are going to trigger the lambda function everyday at 7AM including weekends.

![](/files/-MgZ_EzN1i8N9e0TtwLl)

![We provide the Lambda function trigger URL from previous step](/files/-MgZ_Gjn1J_RtzrO_HRc)

### 4. Create Zap to listen for webhook callback and send a tweet

Our lambda function will call Stencil's API to generate the image. We also setup our payload to include Zapier webhook below. Once image is generated, it will call this webhoook.

![This is the Zapier's Webhook that we set in our payload in step 2 (Lambda function)](/files/-MgZ_Vg2ypIlLLkaht0k)

We add another step to parse our JSON payload and assign our output variable to include the link to the image and the date of the data.

{% hint style="info" %}
Zapier's output variable must be a dictionary
{% endhint %}

![We parse the response we get from Stencil's API to send it to the next step (Twitter)](/files/-MgZ_dgLx1HiocEjzCJE)

We parse the response we get from Stencil's API to send it to the next step (Twitter)

![We add date to our message so the message is always unique.](/files/-MgZ_iQQ1xHLNSsCPdU1)

You're done! Here is sample of the tweet.

![](/files/-MgZ_rrKunU_rxHbdMK1)

Now do something productive with the extra time you've gained. This workflow is fully automated.

## Debugging issues

Sometimes, problems can arise and you need to know where to look. You can view image requests by either clicking on Requests tab or by accessing your template's Console page.

You should be able to see your generated image in the list of requests. If you don't, that means Stencil does not receive your request. Please check your Zap or AWS Lambda function.

You can also see the webhook response in the log.

![](/files/-MgZa-f1z7p2-r7E9kIQ)

{% hint style="warning" %}
Twitter Zap can be disabled automatically by Zapier if they think you're sending a duplicate tweet. It is against Twitter's ToC.
{% endhint %}

Now try do the same workflow but with Integromat.


# Generate Instagram Post from Shopify

Drive traffics to your Shopify store by generating creative images from your products

## Motivation

Part of digital marketing strategies is to convert traffics from your social media like Instagram to actual sales in e-commerce store like Shopify.

However, the process of creating product images for Instagram post can be tedious work and often time small businesses do not have enough resources to do this.

Stencil provides the service to generate those images without much effort and using no-code tools like Integromat, this process can be fully automated. Design once, post many times.

## Guides

### Design your template

For our template design, we will need few fields that we will replace using our API.

![](/files/-MfOFBjxekGcO3vhrDVM)

The image uses image frame component so we it can resize the image automatically to fit into the dimension we have set. The title and and the price will also be replaced with values coming in from our API. Same goes with the rating.&#x20;

### Automating the workflow with Integromat

{% hint style="warning" %}
You need Instagram for Business/Professional account in order to continue with this integration. This limitation is imposed by Instagram.
{% endhint %}

Here's the overview on the approach we will take to automate this.

1. Use Shopify module to get products we are interested in.
2. Generate those images with Stencil module
3. Get the generated images.
4. Post those images to our Instagram account.

We also provide a video walk through on the automation process.

{% embed url="<https://youtu.be/SSPHf6IguUQ>" %}

In the video walk through we use a delay of 10 seconds. That is mostly to simplify the scenario. For production setup, you should use [Instant Trigger](/integrations/integromat-integration/instant-trigger) to watch for generated image and to post it to Instagram as soon as the image is generated.

## Result

![](/files/-MfOHaN_bfGnnxuuFnDQ)


# Automating Webflow Open Graph Image

Generate open graph images for Webflow automatically

## What is Open Graph?

[Open Graph](https://ogp.me/) is an internet protocol that was originally created by [Facebook](http://fbdevwiki.com/wiki/Open_Graph_protocol) to standardize the use of metadata within a webpage to represent the content of a page.

## Motivation

Images are powerful marketing tools, that's the reason social media is such a powerful tool for marketers. Images can speak louder than words sometimes.&#x20;

When sharing blog or website link, it would be nice if there's a way to display some sort of image preview to attract clicks. Here's where Open Graph Image helps.

![Sharing link without Open Graph](/files/-MfIRJUBLHHpZ7ni7PD1)

![Sharing link with Open Graph](/files/-MfIRVIvVotgbOwS8N3V)

Clearly link with attractive preview image could attract more clicks and engagements. In this guide, we will walk through how we can generate these images if you're using Webflow.

## Guides

{% hint style="success" %}
All videos attached have no sound.&#x20;

*Listen to silent, it has so much to say - Rumi*
{% endhint %}

### Designing our template

For the purpose of this demo, we are going to create a template with the size of 1306x833.

![Our simple design](/files/-MfHL9RaFixCG18PAlq_)

The main title (with the yellow background) is our `text_main` in our template and the subtitle is `text_sub`.

The rest of the components won't be changed in the generated image. Of course, they are customizable but we want to keep the demo as concise as possible. You should change the author's name and image for production work.

If you like to see the whole process, please see the video below. As you can see, it takes a very short time to create a template like this.

{% embed url="<https://youtu.be/-sTlsN0vGAw>" %}

### Setting up Webflow

Our goal is to create an open graph image for every blog post. In order to do that, we need to tell Webflow to use the image that we will attach later as the open graph image source.

#### 1. Create an image field

We can either use existing image field or we can create a new field specifically for open graph image. In this case, we choose the latter option.

{% embed url="<https://youtu.be/b5DFYBi5Miw>" %}

#### 2. Set our newly created field as the open graph image source

Go to your CMS Collection Pages settings page and find Open Graph section. Select our newly created field (if you created a new field) and also fill in the rest of the details.

{% embed url="<https://youtu.be/zqTYBzn7-b8>" %}

### Automating the workflow with Integromat

Now comes the fun part where we ties everything together.

#### Workflow

1. List all items in Webflow's collection
2. For each of the item, we grab the title of the blog post and its summary and we send it to Stencil API to generate an image.
3. Here we use asynchronous image API and wait 10 seconds for the image to finish generating. Ideally we should use [Instant Trigger](/integrations/integromat-integration/instant-trigger) so Integromat can immediately run your scenario. However, for this demo purpose, this is sufficient.
4. We retrieve the image by calling Stencil Get Image module.
5. Using this image, we update each of the blog post's OG Field with the link to the generated image.

You can watch the following video to see how seamless the integration between Stencil, Integromat and Webflow.

{% embed url="<https://youtu.be/OdeUGYBiNnE>" %}

{% hint style="info" %}
**How about Zapier?**

Zapier's Webflow Zap is quite limited at the moment in term of its functionality and thus not able to do this integration properly. Please reach out to Webflow to ask them to improve their Zapier integration.
{% endhint %}

## Result

You can see an example of the images generated.

![](/files/-MfINxyi6FIFTVc6Jwcd)

![](/files/-MfIO-dLR3vQWu0akCyi)


# Generate certificate of accomplishment

## Case study

Often time, course participants will receive their certificate of accomplishment digitally and they can link the image of the certificate in their social profile network like LinkedIn.

If you are running these courses and would like to provide such certificate there are few things that you need to handle.&#x20;

First, generating the certificate image itself. Then, most importantly ensuring certificate authenticity. Since the image can be doctored digitally, you need to employ your own system to handle certificate validation.

Stencil's secured signed image can solve these issues and you'll see how we can this is handled. Of course, in some complicated cases you would still need to roll your own system but for the majority of the time, this is sufficient.

## Guides

### Design your certificate template

![AWS-like certificate](/files/-MfRNubCEMsR7Mu2RsjQ)

For this design, candidate's name can be customized later using our API.

### Creating Signed URL for Image

#### Getting the necessary information

You'll need some information that can be retrieved from *Console > Integrations > Secure Signed Image*

![Information required in the next step](/files/-MfRPzyD0OX9pucs1PgQ)

#### Generate the URL

The section [Basic](/integrations/secure-signed-image/signed-image) explains how the URL is generated. We can use the Python code provided and modifying it based on the information we get. We also need to specify the proper modifications that will be sent to our API.

For example this is the Python code we use with the certificate template that was created from previous step.

{% tabs %}
{% tab title="Python" %}

```python
import json
import hmac
import hashlib
import base64

def base64_encode(string):
    """
    Removes any `=` used as padding from the encoded string.
    """
    encoded = base64.b64encode(string.encode())
    encoded = encoded.rstrip(b'=')
    return encoded

def generate_url():
  user_id = "be6a052e-bce8-4c14-8bbb-ea6b6f9941d5"
  secret_key = "secretzzzz"
  base_id = "TEafv3AfNptmzii4zj2jkc"

  modifications = [
    { "name": "text_candidate", "text": "John Wick" }
  ]

  # ensure no spaces in json output
  encoded = json.dumps(modifications, separators=(',', ':'))
  encoded = base64_encode(encoded).decode()

  parameters = "{user_id}+{base_id}+{modifications}".format(user_id=user_id, base_id=base_id, modifications=encoded)

  signature = hmac.new(secret_key.encode(), parameters.encode(), hashlib.sha256).hexdigest()

  url = "https://images.usestencil.com/signed-images/{user_id}/{base_id}.png?modifications={modifications}&s={signature}".format(
      base_id=base_id,
      user_id=user_id,
      modifications=encoded,
      signature=signature
  )
  print(url)

if __name__ == "__main__":
    generate_url()
```

{% endtab %}
{% endtabs %}

Running the above code gives us the following output,

```
https://images.usestencil.com/signed-images/be6a052e-bce8-4c14-8bbb-ea6b6f9941d5/TEafv3AfNptmzii4zj2jkc.png?modifications=W3sibmFtZSI6InRleHRfY2FuZGlkYXRlIiwidGV4dCI6IkpvaG4gV2ljayJ9XQ&s=f69736c6f17cea118a45dc018103694cbde78d89513de977c2fa1c55faf697df
```

### Result

Visiting the URL gives us the certificate,

![Signed image with candidate's name](/files/-MfRSE1Dfc0FPDMMFVui)

### Determining authenticity

All certificates generated and hot-linked using our CDN can be considered as authentic i.e. all links started with `https://images.usestencil.com/signed-images/...` are authentic.

#### Why?

{% hint style="success" %}
**TLDR**

All images are signed with your secret key. Trying to modify the data will invalidate the signature.

Continue reading to know the details.
{% endhint %}

Let's break it down. The URL can be broken down to several parts,

| Item                                                                                                          | Description                                                        |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `https://images.usestencil.com/signed-images/be6a052e-bce8-4c14-8bbb-ea6b6f9941d5/TEafv3AfNptmzii4zj2jkc.png` | This is the base URL                                               |
| `modifications=W3sibmFtZSI6InRleHRfY2FuZGlkYXRlIiwidGV4dCI6IkpvaG4gV2ljayJ9XQ`                                | Base64 encoded modifications                                       |
| `s=f69736c6f17cea118a45dc018103694cbde78d89513de977c2fa1c55faf697df`                                          | Signature that we use to determine the authenticity of the request |

If we decode the base64 modifications, we will get the following output,

```javascript
[{"name":"text_candidate","text":"John Wick"}]
```

You might think, "Hey what if I modify the value and encode it back. That should work, right?"

Let's just do that. We changed the value to `John the Impostor` and got back the following base64 encoded string.

```javascript
[{"name":"text_candidate","text":"John the Impostor"}]

W3sibmFtZSI6InRleHRfY2FuZGlkYXRlIiwidGV4dCI6IkpvaG4gdGhlIEltcG9zdG9yIn1d
```

Putting everything together we get back,

```javascript
https://images.usestencil.com/signed-images/be6a052e-bce8-4c14-8bbb-ea6b6f9941d5/TEafv3AfNptmzii4zj2jkc.png?modifications=W3sibmFtZSI6InRleHRfY2FuZGlkYXRlIiwidGV4dCI6IkpvaG4gdGhlIEltcG9zdG9yIn1d&s=f69736c6f17cea118a45dc018103694cbde78d89513de977c2fa1c55faf697df
```

Visiting the URL gave us the following image instead,

![Invalid image](/files/-MfRZQcC3Rw5wxF1m5J9)

{% hint style="warning" %}
If you look at the response header, you should see the following header

**`x-stencil-error: Invalid signature`**
{% endhint %}

The reason is that, the signature doesn't contain the right modifications and thus rejected by our server. By ensuring the image is signed with our secret key, we can ensure image authenticity. Simple, right?

## No-code URL generation

As you can see, we have to run one Python script to generate the link for our certificate. There are few things we can do so that we don't have to run the Python script each time. Here are few options you can explore to do this.

### 1. AWS Lambda

You can modify the Python script as a lambda function that accepts the candidate's name and when run, it will return the generated URL.

You can then combine this with other tools such as Airtable where you can input list of names, and use any automation services like Zapier, Integromat, UiPath and others to run the lambda function when your Airtable rows are added/updated and update the corresponding rows with the generated link.

### 2. Airtable

Airtable has scripting ability and you can write a similar script to generate the link.

### 3. Google Sheet

Google Sheet also has scripting ability and you can write a similar script to generate the link.

There are numerous of ways to do this and these are just some of them.&#x20;

{% hint style="success" %}
We have created a guide utilizing AWS Lambda for this. You can find it [here](/integrations/case-studies/automate-candidates-certificate-generation)
{% endhint %}


# Automate Candidate's Certificate Generation

{% hint style="success" %}
This is a follow up from the [Secure Signed Image guide](/integrations/case-studies/generate-certificate-of-accomplishment) on using signed image as building block for certificate.
{% endhint %}

In this guide, we will explore one of the techniques to automate the generation of signed URL.

## Requirements

You will need,

1. AWS account to access AWS Lambda
2. Airtable account
3. Integromat account
4. A working template to generate secure signed image. Please check the previous guide linked above.

{% hint style="info" %}
To be pedantic, this part involves a very small amount of code. So, maybe it's low-code rather than no-code. Either way, it is pretty straight forward to follow.
{% endhint %}

## Guides

### Deploying AWS Lambda Function

This is the part where very small amount of code is involved. So we will do this first.

#### Creating Lambda Function

1. Create a new lambda function named `secure_signed_image`.
2. Paste the following modified code. In this example, the modification is similar to what we had from the previous tutorial. It is modified to work with AWS lambda function.

```python
import json
import hmac
import hashlib
import base64

def base64_encode(string):
    """
    Removes any `=` used as padding from the encoded string.
    """
    encoded = base64.b64encode(string.encode())
    encoded = encoded.rstrip(b'=')
    return encoded

def generate_url(candidate):
  user_id = ""
  secret_key = ""
  base_id = ""

  modifications = [
    { "name": "text_candidate", "text": candidate }
  ]

  # ensure no spaces in json output
  encoded = json.dumps(modifications, separators=(',', ':'))
  encoded = base64_encode(encoded).decode()

  parameters = "{user_id}+{base_id}+{modifications}".format(user_id=user_id, base_id=base_id, modifications=encoded)

  signature = hmac.new(secret_key.encode(), parameters.encode(), hashlib.sha256).hexdigest()

  url = "https://images.usestencil.com/signed-images/{user_id}/{base_id}.png?modifications={modifications}&s={signature}".format(
      base_id=base_id,
      user_id=user_id,
      modifications=encoded,
      signature=signature
  )
  
  return url


def lambda_handler(event, context):
    # we read the name from the query string
    # i.e. https://xxx.amazonaws.com/default/secure_signed_image?name=David
    candidate = event["queryStringParameters"]["name"]
    
    url = generate_url(candidate)
    resp = {
        "url": url
    }
    
    return {
        'statusCode': 200,
        'body': json.dumps(resp)
    }

```

#### Creating HTTP trigger

We need to set up HTTP trigger so we can call the URL publicly and execute our lambda function. One additional step we need to take care of is to ensure our trigger is proxied to our lambda so we can capture the query string properly.

![](/files/-MfWhKUS39N3DUQhvLlg)

#### Video Guide

The following video shows the setup one-by-one.

{% embed url="<https://youtu.be/LXTb_bSCZlQ>" %}

### Setting up Airtable for automation

We will get our candidates' name from table in Airtable. Airtable needs to be set in certain ways to allow for Integromat automation.

![Generated signed URL will be populated into Signed URL column](/files/-MfXSyuZsPNH0AZsppAD)

For this example, we create 3 columns in which 2 columns are text and the last column (Last Modified) is a special Airtable column that tracks the last modified time. For this particular column, we only set the last modified time when the Name column is updated.

See the video for the process

{% embed url="<https://youtu.be/1aD1mOqgbCM>" %}

### Automate image generation with Airtable and Integromat

Now that we have launched our function to AWS Lambda and Airtable ready for integration, we can call the lambda function anytime to get back the signed URL.&#x20;

![This is how the modules are setup](/files/-MfX_FDad4kXCorB8qSK)

The workflow basically works like this,

1. Airtable module watches for record update (this is why we need the Last Modified column)
2. Get the value from the Name column and send a GET request to our lambda function with the name as a query string parameter. We also URL encode it.
3. Once we get back the URL, we update the related record with the generated signed URL.

{% embed url="<https://youtu.be/HSZBacQvv8c>" %}

{% hint style="success" %}
You can work on similar integration with Google Sheet.&#x20;
{% endhint %}


