Buzzz API Reference
REST endpoints, webhooks, and SDK examples for transactional email.
Quick Start
Sign up and get your API key from the dashboard
Send a POST request to the emails endpoint
curl -X POST https://api.buzzz.email/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_hereKeys 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/emails | Send or schedule an email |
| POST | /api/v1/emails/batch | Send 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/sms | Send 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
/api/v1/emailsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | Required | Recipient email address |
| subject | string | Required | Email subject line |
| html | string | html or text | HTML body content |
| text | string | html or text | Plain text body content |
| from | string | Optional | Sender email (must be verified) |
| fromName | string | Optional | Sender display name |
| replyTo | string | Optional | Reply-to address |
| cc | string | Optional | CC recipients (comma-separated) |
| bcc | string | Optional | BCC recipients (comma-separated) |
| category | string | Optional | transactional (default) or marketing. See below. |
| scheduled_at | string | Optional | ISO 8601 datetime, in the future and at most 72 hours ahead. See Scheduling below. |
| tags | string[] | Optional | Up to 20 labels (64 characters each), returned on reads and filterable with ?tag= |
| metadata | object | Optional | Up to 50 string key/value pairs (values max 1024 characters), stored verbatim and returned on reads |
| attachments | object[] | Optional | Up 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://api.buzzz.email/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"
}/api/v1/emails/batchRequest 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://api.buzzz.email/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
}
]
}/api/v1/emails/{id}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" }
]
}/api/v1/emails/{id}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://api.buzzz.email/api/v1/emails/email_abc123xyz \
-H "Authorization: Bearer bz_your_api_key"List Emails
/api/v1/emailsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number |
| limit | number | 50 | Items per page (max 100) |
| status | string | - | Filter: PENDING, SCHEDULED, SENT, FAILED, BOUNCED, CANCELLED |
| tag | string | - | Only records carrying this tag |
Example Request
curl "https://api.buzzz.email/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
/api/v1/smsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | Required | Recipient phone number. Stored in E.164; a number that cannot be resolved to E.164 is rejected as a bad request. |
| body | string | Required | Message text (max 1600 characters) |
| senderId | string | Optional | Alphanumeric sender ID (max 20 characters), where your provider allows one |
| category | string | Optional | marketing (default) or transactional. See below. |
| scheduled_at | string | Optional | ISO 8601 datetime, in the future and at most 72 hours ahead. See Scheduling below. |
| tags | string[] | Optional | Up to 20 labels (64 characters each), returned on reads and filterable with ?tag= |
| metadata | object | Optional | Up 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://api.buzzz.email/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"
}/api/v1/sms/{id}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://api.buzzz.email/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.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/contacts | List contacts; filter with ?email=, ?list_id=, ?status= |
| POST | /api/v1/contacts | Create a contact |
| POST | /api/v1/contacts/bulk | Create 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/lists | List contact lists |
| POST | /api/v1/lists | Create 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}/members | List the contacts on a list |
| POST | /api/v1/lists/{id}/members | Add contacts to a list |
| DELETE | /api/v1/lists/{id}/members | Remove contacts from a list |
| GET | /api/v1/templates | List templates |
| POST | /api/v1/templates | Create 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/domains | List sending domains with the DNS records they need |
| POST | /api/v1/domains | Register a sending domain |
| POST | /api/v1/domains/{id}/verify | Re-check verification against SES |
| DELETE | /api/v1/domains/{id} | Remove a sending domain |
| POST | /api/v1/campaigns | Create a campaign |
| GET | /api/v1/campaigns/{id} | Campaign detail, status and stats |
| POST | /api/v1/campaigns/{id}/send | Queue a campaign for sending |
| POST | /api/v1/events | Report an event for a contact, starting any automation that listens for it |
| GET | /api/v1/inbound | List received messages (cursor paginated: ?cursor=, ?limit=, ?recipient=) |
| GET | /api/v1/inbound/{id} | Retrieve one received message with its bodies and attachments |
/api/v1/contactscurl -X POST https://api.buzzz.email/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" }
}'/api/v1/campaigns/{id}/sendcurl -X POST https://api.buzzz.email/api/v1/campaigns/campaign_abc123/send \
-H "Authorization: Bearer bz_your_api_key"Webhooks
Delivery events are tracked automatically for every email you send. Buzzz receives delivery receipts, bounces, and complaints from the sending infrastructure and updates your email and contact statuses in real time — no setup required on your side.
Supported Events
| Event | Description | Action Taken |
|---|---|---|
| delivered | Email successfully delivered | Email status → SENT |
| bounce | Email bounced (hard or soft) | Contact status → BOUNCED |
| complaint | Recipient marked as spam | Contact status → COMPLAINED |
| open | Email was opened | Campaign opens +1 |
| click | Link was clicked | Campaign clicks +1 |
| unsubscribe | Recipient unsubscribed | Contact status → UNSUBSCRIBED |
Automatic Contact Management
When a bounce or complaint webhook is received, Buzzz automatically updates the contact's status and excludes them from future sends. This protects your sender reputation.
SDKs & Code Examples
// Using fetch (Node.js 18+)
const response = await fetch('https://api.buzzz.email/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_abc123Error Codes
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | Missing required fields or invalid data format |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | API key lacks required permissions |
| 404 | Not Found | Resource doesn't exist |
| 429 | Too Many Requests | Rate limit exceeded. Wait and retry. |
| 500 | Internal Error | Server 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.