API Documentation

Buzzz API Reference

REST endpoints, webhooks, and SDK examples for transactional email.

REST APIJSONHTTPS Only

Quick Start

Send your first email in 30 seconds
1

Sign up and get your API key from the dashboard

2

Send a POST request to the emails endpoint

curl -X POST https://buzzzmail.co.za/api/v1/emails \
  -H "Authorization: Bearer bz_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "recipient@example.com",
    "subject": "Hello from Buzzz!",
    "html": "<h1>Welcome!</h1><p>Your first email via Buzzz API.</p>"
  }'

Authentication

All API requests require authentication via API key in the Authorization header:

Authorization: Bearer bz_your_api_key_here

Keys carry a scope, chosen when the key is created. A full key reaches every endpoint on this page. A sending-only key reaches exactly these six, and nothing else: everything else answers 403 api_key_scope, so leaking one cannot export your contacts:

POST/api/v1/emailsSend or schedule an email
POST/api/v1/emails/batchSend up to 100 emails
GET/api/v1/emails/{id}Read one of this key's own sends, with its delivery timeline
DELETE/api/v1/emails/{id}Cancel one of this key's scheduled emails
POST/api/v1/smsSend or schedule an SMS
DELETE/api/v1/sms/{id}Cancel one of this key's scheduled SMS

The two reads on that list are scoped to the presenting key's own sends, which is what makes them safe for a sending-only key: it can poll the status of a message it sent, but it cannot enumerate your sends; GET /api/v1/emails and GET /api/v1/sms need a full key. A key can also be restricted to a single verified sending domain, which refuses any send whose From address is outside it.

Security Best Practices

  • Never expose API keys in client-side code
  • Use environment variables to store keys
  • Rotate keys periodically
  • Use separate keys for production and development

Send Email

POST/api/v1/emails
Send an email to one or more recipients

Request Body

FieldTypeRequiredDescription
tostringRequiredRecipient email address
subjectstringRequiredEmail subject line
htmlstringhtml or textHTML body content
textstringhtml or textPlain text body content
fromstringOptionalSender email (must be verified)
fromNamestringOptionalSender display name
replyTostringOptionalReply-to address
ccstringOptionalCC recipients (comma-separated)
bccstringOptionalBCC recipients (comma-separated)
categorystringOptionaltransactional (default) or marketing. See below.
scheduled_atstringOptionalISO 8601 datetime, in the future and at most 72 hours ahead. See Scheduling below.
tagsstring[]OptionalUp to 20 labels (64 characters each), returned on reads and filterable with ?tag=
metadataobjectOptionalUp to 50 string key/value pairs (values max 1024 characters), stored verbatim and returned on reads
attachmentsobject[]OptionalUp to 20 files, each { filename, content | url, content_type }. Transactional sends only; see below.

Transactional vs. marketing

This endpoint defaults to transactional: one-to-one mail the recipient is expecting (receipts, password resets, alerts). Transactional mail carries no unsubscribe headers or footer, and is still delivered to addresses that unsubscribed from your marketing lists (though never to addresses that bounced or filed a spam complaint).

Promotional mail must be sent with "category": "marketing". Marketing sends automatically get RFC 8058 List-Unsubscribe and List-Unsubscribe-Post headers plus an unsubscribe footer, and are rejected for recipients who have unsubscribed. The unsubscribe link identifies the to address, so send marketing mail to one recipient per request.

Sending promotional content as transactional is a Terms of Service violation: it puts the shared sending reputation at risk and denies recipients the opt-out the law requires.

Scheduling and cancellation

With scheduled_at the request returns 202 and a record in status SCHEDULED instead of sending immediately. The allowance is committed when the send is accepted, not when it fires, and entitlements are re-checked at fire time: a plan that shrank in the meantime refuses the send rather than exceeding it.

DELETE /api/v1/emails/{id} cancels it. Before the worker picks the email up you get 200 and the record moves to CANCELLED, which is not counted against your allowance. Once it has been picked up you get 409: it may already be with the provider, so no promise can be made about stopping it.

Idempotency

Send an Idempotency-Key header (up to 255 characters of A-Z a-z 0-9 _ - : .; a UUID or ULID does nicely) to make a retry safe. Within 24 hours a repeat of the same key returns the first response verbatim (same id, same status), and sends nothing further.

A key whose first request is still in flight returns 409 idempotency_key_in_use; retry in a moment for the stored result. Keys pin only successful requests: one that was refused or failed releases its key, so a corrected retry is a fresh request.

Suppression

