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:
- Create a project – Set up an HTML email project to hold your templates
- Create a template – Initialize a new template with basic settings
- Add content – Upload your HTML and set the subject line
- 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:
代码示例
{
"name": "Transactional Emails",
"type": "htmlEmail"
}Response
代码示例
{
"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:
代码示例
{
"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
代码示例
{
"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:
代码示例
{
"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:
代码示例
{
"editor": "html",
"defaultLocale": "en"
}Response
代码示例
{
"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:
代码示例
{
"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.
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:
代码示例
{"code": "InvalidPayload", "details": {".plainHtml": ["{% unsubscribe %}: missing required property"]}}Example of a valid unsubscribe link in your 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.
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.).
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.
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
代码示例
{
"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:
代码示例
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:
代码示例
<!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):
代码示例
<!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="/gitbook-assets/api/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}
代码示例
{
"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:
代码示例
{
"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="/gitbook-assets/api/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:
代码示例
{
"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:
代码示例
{
"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:
代码示例
{
"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
代码示例
{
"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
代码示例
{
"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).
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.
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.
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.
Migrating templates between workspaces
To move a template from one workspace to another (e.g., sandbox to production), follow these steps:
- Retrieve the source template content – Call the GET content endpoint for each locale. Save the plainHtml, subject, variables, attachments, and assetRefs fields.
- Create a project in the target workspace – Use the POST projects endpoint.
- Create a template in the new project – Use the POST html-emails endpoint with the same editor and defaultLocale.
- 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.
- 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}
代码示例
{
"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
代码示例
{
"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.