DonutworkHelp Center
Api ReferenceContent & Media
Donutwork documentation

Newsletter Campaigns

Create campaigns, manage recipients, queue sends, inspect stats, and control suppression lists.

Newsletter API

The Newsletter API exposes campaign lifecycle, recipient synchronization, dispatch queue controls, analytics, and suppression list management.

Operational Validation Behavior

Preflight and compliance analysis can be attached as advisory diagnostics in send/test flows. These diagnostics improve quality but do not hard-block operational newsletter dispatch by themselves.


List Newsletter Campaigns

GET
/2026-02-01/newsletters.json
Required permissionnewsletters:read

Path, query, and header parameters

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

Responses

Newsletter list returned.

{
  "entities": "Newsletter",
  "count": 1,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 1
  },
  "elements": [
    {
      "id": "672489...",
      "title": "Launch Campaign",
      "subject": "New release",
      "template_id": "welcome.html",
      "total_recipients": 210,
      "status": "draft",
      "created_at": 1704067200
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/newsletters.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters.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/newsletters.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 Newsletter Campaign

POST
/2026-02-01/newsletters.json
Required permissionnewsletters:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "newsletter": {
    "title": "Launch Campaign",
    "subject": "New release",
    "template_id": "welcome.html",
    "contact_ids": [
      "cust_1",
      "cust_2"
    ]
  }
}
newsletter.titlestringRequired
Internal campaign name.
newsletter.subjectstringRequired
Subject line used during sending.
newsletter.template_idstringRequired
Template filename to use for this campaign.
newsletter.contact_idsarray
Optional initial recipients list.

Responses

Newsletter draft created.

{
  "id": "672489...",
  "status": "draft",
  "skipped_suppressed": 0,
  "skipped_not_found": 1
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/newsletters.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "newsletter": {
    "title": "Launch Campaign",
    "subject": "New release",
    "template_id": "welcome.html",
    "contact_ids": [
      "cust_1",
      "cust_2"
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "newsletter": {
      "title": "Launch Campaign",
      "subject": "New release",
      "template_id": "welcome.html",
      "contact_ids": [
        "cust_1",
        "cust_2"
      ]
    }
  }),
});
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/newsletters.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 => '{
  "newsletter": {
    "title": "Launch Campaign",
    "subject": "New release",
    "template_id": "welcome.html",
    "contact_ids": [
      "cust_1",
      "cust_2"
    ]
  }
}',
]);
$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.

Retrieve Campaign Details

GET
/2026-02-01/newsletters/{newsletterId}.json
Required permissionnewsletters:read

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Responses

Campaign details returned.

{
  "id": "672489...",
  "title": "Launch Campaign",
  "subject": "New release",
  "template_id": "welcome.html",
  "total_recipients": 210,
  "status": "queued",
  "scheduled_at": null,
  "created_at": 1704067200
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.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/newsletters/NEWSLETTERID.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 Campaign Metadata

PUT
/2026-02-01/newsletters/{newsletterId}.json
Required permissionnewsletters:write

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Request Body

JSON
{
  "newsletter": {
    "title": "Launch Campaign v2",
    "subject": "Release update",
    "template_id": "welcome-v2.html"
  }
}
newsletter.titlestring
Updated title. Allowed only in draft status.
newsletter.subjectstring
Updated subject. Allowed only in draft status.
newsletter.template_idstring
Updated template id. Allowed only in draft status.

Responses

Campaign metadata updated.

{
  "id": "672489...",
  "title": "Launch Campaign v2",
  "subject": "Release update",
  "template_id": "welcome-v2.html",
  "total_recipients": 210,
  "status": "draft",
  "scheduled_at": null,
  "created_at": 1704067200
}

Campaign is not in draft status.

{
  "error": "Only newsletters in draft status can be updated"
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "newsletter": {
    "title": "Launch Campaign v2",
    "subject": "Release update",
    "template_id": "welcome-v2.html"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "newsletter": {
      "title": "Launch Campaign v2",
      "subject": "Release update",
      "template_id": "welcome-v2.html"
    }
  }),
});
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/newsletters/NEWSLETTERID.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 => '{
  "newsletter": {
    "title": "Launch Campaign v2",
    "subject": "Release update",
    "template_id": "welcome-v2.html"
  }
}',
]);
$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 Campaign

DELETE
/2026-02-01/newsletters/{newsletterId}.json
Required permissionnewsletters:write

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Responses

Campaign and related queue entries deleted.

{
  "id": "672489...",
  "deleted": true
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID.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/newsletters/NEWSLETTERID.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.

Get Campaign Recipients Snapshot

GET
/2026-02-01/newsletters/{newsletterId}/recipients.json
Required permissionnewsletters:read

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.
searchstring· query
Optional search filter by customer name or email.

Responses

Recipients snapshot returned.

{
  "newsletter_status": "draft",
  "selected_ids": [
    "cust_1",
    "cust_2"
  ],
  "all_customers": [
    {
      "id": "cust_1",
      "name": "Alex Doe",
      "email": "alex@example.com",
      "tags": [
        "beta"
      ],
      "subscription_count": 1,
      "has_consent": true,
      "is_suppressed": false
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/recipients.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/recipients.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/newsletters/NEWSLETTERID/recipients.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.

Set Campaign Recipients

PUT
/2026-02-01/newsletters/{newsletterId}/recipients.json
Required permissionnewsletters:write

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Request Body

JSON
{
  "newsletter": {
    "contact_ids": [
      "cust_1",
      "cust_2",
      "cust_3"
    ]
  }
}
newsletter.contact_idsarrayRequired
Recipient customer IDs to store on the campaign.

Responses

Recipients synchronized.

{
  "newsletter_id": "672489...",
  "saved_recipients": 2,
  "skipped_suppressed": 1,
  "skipped_not_found": 0
}

Request examples

curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/recipients.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "newsletter": {
    "contact_ids": [
      "cust_1",
      "cust_2",
      "cust_3"
    ]
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/recipients.json', {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "newsletter": {
      "contact_ids": [
        "cust_1",
        "cust_2",
        "cust_3"
      ]
    }
  }),
});
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/newsletters/NEWSLETTERID/recipients.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 => '{
  "newsletter": {
    "contact_ids": [
      "cust_1",
      "cust_2",
      "cust_3"
    ]
  }
}',
]);
$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.