Every address in to, cc and bcc is checked against your suppression list. If any one of them has hard-bounced, complained, or (for marketing sends) unsubscribed, the whole request is rejected with 422 recipient_suppressed and nothing is sent.

Attachments

Each attachment carries a filename and either content (base64) or url (a URL of a file already uploaded to your account). Arbitrary URLs are not fetched. Together they may total 10 MB once decoded; a bigger request is rejected with 400 attachments_too_large.

Attachments are transactional only: a marketing send is rejected, and so is a send with scheduled_at: the bytes are not stored, so there would be nothing left to attach when the schedule fires. An account configured to send through Gmail gets 400 attachments_unsupported; every other provider carries them.

Example Request

curl -X POST https://buzzzmail.co.za/api/v1/emails \
  -H "Authorization: Bearer bz_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "subject": "Your Order Confirmation",
    "from": "orders@yourdomain.com",
    "fromName": "Your Store",
    "replyTo": "support@yourdomain.com",
    "html": "<h1>Order Confirmed!</h1><p>Your order #12345 is confirmed.</p>"
  }'

Response

{
  "id": "email_abc123xyz",
  "status": "SENT",
  "message": "Email sent successfully"
}
POST/api/v1/emails/batch
Send up to 100 messages in one request

Request Body

{ "emails": [ … ] }: 1 to 100 messages, each exactly the body of a single send. An Idempotency-Key covers the whole batch: a retry replays the whole result array rather than re-sending the messages that already went out.

Results are per item

The response is always 200 with data holding one entry per message, in request order: either { id, status, message } or { error, message, statusCode }. A malformed message, or one whose recipient is suppressed, fails on its own without touching its siblings.

Your monthly allowance is checked once, for the batch as a whole: if the whole batch does not fit, the request is refused with 402 and nothing is sent. Authentication and plan-level refusals are likewise whole-request.

Example Request

curl -X POST https://buzzzmail.co.za/api/v1/emails/batch \
  -H "Authorization: Bearer bz_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      { "to": "a@example.com", "subject": "Receipt", "text": "Thanks!" },
      { "to": "b@example.com", "subject": "Receipt", "text": "Thanks!" }
    ]
  }'

Response

{
  "data": [
    { "id": "email_abc123", "status": "SENT", "message": "Email sent successfully" },
    {
      "error": "recipient_suppressed",
      "message": "The recipient address b@example.com is suppressed…",
      "statusCode": 422
    }
  ]
}
GET/api/v1/emails/{id}
Retrieve one email with its delivery timeline

Timeline

events lists what happened to the message, oldest first: created, scheduled, sent, delivered, bounced, complained, failed, cancelled. Only moments that actually occurred appear; bounced and failed carry the provider's reason. There are no open or click events here: mail sent through this API is not tracked, only campaign mail is. 404 if the email was not sent by this API key.

Response

{
  "id": "email_abc123xyz",
  "toEmail": "customer@example.com",
  "subject": "Your Order Confirmation",
  "status": "SENT",
  "tags": ["order"],
  "metadata": { "order_id": "12345" },
  "events": [
    { "type": "created", "created_at": "2026-09-05T12:00:00.000Z" },
    { "type": "sent", "created_at": "2026-09-05T12:00:01.000Z" },
    { "type": "delivered", "created_at": "2026-09-05T12:00:05.000Z" }
  ]
}
DELETE/api/v1/emails/{id}
Cancel a scheduled email before it fires

Responses

200 while the record is still SCHEDULED and unclaimed; it moves to CANCELLED and stops counting against your monthly allowance. 409 once the worker has picked it up: it may already be with the provider, so cancellation cannot be promised. 404 if this API key never sent it.

Example Request

curl -X DELETE https://buzzzmail.co.za/api/v1/emails/email_abc123xyz \
  -H "Authorization: Bearer bz_your_api_key"

List Emails

GET/api/v1/emails
Retrieve a paginated list of emails sent via your API key

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber50Items per page (max 100)
statusstring-Filter: PENDING, SCHEDULED, SENT, FAILED, BOUNCED, CANCELLED
tagstring-Only records carrying this tag

Example Request

curl "https://buzzzmail.co.za/api/v1/emails?page=1&limit=10&status=SENT" \
  -H "Authorization: Bearer bz_your_api_key"

Response

