> For the complete documentation index, see [llms.txt](https://docs.bird.com/api/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.bird.com/api/touchpoints-api/supported-projects/creating-html-email-templates.md).

# Creating HTML Email Templates

## Creating HTML Email Templates

### Creating HTML Email Templates

This guide walks you through creating and managing HTML email templates using the Bird API. By the end, you'll have a fully functional email template ready to send.

#### Overview

Creating an HTML email template involves the following steps:

1. **Create a project** – Set up an HTML email project to hold your templates
2. **Create a template** – Initialize a new template with basic settings
3. **Add content** – Upload your HTML and set the subject line
4. **Activate the template** – Make it available for sending

Let's walk through each step.

***

#### Step 1: Create a project

Before you can create templates, you need an HTML email project to store them. Projects act as containers that organize your templates and define their type.

**Request**

```
POST /workspaces/{workspaceId}/projects
```

To create a basic HTML email project:

```json
{
  "name": "Transactional Emails",
  "type": "htmlEmail"
}
```

**Response**

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "workspaceId": "ws-123",
  "name": "Transactional Emails",
  "type": "htmlEmail",
  "draftCount": 0,
  "activeCount": 0,
  "inactiveCount": 0,
  "createdAt": "2025-01-15T10:00:00Z",
  "updatedAt": "2025-01-15T10:00:00Z"
}
```

Save the `id` from the response – this is your `projectId` for the next steps.

**Adding optional fields**

You can include additional fields when creating the project:

```json
{
  "name": "Marketing Campaigns",
  "description": "Email templates for marketing campaigns",
  "type": "htmlEmail",
  "suites": ["marketing", "automations"]
}
```

| Field       | Required | Description                                                                    |
| ----------- | -------- | ------------------------------------------------------------------------------ |
| name        | No       | Project name (max 100 characters)                                              |
| description | No       | Project description (max 300 characters)                                       |
| type        | Yes      | Must be `htmlEmail` for email templates                                        |
| suites      | No       | Categorization: `marketing`, `service`, `payments`, `automations`, `developer` |

**Alternative: Create project with initial template**

You can also create a project and its first template in a single request:

```
POST /workspaces/{workspaceId}/projects/html-emails
```

```json
{
  "name": "Order Notifications",
  "type": "htmlEmail",
  "templateEditor": "html",
  "templateDefaultLocale": "en",
  "templateDescription": "Order confirmation email",
  "templateUseCase": "transactional"
}
```

This approach saves an API call when you know you'll need a template immediately.

**Combo endpoint response**

The response from this endpoint has a **different structure** than the standalone create-project response. It returns a nested object containing the project, template, a presigned upload URL (so you can upload HTML immediately), and default content:

```json
{
  "project": {
    "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
    "name": "Order Notifications",
    "type": "htmlEmail",
    "createdAt": "2025-01-15T10:00:00Z",
    "updatedAt": "2025-01-15T10:00:00Z"
  },
  "template": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "projectId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
    "status": "draft",
    "editor": "html",
    "defaultLocale": "en",
    "description": "Order confirmation email",
    "useCase": "transactional",
    "createdAt": "2025-01-15T10:00:00Z",
    "updatedAt": "2025-01-15T10:00:00Z"
  },
  "presignedUpload": {
    "uploadUrl": "https://s3.amazonaws.com/...",
    "uploadMethod": "POST",
    "uploadFormData": { ... }
  },
  "content": {
    "subject": "",
    "plainHtml": "<!DOCTYPE html>\n<html></html>"
  }
}
```

Save `project.id` as your `projectId` and `template.id` as your `templateId`. You can use the included `presignedUpload` to upload HTML content straight away (see Step 3b), skipping the separate presigned-upload request.

***

#### Step 2: Create a template

Once you have a project, you can create templates within it. Each project can contain multiple templates.

**Request**

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails
```

For a simple HTML template, you only need to specify the editor type and default locale:

```json
{
  "editor": "html",
  "defaultLocale": "en"
}
```

**Response**

```json
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "projectId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "status": "draft",
  "editor": "html",
  "defaultLocale": "en",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}
```

The template is created in `draft` status. Save the `id` from the response – you'll need it for the next steps.

**Adding optional fields**

