DonutworkHelp Center
Api ReferenceContent & Media
Donutwork documentation

Email Gateway

Manage reusable email templates, preview rendered output, and send transactional emails.

Email Gateway API

The Email Gateway API manages template files and transactional sends.

Template Status And Preflight

Transactional API sends can use templates in draft status. Preflight is a quality signal for operator workflows and publish control, but it does not hard-block send/test API execution.


List Templates

GET
/2026-02-01/email-gateway/templates.json
Required permissionemail_templates:read

Path, query, and header parameters

sizeinteger· query
Records per page (max 100).
pageinteger· query
Page index (starting from 1).

Responses

Template list returned.

{
  "entities": "Email Templates",
  "count": 2,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 1
  },
  "elements": [
    {
      "id": "welcome.html",
      "endpoint": [
        {
          "type": "POST",
          "action": "Send email using this template",
          "url": "https://api.hub.donutwork.com/2026-02-01/email-gateway/welcome.html/send.json"
        }
      ]
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Create Template

POST
/2026-02-01/email-gateway/templates.json
Required permissionemail_templates:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello FIRST_NAME</h1>"
  }
}
template.idstringRequired
Template filename identifier.
template.contentstring
Optional initial template content.

Responses

Template created.

{
  "id": "welcome.html"
}

Template already exists.

{
  "error": "Template already exists"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello FIRST_NAME</h1>"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "template": {
      "id": "welcome.html",
      "content": "<h1>Hello FIRST_NAME</h1>"
    }
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello FIRST_NAME</h1>"
  }
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Validate Template

Before persisting a template, validate its HTML, manifest, variables, allowed domains, and sensitive placeholders. Validation has no write side effect.

POST
/2026-02-01/email-gateway/templates/validate.json
Required permissionemail_templates:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello {{ first_name }}</h1>",
    "vars": {
      "first_name": "Jane"
    },
    "manifest": {
      "type": "transactional",
      "vars_required": [
        "first_name"
      ],
      "allowed_domains": []
    }
  }
}
template.idstringRequired
Template identifier validated with the same rules as create/update.
template.contentstringRequired
Candidate template content.
template.varsobject
Example variables used during preflight.
template.manifestobject
Candidate manifest. Transactional defaults are applied when fields are omitted.

Responses

Preflight report returned, including validation failures as data.

{
  "template_id": "welcome.html",
  "valid": true,
  "report": {
    "status": "ok",
    "errors": [],
    "warnings": []
  }
}

Input envelopes or field types are invalid.