{
  "emails": [
    {
      "id": "email_abc123",
      "fromEmail": "you@yourdomain.com",
      "fromName": "Your Company",
      "toEmail": "customer@example.com",
      "subject": "Order Confirmation",
      "status": "SENT",
      "createdAt": "2025-12-19T10:30:00.000Z",
      "sentAt": "2025-12-19T10:30:01.000Z",
      "errorMsg": null
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 156,
    "totalPages": 16
  }
}

Send SMS

POST/api/v1/sms
Send a single SMS message to one recipient

Request Body

FieldTypeRequiredDescription
tostringRequiredRecipient phone number. Stored in E.164; a number that cannot be resolved to E.164 is rejected as a bad request.
bodystringRequiredMessage text (max 1600 characters)
senderIdstringOptionalAlphanumeric sender ID (max 20 characters), where your provider allows one
categorystringOptionalmarketing (default) or transactional. See below.
scheduled_atstringOptionalISO 8601 datetime, in the future and at most 72 hours ahead. See Scheduling below.
tagsstring[]OptionalUp to 20 labels (64 characters each), returned on reads and filterable with ?tag=
metadataobjectOptionalUp to 50 string key/value pairs (values max 1024 characters), stored verbatim and returned on reads

Transactional vs. marketing

Unlike the email endpoint, this one defaults to marketing: the stricter setting. A marketing SMS is never delivered to a number that replied STOP, nor to one the carrier has blocked. If you never send the field, nothing about your integration changes.

Send one-time passcodes, alerts and other messages the recipient is expecting with "category": "transactional". Transactional narrows which opt-outs apply; it does not turn opt-out enforcement off. A voluntary STOP no longer blocks the send, because withdrawing consent to promotions is not a request to be locked out of your own account. A carrier-level block still refuses it: the carrier will not deliver the message whatever you label it.

Transactional sends require the transactional_sms entitlement on your account. Without it the request is refused rather than quietly downgraded, so a misconfigured integration fails loudly instead of dropping passcodes. Labelling promotional content as transactional is a Terms of Service violation and, for SMS to numbers that opted out, a legal exposure under the TCPA.

Scheduling and cancellation

With scheduled_at the request returns 202 and a record in status SCHEDULED instead of sending immediately. The allowance is committed when the send is accepted, not when it fires, and entitlements are re-checked at fire time: a plan that shrank in the meantime refuses the send rather than exceeding it.

DELETE /api/v1/sms/{id} cancels it. Before the worker picks the message up you get 200 and the record moves to CANCELLED, which is not counted against your allowance. Once it has been picked up you get 409: it may already be with the provider, so no promise can be made about stopping it.

Idempotency

Send an Idempotency-Key header (up to 255 characters of A-Z a-z 0-9 _ - : .; a UUID or ULID does nicely) to make a retry safe. Within 24 hours a repeat of the same key returns the first response verbatim (same id, same status), and sends nothing further.

A key whose first request is still in flight returns 409 idempotency_key_in_use; retry in a moment for the stored result. Keys pin only successful requests: one that was refused or failed releases its key, so a corrected retry is a fresh request.

Opt-outs

Replies of STOP, UNSUBSCRIBE and the other carrier-standard keywords are recorded against your account, and START re-enables the number. A send that the opt-out list refuses returns 422 recipient_opted_out: the message names the reason, which differs by category, and is not counted against your monthly allowance.

Example Request

curl -X POST https://buzzzmail.co.za/api/v1/sms \
  -H "Authorization: Bearer bz_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+27821234567",
    "body": "Your verification code is 481920.",
    "category": "transactional"
  }'

Response

{
  "id": "sms_abc123xyz",
  "status": "SENT",
  "segments": 1,
  "message": "SMS sent successfully"
}
DELETE/api/v1/sms/{id}
Cancel a scheduled SMS before it fires

Responses

200 while the record is still SCHEDULED and unclaimed; it moves to CANCELLED and stops counting against your monthly allowance. 409 once the worker has picked it up: it may already be with the provider, so cancellation cannot be promised. 404 if this API key never sent it.

Example Request

curl -X DELETE https://buzzzmail.co.za/api/v1/sms/sms_abc123xyz \
  -H "Authorization: Bearer bz_your_api_key"

Contacts, Lists, Templates, Domains & Campaigns

Everything the dashboard manages is available over the API, scoped to the organization the key belongs to. These endpoints require a key with full permission. Field names are snake_case, and list endpoints paginate with ?page= and ?limit= like GET /api/v1/emails.