You can include additional fields when creating the template:

```json
{
  "editor": "html",
  "defaultLocale": "en",
  "description": "Order confirmation email",
  "useCase": "transactional"
}
```

| Field         | Required | Description                                                                            |
| ------------- | -------- | -------------------------------------------------------------------------------------- |
| editor        | Yes      | Set to `html` for raw HTML templates                                                   |
| defaultLocale | Yes      | Default language code (e.g., `en`, `de`, `fr`)                                         |
| description   | No       | A description to help identify the template                                            |
| useCase       | No       | Classification: `transactional`, `marketing`, `marketingCustomUnsubscribe`, or `other` |

The `useCase` field affects compliance requirements. Marketing emails require an unsubscribe link, while transactional emails do not.

{% hint style="warning" %}
**Marketing unsubscribe requirement:** If `useCase` is `marketing`, your HTML **must** contain the `{% unsubscribe %}` template tag as a link href. Bird replaces this tag with the actual unsubscribe URL at send time. Without it, the API will reject your HTML upload with an error:

```json
{"code": "InvalidPayload", "details": {".plainHtml": ["{% unsubscribe %}: missing required property"]}}
```

Example of a valid unsubscribe link in your HTML:

```html
<a href="{% unsubscribe %}">Unsubscribe</a>
```

If `useCase` is `marketingCustomUnsubscribe`, you manage the unsubscribe flow yourself and this tag is not required. Transactional templates (`transactional` or `other`) do not require an unsubscribe link.
{% endhint %}

***

#### Step 3: Add content

Adding content to an `html` editor template is a two-part process: first you upload the HTML file, then you update the template metadata (subject, variables, attachments, etc.).

{% hint style="info" %}
**Content is always locale-scoped.** All content endpoints (upload, get, patch, delete) require a `{locale}` path parameter. There is no endpoint to retrieve or set content without specifying a locale. If you just need the "main" content, use the template's `defaultLocale` value (e.g., `en`) as the locale parameter.
{% endhint %}

**3a. Get a presigned upload URL**

Request a presigned URL to upload your HTML file:

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/{locale}/presigned-upload
```

No request body is required (send `{}`).

**Response**

```json
{
  "uploadUrl": "https://s3.amazonaws.com/...",
  "uploadMethod": "POST",
  "uploadFormData": {
    "Content-Type": "text/html",
    "acl": "private",
    "bucket": "touchpoints--html-email-contents--...",
    "key": "{workspaceId}/{projectId}/{templateId}/{locale}.html",
    "policy": "...",
    "x-amz-algorithm": "AWS4-HMAC-SHA256",
    "x-amz-credential": "...",
    "x-amz-date": "...",
    "x-amz-security-token": "...",
    "x-amz-signature": "..."
  }
}
```

The response contains the upload URL along with form data fields that **must** be included in the upload request.

**3b. Upload the HTML file**

Upload your HTML content to the presigned URL as a **multipart form POST**. You must include all fields from `uploadFormData` as form fields, with the `file` field appended **last**:

```bash
curl -X POST "${uploadUrl}" \
  -F "Content-Type=text/html" \
  -F "acl=private" \
  -F "bucket=${bucket}" \
  -F "key=${key}" \
  -F "policy=${policy}" \
  -F "x-amz-algorithm=${algorithm}" \
  -F "x-amz-credential=${credential}" \
  -F "x-amz-date=${date}" \
  -F "x-amz-security-token=${token}" \
  -F "x-amz-signature=${signature}" \
  -F "file=@template.html"
```

> **Important:** Use `POST` with multipart form data — not `PUT` with a raw body. The `uploadMethod` field in the response confirms this. All `uploadFormData` fields are required for S3 to authenticate and accept the upload.

A successful upload returns **HTTP 204 No Content** with an empty response body. This is expected — it means the upload to S3 succeeded. If you receive a 204, proceed to step 3c.

Your HTML can include variables using the `{{variableName}}` syntax.

**Transactional example:**

```html
<!DOCTYPE html>
<html>
  <head><meta charset="utf-8"></head>
  <body>
    <h1>Thanks, {{customerName}}!</h1>
    <p>Your order #{{orderId}} for {{orderTotal}} has been confirmed.</p>
  </body>
