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
email_templates:readPath, query, and header parameters
sizepageResponses
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
email_templates:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"template": {
"id": "welcome.html",
"content": "<h1>Hello FIRST_NAME</h1>"
}
}template.idRequiredtemplate.contentResponses
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.
email_templates:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"template": {
"id": "welcome.html",
"content": "<h1>Hello {{ first_name }}</h1>",
"vars": {
"first_name": "Jane"
},
"manifest": {
"type": "transactional",
"vars_required": [
"first_name"
],
"allowed_domains": []
}
}
}template.idRequiredtemplate.contentRequiredtemplate.varstemplate.manifestResponses
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
email_templates:readPath, query, and header parameters
templateIdRequiredResponses
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
email_templates:writePath, query, and header parameters
templateIdRequiredRequest Body
{
"template": {
"content": "<h1>Hello FIRST_NAME</h1><p>Updated</p>"
}
}template.contentRequiredResponses
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
email_templates:writePath, query, and header parameters
templateIdRequiredResponses
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
email_templates:writePath, query, and header parameters
templateIdRequiredRequest Body
{
"preview": {
"vars": {
"first_name": "Alex"
}
}
}preview.varsResponses
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
email_messages:writePath, query, and header parameters
templateIdRequiredRequest Body
{
"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.toRequiredemail.subjectRequiredemail.fromemail.ccemail.bccemail.varsemail.attachmentsResponses
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.