MethodPathDescription
GET/api/v1/contactsList contacts; filter with ?email=, ?list_id=, ?status=
POST/api/v1/contactsCreate a contact
POST/api/v1/contacts/bulkCreate or update up to 500 contacts by email
GET/api/v1/contacts/{id}Retrieve a contact
PATCH/api/v1/contacts/{id}Update a contact
DELETE/api/v1/contacts/{id}Delete a contact
GET/api/v1/listsList contact lists
POST/api/v1/listsCreate a contact list
GET/api/v1/lists/{id}Retrieve a list
PATCH/api/v1/lists/{id}Update a list
DELETE/api/v1/lists/{id}Delete a list (refused while a campaign uses it)
GET/api/v1/lists/{id}/membersList the contacts on a list
POST/api/v1/lists/{id}/membersAdd contacts to a list
DELETE/api/v1/lists/{id}/membersRemove contacts from a list
GET/api/v1/templatesList templates
POST/api/v1/templatesCreate a template (HTML is sanitized; `removed` reports what was stripped)
GET/api/v1/templates/{id}Retrieve a template
PATCH/api/v1/templates/{id}Update a template
DELETE/api/v1/templates/{id}Delete a template
GET/api/v1/domainsList sending domains with the DNS records they need
POST/api/v1/domainsRegister a sending domain
POST/api/v1/domains/{id}/verifyRe-check verification against SES
DELETE/api/v1/domains/{id}Remove a sending domain
POST/api/v1/campaignsCreate a campaign
GET/api/v1/campaigns/{id}Campaign detail, status and stats
POST/api/v1/campaigns/{id}/sendQueue a campaign for sending
POST/api/v1/eventsReport an event for a contact, starting any automation that listens for it
GET/api/v1/inboundList received messages (cursor paginated: ?cursor=, ?limit=, ?recipient=)
GET/api/v1/inbound/{id}Retrieve one received message with its bodies and attachments
GET/api/v1/surveysList surveys with their questions and response counts
GET/api/v1/surveys/{id}/responsesList a survey's responses (cursor paginated, oldest first: ?cursor=, ?limit=, ?since=)
GET/api/v1/webhooksList registered webhook endpoints
POST/api/v1/contacts
Create a contact. Use /api/v1/contacts/bulk to create or update up to 500 by email in one request.
curl -X POST https://buzzzmail.co.za/api/v1/contacts \
  -H "Authorization: Bearer bz_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "list_ids": ["list_abc123"],
    "metadata": { "plan": "pro" }
  }'
POST/api/v1/campaigns/{id}/send
Queues a campaign to its list. Answers 202 with the recipient count, or 402 when the send would exceed the plan's monthly allowance.
curl -X POST https://buzzzmail.co.za/api/v1/campaigns/campaign_abc123/send \
  -H "Authorization: Bearer bz_your_api_key"

Webhooks

Register an HTTPS endpoint under Settings → Webhooks, pick the events you want, and copy the signing secret (it is shown once). Deliveries are retried with exponential backoff and every attempt is visible in the dashboard, where a failed delivery can be replayed.

Event types

email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.suppressed, email.received, sms.sent, sms.delivered, sms.failed, sms.undelivered, sms.opt_out, contact.created, contact.updated, contact.unsubscribed, survey.responded

{{survey_url:<slug>}} is resolved in campaign and automation mail only, not in /api/v1/emails sends.

Request

POST https://your-app.example.com/hooks/buzzz
Content-Type: application/json
buzzz-event: email.bounced
buzzz-signature: t=1767225600,v1=9f8c...
{
  "type": "email.bounced",
  "created_at": "2026-01-01T00:00:00.000Z",
  "data": {
    "email_id": "cm5x...",
    "email": "recipient@example.com",
    "bounce_type": "Permanent",
    "tags": ["order"],
    "metadata": { "order_id": "12345" }
  }
}

Every email.* event about a message sent through /api/v1/emails carries that message's tags and metadata, so you can correlate a delivery without a follow-up request. Campaign mail has neither, and both fields are absent there.

Verifying the signature

buzzz-signature ist=<unix seconds>,v1=<hex HMAC-SHA256>over ${t}.${rawBody}, keyed by your endpoint secret. Verify against the raw body, before any JSON parsing, and reject timestamps older than about five minutes so a captured request cannot be replayed. During a secret rotation the header may carry two v1= values (signed with the old and new secret); accept the header if either one matches.

import crypto from "crypto";