Queue Campaign Dispatch

POST
/2026-02-01/newsletters/{newsletterId}/queue.json
Required permissionnewsletters:write

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Responses

Campaign queued for sending.

{
  "newsletter_id": "672489...",
  "status": "queued",
  "queued_recipients": 150,
  "skipped_suppressed": 0,
  "skipped_not_found": 0
}

Invalid state or usage limit reached.

{
  "error": "This newsletter is already queued and cannot be queued again"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/queue.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/queue.json', {
  method: 'POST',
  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/newsletters/NEWSLETTERID/queue.json');
curl_setopt_array($request, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    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.

Send Test Email

POST
/2026-02-01/newsletters/{newsletterId}/test.json
Required permissionnewsletters:write

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.

Request Body

JSON
{
  "newsletter": {
    "test_email": "qa@example.com"
  }
}
newsletter.test_emailstringRequired
Target email for test send.

Responses

Test email sent.

{
  "newsletter_id": "672489...",
  "test_email": "qa@example.com",
  "sent": true
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/test.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "newsletter": {
    "test_email": "qa@example.com"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/test.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "newsletter": {
      "test_email": "qa@example.com"
    }
  }),
});
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/newsletters/NEWSLETTERID/test.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 => '{
  "newsletter": {
    "test_email": "qa@example.com"
  }
}',
]);
$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 Campaign Stats

GET
/2026-02-01/newsletters/{newsletterId}/stats.json
Required permissionnewsletters:read

Path, query, and header parameters

newsletterIdstring· pathRequired
Newsletter identifier.
failure_limitinteger· query
Maximum failed/skipped items to include.

Responses

Campaign stats returned.

{
  "newsletter": {
    "id": "672489...",
    "title": "Launch Campaign",
    "subject": "New release",
    "template_id": "welcome.html",
    "total_recipients": 150,
    "status": "queued",
    "scheduled_at": null,
    "created_at": 1704067200
  },
  "stats": {
    "pending": 12,
    "processing": 3,
    "sent": 120,
    "failed": 10,
    "skipped": 5,
    "total": 150
  },
  "progress": {
    "processed": 135,
    "total": 150,
    "overall_progress": 90,
    "delivery_rate": 80
  },
  "failures": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/stats.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/NEWSLETTERID/stats.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/newsletters/NEWSLETTERID/stats.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.


Suppression List

List Unsubscribes

GET
/2026-02-01/newsletters/unsubscribes.json
Required permissionnewsletter_unsubscribes:read

Path, query, and header parameters

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

Responses

Suppression list returned.

{
  "entities": "Unsubscribes",
  "count": 1,
  "per_page": 100,
  "pages": {
    "current": 1,
    "max": 1
  },
  "elements": [
    {
      "email": "user@example.com",
      "reason": "Manual unsubscribe via API",
      "unsubscribed_at": 1704067200
    }
  ]
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes.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/newsletters/unsubscribes.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.

Add Unsubscribe

POST
/2026-02-01/newsletters/unsubscribes.json
Required permissionnewsletter_unsubscribes:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "unsubscribe": {
    "email": "user@example.com",
    "reason": "Support request"
  }
}
unsubscribe.emailstringRequired
Email to suppress.
unsubscribe.reasonstring
Optional reason for audit trail.

Responses

Email added to suppression list.

{
  "email": "user@example.com",
  "unsubscribed": true
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "unsubscribe": {
    "email": "user@example.com",
    "reason": "Support request"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "unsubscribe": {
      "email": "user@example.com",
      "reason": "Support request"
    }
  }),
});
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/newsletters/unsubscribes.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 => '{
  "unsubscribe": {
    "email": "user@example.com",
    "reason": "Support request"
  }
}',
]);
$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.

Resubscribe Email

POST
/2026-02-01/newsletters/unsubscribes/resubscribe.json
Required permissionnewsletter_unsubscribes:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "unsubscribe": {
    "email": "user@example.com"
  }
}
unsubscribe.emailstringRequired
Email to remove from suppression list.

Responses

Email removed from suppression list.

{
  "email": "user@example.com",
  "resubscribed": true
}

Email not found in suppression list.

{
  "error": "Email not found in suppression list"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes/resubscribe.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "unsubscribe": {
    "email": "user@example.com"
  }
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/newsletters/unsubscribes/resubscribe.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "unsubscribe": {
      "email": "user@example.com"
    }
  }),
});
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/newsletters/unsubscribes/resubscribe.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 => '{
  "unsubscribe": {
    "email": "user@example.com"
  }
}',
]);
$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