{
  "error": "template.vars and template.manifest must be objects"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/validate.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello {{ first_name }}</h1>",
    "vars": {
      "first_name": "Jane"
    },
    "manifest": {
      "type": "transactional",
      "vars_required": [
        "first_name"
      ],
      "allowed_domains": []
    }
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/validate.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "template": {
      "id": "welcome.html",
      "content": "<h1>Hello {{ first_name }}</h1>",
      "vars": {
        "first_name": "Jane"
      },
      "manifest": {
        "type": "transactional",
        "vars_required": [
          "first_name"
        ],
        "allowed_domains": []
      }
    }
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/validate.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "template": {
    "id": "welcome.html",
    "content": "<h1>Hello {{ first_name }}</h1>",
    "vars": {
      "first_name": "Jane"
    },
    "manifest": {
      "type": "transactional",
      "vars_required": [
        "first_name"
      ],
      "allowed_domains": []
    }
  }
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Get Template

GET
/2026-02-01/email-gateway/templates/{templateId}.json
Required permissionemail_templates:read

Path, query, and header parameters

templateIdstring· pathRequired
Template identifier.

Responses

Template returned.

{
  "id": "welcome.html",
  "content": "<h1>Hello FIRST_NAME</h1>"
}

Template not found.

{
  "error": "Template not found"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Update Template

PUT
/2026-02-01/email-gateway/templates/{templateId}.json
Required permissionemail_templates:write

Path, query, and header parameters

templateIdstring· pathRequired
Template identifier.

Request Body

JSON
{
  "template": {
    "content": "<h1>Hello FIRST_NAME</h1><p>Updated</p>"
  }
}
template.contentstringRequired
Template content to persist.

Responses

Template updated.

{
  "id": "welcome.html",
  "updated": true
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "template": {
    "content": "<h1>Hello FIRST_NAME</h1><p>Updated</p>"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "template": {
      "content": "<h1>Hello FIRST_NAME</h1><p>Updated</p>"
    }
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "template": {
    "content": "<h1>Hello FIRST_NAME</h1><p>Updated</p>"
  }
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Delete Template

DELETE
/2026-02-01/email-gateway/templates/{templateId}.json
Required permissionemail_templates:write

Path, query, and header parameters

templateIdstring· pathRequired
Template identifier.

Responses

Template deleted.

{
  "id": "welcome.html",
  "deleted": true
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
  },
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
    ],
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Preview Template

POST
/2026-02-01/email-gateway/templates/{templateId}/preview.json
Required permissionemail_templates:write

Path, query, and header parameters

templateIdstring· pathRequired
Template identifier.

Request Body

JSON
{
  "preview": {
    "vars": {
      "first_name": "Alex"
    }
  }
}
preview.varsobject
Variables used for server-side placeholder rendering.

Responses

Rendered HTML preview returned.

{
  "template_id": "welcome.html",
  "html": "<h1>Hello Alex</h1>"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID/preview.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "preview": {
    "vars": {
      "first_name": "Alex"
    }
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID/preview.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "preview": {
      "vars": {
        "first_name": "Alex"
      }
    }
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/templates/TEMPLATEID/preview.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "preview": {
    "vars": {
      "first_name": "Alex"
    }
  }
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

Dispatch Transactional Email

POST
/2026-02-01/email-gateway/{templateId}/send.json
Required permissionemail_messages:write

Path, query, and header parameters

templateIdstring· pathRequired
Template identifier.

Request Body

JSON
{
  "email": {
    "to": "recipient@example.com",
    "subject": "Operational Update",
    "from": "notifications@example.com",
    "cc": [
      "ops@example.com"
    ],
    "vars": {
      "first_name": "Alex"
    },
    "attachments": [
      {
        "content": "SGVsbG8gd29ybGQ=",
        "filename": "summary.txt",
        "type": "text/plain"
      }
    ]
  }
}
email.tostringRequired
Primary recipient email.
email.subjectstringRequired
Message subject.
email.fromstring
Optional sender email from configured alternative senders.
email.ccarray
CC recipients.
email.bccarray
BCC recipients.
email.varsobject
Template variables.
email.attachmentsarray
Attachments in base64 format.

Responses

Email accepted for delivery.

{
  "email": {
    "id": "msg_123"
  }
}

Template not found.

{
  "error": "Template not found or empty"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/email-gateway/TEMPLATEID/send.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "email": {
    "to": "recipient@example.com",
    "subject": "Operational Update",
    "from": "notifications@example.com",
    "cc": [
      "ops@example.com"
    ],
    "vars": {
      "first_name": "Alex"
    },
    "attachments": [
      {
        "content": "SGVsbG8gd29ybGQ=",
        "filename": "summary.txt",
        "type": "text/plain"
      }
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/email-gateway/TEMPLATEID/send.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "email": {
      "to": "recipient@example.com",
      "subject": "Operational Update",
      "from": "notifications@example.com",
      "cc": [
        "ops@example.com"
      ],
      "vars": {
        "first_name": "Alex"
      },
      "attachments": [
        {
          "content": "SGVsbG8gd29ybGQ=",
          "filename": "summary.txt",
          "type": "text/plain"
        }
      ]
    }
  }),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
  const error = await response.text();
  throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
  : contentType.includes('application/json') ? await response.json()
  : await response.text();
$request = curl_init('https://api.hub.donutwork.com/2026-02-01/email-gateway/TEMPLATEID/send.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{
  "email": {
    "to": "recipient@example.com",
    "subject": "Operational Update",
    "from": "notifications@example.com",
    "cc": [
      "ops@example.com"
    ],
    "vars": {
      "first_name": "Alex"
    },
    "attachments": [
      {
        "content": "SGVsbG8gd29ybGQ=",
        "filename": "summary.txt",
        "type": "text/plain"
      }
    ]
  }
}',
]);
$response = curl_exec($request);
if ($response === false) {
    $error = curl_error($request);
    curl_close($request);
    throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
    : (str_contains($contentType, 'application/json')
        ? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);

Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.

On this page