</html>
```

**Marketing example** (note the required `{% unsubscribe %}` tag):

```html
<!DOCTYPE html>
<html>
  <head><meta charset="utf-8"></head>
  <body>
    <h1>{{firstName}}, your exclusive offer is waiting!</h1>
    <p>Get 30% off with code <strong>SAVE30</strong>.</p>
    <a href="https://example.com/sale">Shop the Sale</a>
    <hr>
    <p><a href="{% unsubscribe %}">Unsubscribe</a></p>
  </body>
</html>
```

> **Note on image references:** If your HTML uses relative image paths like `<img src="header.png">`, these will not resolve in the sent email. Either use absolute URLs (e.g., `<img src="https://cdn.example.com/header.png">`) or use the raw-files upload + `assetRefs` flow described later in this section to map local filenames to hosted URLs.

**3c. Update the template content**

After the upload completes, set the subject, variables, and other metadata. The uploaded HTML is automatically associated with the template locale — no file name reference is needed.

```
PATCH /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/{locale}
```

```json
{
  "subject": "Order #{{orderId}} confirmed",
  "variables": {
    "customerName": {
      "description": "Customer's full name",
      "example": "Jane Smith"
    },
    "orderId": {
      "description": "Order reference",
      "example": "ORD-98765"
    },
    "orderTotal": {
      "description": "Formatted order total",
      "example": "$149.99"
    }
  }
}
```

> **Important:** You cannot set HTML content directly via `plainHtml` on this endpoint — it must be uploaded via the presigned URL. The `plainHtml` field is read-only on the GET response.

**Adding attachments**

Include file attachments in the same PATCH request:

```json
{
  "subject": "Your invoice is ready",
  "attachments": [
    {
      "url": "https://cdn.example.com/invoices/invoice-123.pdf",
      "filename": "invoice.pdf"
    }
  ]
}
```

Each attachment requires:

* `url` – The URL where the file is hosted
* `filename` – The name shown to recipients

**Mapping image assets (raw file upload)**

If your HTML references images by local filenames (e.g. `<img src="logo.png">`) and you want to map them to CDN URLs, use the **raw file upload** endpoint instead:

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/raw-files/presigned-upload
```

This endpoint returns a `fileName` field in addition to the upload details. After uploading, pass `rawFileName` and `assetRefs` in the PATCH request:

```json
{
  "subject": "Check out our new products",
  "rawFileName": "abc123-template.html",
  "assetRefs": {
    "logo.png": "https://cdn.example.com/assets/logo.png",
    "header.png": "https://cdn.example.com/assets/email-header.png"
  }
}
```

> **Note:** `assetRefs` should only be specified together with `rawFileName`.

**Complete content example**

Here's a full PATCH example combining subject, variables, and attachments:

```json
{
  "subject": "Order #{{orderId}} confirmed",
  "variables": {
    "customerName": {
      "description": "Customer's full name",
      "example": "Jane Smith"
    },
    "orderId": {
      "description": "Order reference",
      "example": "ORD-98765"
    },
    "orderTotal": {
      "description": "Formatted order total",
      "example": "$149.99"
    }
  },
  "attachments": [
    {
      "url": "https://cdn.example.com/receipts/receipt.pdf",
      "filename": "receipt.pdf"
    }
  ]
}
```

**Updating only the subject or variables**

If you only need to update metadata without changing the HTML, just send the fields you want to update:

```json
{
  "subject": "Updated subject line for {{customerName}}"
}
```

***

#### Step 4: Activate the template

Once your content is ready, activate the template to make it available for sending emails.

**Request**

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/activate
```

No request body is required.

**Response**

```json
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "active",
  "editor": "html",
  "defaultLocale": "en",
  "description": "Order confirmation email",
  "useCase": "transactional",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:35:00Z"
}
```

The template status changes from `draft` to `active`. You can now use this template to send emails.

***

#### Adding multiple languages

Templates support multiple locales. After creating content for your default language, you can add additional languages by repeating the upload-and-patch process with a different locale code.

**Add German content**

First, get a presigned upload URL for the German locale:

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/de/presigned-upload
```

Upload the German HTML file to the returned `uploadUrl` using multipart form POST (as described in Step 3b), then patch the content:

```
PATCH /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/de
```

```json
{
  "subject": "Bestellung #{{orderId}} bestätigt",
  "variables": {
    "customerName": {
      "description": "Name des Kunden",
      "example": "Max Mustermann"
    },
    "orderId": {
      "description": "Bestellnummer",
      "example": "ORD-98765"
    }
  }
}
```

Common locale codes: `en`, `en-US`, `en-GB`, `de`, `de-DE`, `fr`, `fr-FR`, `es`, `es-ES`, `it`, `nl`, `pt`, `pt-BR`, `ja`, `zh`, `ko`.

***

#### Verifying your template

After activation, you can verify your template was created correctly.

**Get template details**

```
GET /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}
```

This returns the template metadata and a content summary per locale (subject and parameters). It does not include the full HTML content or attachments.

**Get full template content**

To retrieve the full content for a specific locale, including the HTML and attachments, use the content endpoint:

```
GET /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/{locale}
```

The `plainHtml` field in the response contains the HTML content as stored by Bird. This endpoint returns the HTML without any additional transformations (no branded footer, no variable substitution).

{% hint style="info" %}
**About `plainHtml`:** The returned HTML may not be byte-for-byte identical to what you originally uploaded. Bird may normalize the HTML during storage (whitespace, attribute ordering, etc.). The content is functionally equivalent and suitable for migration between workspaces, but do not expect it to match your source file exactly.

`plainHtml` is a **read-only** field. To update HTML content, use the presigned upload workflow described in Step 3.
{% endhint %}

**Preview the template**

To see how the template renders with example variable values:

```
GET /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/render/{locale}
```

This returns the HTML with `{{variables}}` replaced by their example values and may include a branded footer ("Powered by Bird") depending on your workspace settings.

{% hint style="info" %}
**`content` vs `render`:** Use `GET .../content/{locale}` when you need the stored source HTML (for migration, backup, or editing). Use `GET .../render/{locale}` when you want to preview how the email will look to recipients.
{% endhint %}

***

#### Migrating templates between workspaces

To move a template from one workspace to another (e.g., sandbox to production), follow these steps:

1. **Retrieve the source template content** – Call the GET content endpoint for each locale. Save the `plainHtml`, `subject`, `variables`, `attachments`, and `assetRefs` fields.
2. **Create a project in the target workspace** – Use the POST projects endpoint.
3. **Create a template in the new project** – Use the POST html-emails endpoint with the same `editor` and `defaultLocale`.
4. **Upload HTML and set content for each locale** – For each locale: a. Get a presigned upload URL in the target workspace via the `.../content/{locale}/presigned-upload` endpoint. b. Save the `plainHtml` from step 1 as an HTML file and upload it to the presigned URL using multipart form POST with all `uploadFormData` fields. c. PATCH the content with `subject`, `variables`, and `attachments`.
5. **Activate the template** – Use the POST activate endpoint.

***

#### Managing templates

**List all templates**

```
GET /workspaces/{workspaceId}/projects/{projectId}/html-emails
```

Returns all templates in the project.

**Update template metadata**

To change the description or use case without modifying content:

```
PATCH /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}
```

```json
{
  "description": "Updated order confirmation",
  "useCase": "marketing"
}
```

**Deactivate a template**

To temporarily disable a template:

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/deactivate
```

The status changes to `inactive`. You can reactivate it later using the activate endpoint.

**Clone a template**

To copy a template to the same or different project:

```
POST /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/clone
```

```json
{
  "toProjectId": "target-project-id"
}
```

Omit `toProjectId` to clone within the same project.

**Delete content for a locale**

To remove a specific language version:

```
DELETE /workspaces/{workspaceId}/projects/{projectId}/html-emails/{templateId}/content/{locale}
```

***

#### Template status reference

| Status        | Description                                             |
| ------------- | ------------------------------------------------------- |
| draft         | Template is being edited and cannot be used for sending |
| active        | Template is live and available for sending emails       |
| inactive      | Template has been deactivated                           |
| pending       | Template is being processed                             |
| pendingReview | Template is awaiting review                             |

***

#### Next steps

For a complete list of endpoints and parameters, see the [API Reference](https://docs.bird.com/api/touchpoints-api/api-reference/email-templates).