function verifyBuzzzWebhook(secret, header, rawBody, toleranceSeconds = 300) {
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(",")) {
    const [name, value] = part.split("=", 2);
    if (name === "t") timestamp = Number(value);
    if (name === "v1") signatures.push(value);
  }
  if (!Number.isFinite(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  return signatures.some((sig) => {
    const b = Buffer.from(sig);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

// Express: express.raw({ type: "application/json" }) keeps the body verbatim.
app.post("/hooks/buzzz", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyBuzzzWebhook(process.env.BUZZZ_WEBHOOK_SECRET, req.get("buzzz-signature"), raw)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  console.log(event.type, event.data);
  res.sendStatus(200);
});

The Node SDK exposes the same check as verifyWebhookSignature from @buzzzmail/node.

Delivery semantics

  • • Respond 2xx to acknowledge. Anything else is retried.
  • • Up to 5 attempts with exponential backoff; the last failure is recorded and replayable from the dashboard.
  • • At-least-once. Events are deduplicated per (endpoint, event, entity), but a retry after a timed-out 2xx can still repeat, so make your handler idempotent.

SDKs & Code Examples

OpenAPI specification and SDK

The machine-readable description of this API, and the Node client generated from it

Every endpoint on this page is described by an OpenAPI 3.1 document, served unauthenticated so a code generator can read it without a key:

curl https://buzzzmail.co.za/api/v1/openapi.json

The document is generated from the route definitions themselves, and each operation records the key scope it needs in an x-permission extension (full or sending). Point openapi-typescript, openapi-generator or any other generator at it to build a client in your own language.

The official Node client is generated from that same document, and defaults to https://buzzzmail.co.za:

npm install @buzzzmail/node

import { Buzzz } from "@buzzzmail/node";

const buzzz = new Buzzz({ apiKey: process.env.BUZZZ_API_KEY });

const email = await buzzz.emails.send(
  { to: "customer@example.com", subject: "Your receipt", html: "<p>Thanks.</p>" },
  { idempotencyKey: `receipt-${orderId}` }
);
// Using fetch (Node.js 18+)
const response = await fetch('https://buzzzmail.co.za/api/v1/emails', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.BUZZZ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: 'user@example.com',
    subject: 'Hello from Node.js!',
    html: '<h1>Hello!</h1><p>Sent via Buzzz API</p>',
  }),
});

const data = await response.json();
console.log(data.id); // email_abc123

Platform

Visual designer & templates

Dashboard → Templates has a block-based email designer: drag hero, header, text, image, button, card, columns, divider, spacer, social, footer, HTML, video and countdown blocks onto a 600px canvas, or start from the template gallery. Prefer raw markup? A hand-written "code template" is sanitized on save (scripts, frames, forms and event handlers are stripped) and can be converted into blocks with one click, so it drops into the same designer, undo stack and preview.

AI assist & composer

"Suggest subjects" and "Rewrite" (shorten, lengthen, friendlier, formal, punchier, fix grammar) sit on the campaign editor and the designer's text blocks. The AI Composer takes a brief and a conversation and drafts a whole newsletter straight into the designer as blocks, not HTML. Only your own copy is sent to the model (no credentials, no contact data). Each request costs one AI credit (free plan: 20 a month); a failed call never spends one.

MCP connector

Buzzz exposes an MCP server for account context, dashboard stats and history, campaigns, templates, and contact lists & contacts. Add it as a custom connector in claude.ai or Claude Desktop at https://buzzzmail.co.za/api/mcp (Buzzz implements the MCP authorization spec: OAuth 2.1 + PKCE with dynamic client registration), or point a client that takes a static header at the same URL with an API key. From Claude Code:

claude mcp add --transport http buzzz https://buzzzmail.co.za/api/mcp \
  --header "Authorization: Bearer bz_..."

Inbound email receiving

Turn on receiving for a verified sending domain at Settings → Domains ("Enable receiving") and publish the MX record it shows you; incoming mail then lands at Dashboard → Inbound and is readable via GET /api/v1/inbound / GET /api/v1/inbound/{id} with a full-permission key. Attachment URLs are presigned and expire (url_expires_at). Every message fires an email.received webhook. Only available in the AWS regions where SES itself can receive mail.

Error Codes

StatusErrorDescription
400Bad RequestMissing required fields or invalid data format
401UnauthorizedMissing or invalid API key
403ForbiddenAPI key lacks required permissions
403from_domain_unverifiedThe from address is not on one of your verified sending domains. Verify the domain first, or omit from to use your default sender.
409domain_claimedAdding a sending domain another Buzzz account already holds. If you own it, email support@buzzzmail.co.za.
403sending_pausedEmail sending is paused because your hard-bounce or complaint rate crossed the pause level (4% or 0.08% over 7 days; the owners are emailed a warning at 2% or 0.05%). Nothing is sent until support reinstates it; appeal to support@buzzzmail.co.za and a person replies within one business day.
404Not FoundResource doesn't exist
429Too Many RequestsRate limit exceeded. Wait and retry.
500Internal ErrorServer error. Please retry or contact support.

Error Response Format

{
  "error": "Bad Request",
  "message": "Missing required field: to",
  "statusCode": 400
}

Need an API key?

Create an account, then copy your key from the dashboard.