Documentation menu

API reference

Webhooks

Webhooks post signed JSON events to your HTTPS endpoint when something happens to your emails or domains. These endpoints manage webhooks and let you inspect and replay every delivery.

All endpoints need a full-access key whose creator holds the API controls privilege in the console. They can also return the common errors listed in the overview. For receiving and verifying events step by step see Webhooks.

Libraries and CLI
Endpoint Node.js Python CLI
GET /webhookswebhooks.listwebhooks.listtsmart webhooks list
POST /webhookswebhooks.createwebhooks.createtsmart webhooks create
GET /webhooks/{id}webhooks.getwebhooks.gettsmart webhooks get
PATCH /webhooks/{id}webhooks.updatewebhooks.updatetsmart webhooks update
DELETE /webhooks/{id}webhooks.removewebhooks.removetsmart webhooks delete
POST /webhooks/{id}/signing-secret/rotatewebhooks.rotateSecretwebhooks.rotate_secrettsmart webhooks rotate-secret
POST /webhooks/{id}/testwebhooks.testwebhooks.testtsmart webhooks test
GET /webhooks/{id}/eventswebhooks.events.listwebhooks.events.listtsmart webhooks events
GET /webhooks/{id}/events/{event_id}webhooks.events.getwebhooks.events.gettsmart webhooks event
GET …/events/{event_id}/attemptswebhooks.events.attemptswebhooks.events.attemptstsmart webhooks attempts
POST …/events/{event_id}/replaywebhooks.events.replaywebhooks.events.replaytsmart webhooks replay

The webhook object

Name Type Description
object string "webhook" (single webhook responses only).
id string Webhook id (UUID).
endpoint string The HTTPS URL events are posted to.
events string[] The event types this webhook receives.
status string enabled or disabled. A disabled webhook receives nothing. Events already queued for it wait, but events that happen while it is disabled are not recorded for it and are never delivered.
created_at string When the webhook was created.
signing_secret string The secret that signs every delivery, whsec_ followed by base64.

A company can have at most 10 webhooks. The endpoint must be:

  • an https:// URL of at most 2,000 characters, on the default port, 443 or 8443, without a user name or password;
  • a public address: private and internal IP addresses and host names without a dot (or ending in .local, .internal, .lan and similar) are refused, and deliveries to a name that resolves to a private address fail.

Event types

TypeSent when
email.sentAn email was handed to the mail server.
email.deliveredA recipient's server accepted it (one event per recipient).
email.delivery_delayedDelivery to a recipient failed temporarily and is being retried.
email.bouncedDelivery to a recipient failed.
email.complainedA recipient marked the email as spam and their mailbox provider sent us a complaint report (see Spam complaints).
email.failedThe email could not be sent.
email.openedThe email was opened (open tracking on).
email.clickedA tracked link was clicked (click tracking on).
email.scheduledAn email was accepted with scheduled_at.
email.suppressedA recipient was skipped because it is on the suppression list.
domain.createdA domain was added through the API.
domain.updatedA domain's status changed on POST /domains/{id}/verify, or its open or click tracking was set (console or API).
domain.deletedA domain was removed through the API.

Domains added, verified or removed in the console, and status changes found by the platform's periodic re-check, send no domain events.

Deliveries

Each event is a POST to your endpoint with a JSON body and these headers:

HeaderValue
content-typeapplication/json
user-agenttsmartSend-Webhooks/1.0
webhook-idThe event id (msg_…), the same on every retry and replay: use it to ignore duplicates.
webhook-timestampUnix time in seconds when this attempt was signed.
webhook-signaturev1, followed by the base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed with the base64-decoded part of the signing secret after whsec_.

The same values are also sent under an alternative header prefix for compatibility with existing verification libraries.

A delivery succeeds when your endpoint answers with a 2xx status within 10 seconds; redirects are not followed. A failed delivery is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours (8 attempts in all). After 100 failed attempts in a row the webhook is disabled. The events already queued for it are held, but events that happen while it is disabled are not recorded for it and are never delivered. Enabling it again with PATCH /webhooks/{id} resets the count and delivers the held events at once. Events are kept for 30 days after they were created, both finished events and events still held by a disabled webhook.

Node.js: verify a delivery
import { createServer } from "node:http";
import { verifyWebhook, WebhookVerificationError } from "@tsmart/sdk";

createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks); // verify the exact bytes received, before any JSON parsing
  try {
    const event = verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.TSMART_WEBHOOK_SECRET });
    console.log(event.type, event.data);
    res.writeHead(200).end();
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.writeHead(400).end();
    console.error(err);
    res.writeHead(500).end();
  }
}).listen(3000);

The Python SDK has the same check as verify_webhook. Both reject timestamps more than 5 minutes from now.

Payloads

Email events:

email.bounced
{
  "type": "email.bounced",
  "created_at": "2026-10-12T09:15:04.999Z",
  "data": {
    "created_at": "2026-10-12T09:15:02.431Z",
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fa6f60",
    "from": "Acme <hello@yourdomain.com>",
    "to": ["jane@example.com"],
    "subject": "Welcome to Acme",
    "tags": { "category": "welcome" },
    "message_id": "<4ef9a417-02e9-4d39-ad75-9611e0fa6f60@yourdomain.com>",
    "bounce": {
      "type": "Permanent",
      "subType": "General",
      "message": "550 5.1.1 User unknown"
    }
  }
}
Name Type Description
type string The event type.
created_at string When the event happened (ISO 8601).
data.created_at string When the email was accepted.
data.email_id string The email id.
data.from, data.subject string As sent.
data.to string[] The recipient the event is about (delivered, delivery_delayed, bounced, suppressed, complained); otherwise the email's to addresses. For complained it is empty when the mailbox provider did not say which of several recipients complained.
data.tags object The email's tags as name: value.
data.message_id string The Message-ID header, once the email was handed to the mail server.
data.bounce object email.bounced (type Permanent) and email.delivery_delayed (type Temporary): type, subType ("General") and the receiving server's message.
data.open object email.opened: ipAddress, timestamp, userAgent.
data.click object email.clicked: ipAddress, link, timestamp, userAgent.
data.failed object email.failed: reason.
data.suppressed object email.suppressed: message and type, the reason the address is on the suppression list ("bounce", "complaint" or "manual").

Domain events:

domain.updated
{
  "type": "domain.updated",
  "created_at": "2026-10-12T09:41:20.118Z",
  "data": {
    "id": "a3b1c5d2-7e8f-4a6b-9c0d-1e2f3a4b5c6d",
    "name": "yourdomain.com",
    "status": "verified",
    "created_at": "2026-10-12T09:30:11.402Z"
  }
}

GET /webhooks

Lists the company's webhooks, newest first, with their signing secrets.

Permission: full access; the key's creator needs the API controls privilege.

Query

Name Type Description
limit integer 1 to 100, default 20.
after string A webhook id: return older webhooks.
before string A webhook id: return newer webhooks. Use either after or before.

Request

cURL
curl https://send.tsmart.ai/webhooks \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
      "endpoint": "https://hooks.yourdomain.com/tsmart",
      "events": ["email.delivered", "email.bounced"],
      "status": "enabled",
      "created_at": "2026-10-11 16:20:44.071392+00",
      "signing_secret": "whsec_…"
    }
  ]
}

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
422validation_errorlimit out of range, both after and before, or an invalid cursor.

POST /webhooks

Creates a webhook and returns its signing secret.

Permission: full access; the key's creator needs the API controls privilege.

Body

Name Type Description
endpoint required string Public HTTPS URL that receives the events (see the endpoint rules above).
events required string[] 1 to 20 event types from the list above. Duplicates are ignored.

Request

cURL
curl -X POST https://send.tsmart.ai/webhooks \
  -H "Authorization: Bearer $TSMART_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://hooks.yourdomain.com/tsmart",
    "events": ["email.delivered", "email.bounced"]
  }'
Node.js
const { data, error } = await tsmart.webhooks.create({
  endpoint: "https://hooks.yourdomain.com/tsmart",
  events: ["email.delivered", "email.bounced"],
});
Python
webhook = client.webhooks.create({
    "endpoint": "https://hooks.yourdomain.com/tsmart",
    "events": ["email.delivered", "email.bounced"],
})

Response

200 OK
{
  "object": "webhook",
  "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
  "signing_secret": "whsec_…"
}

The new webhook is enabled.

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
403validation_errorThe company already has 10 webhooks.
422missing_required_fieldendpoint or events is missing.
422validation_error"`endpoint` must be a valid URL" (also for a port other than 443 or 8443, or a URL with a user name or password), "`endpoint` must use https", "`endpoint` must be a public address", or "`events` must list one or more supported event types".

GET /webhooks/{id}

Returns one webhook with its signing secret.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Request

cURL
curl https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook",
  "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
  "endpoint": "https://hooks.yourdomain.com/tsmart",
  "events": ["email.delivered", "email.bounced"],
  "status": "enabled",
  "created_at": "2026-10-11T16:20:44.071Z",
  "signing_secret": "whsec_…"
}

Here created_at is ISO 8601, unlike in the list.

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company ("Webhook not found").

PATCH /webhooks/{id}

Changes a webhook's endpoint, events or status.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Body

Name Type Description
endpoint string A new public HTTPS URL.
events string[] The new list of event types (1 to 20); it replaces the old list.
status string enabled or disabled. Disabling holds the events already queued; events that occur while disabled are not delivered. Enabling resets the failure count and delivers the held events at once.

Omitted fields keep their value.

Request

cURL
curl -X PATCH https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e \
  -H "Authorization: Bearer $TSMART_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "events": ["email.delivered", "email.bounced", "email.failed"], "status": "enabled" }'

Response

200 OK
{
  "object": "webhook",
  "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e"
}

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company.
422validation_errorAn invalid endpoint or events (the same messages as on create), or "`status` must be `enabled` or `disabled`".

DELETE /webhooks/{id}

Deletes a webhook. Nothing more is delivered to it.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Request

cURL
curl -X DELETE https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook",
  "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
  "deleted": true
}

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company.

POST /webhooks/{id}/signing-secret/rotate

Replaces the webhook's signing secret.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Request

cURL
curl -X POST https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/signing-secret/rotate \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook",
  "id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
  "signing_secret": "whsec_…"
}

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company.

POST /webhooks/{id}/test

Queues an example email.sent event for this webhook, so you can check your receiver.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Request

cURL
curl -X POST https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/test \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook_event",
  "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL"
}

The event is delivered within seconds, signed and retried like any other, whether or not the webhook subscribes to email.sent. Follow it with GET …/events/{event_id}. Its payload:

Test payload
{
  "type": "email.sent",
  "created_at": "2026-10-12T10:02:09.513Z",
  "data": {
    "created_at": "2026-10-12T10:02:09.513Z",
    "email_id": "00000000-0000-4000-8000-000000000000",
    "from": "Example <hello@example.com>",
    "to": ["someone@example.com"],
    "subject": "Test event",
    "tags": { "test": "true" }
  }
}

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company.
422validation_errorThe webhook is disabled; enable it first.
429rate_limit_exceeded5 test events of this webhook are still waiting to be delivered. Replay one of them to try it now.

GET /webhooks/{id}/events

Lists the events delivered, or being delivered, to this webhook, newest first.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.

Query

Name Type Description
limit integer 1 to 100, default 20.
after string An event id (msg_…) of this webhook: return older events.
before string An event id of this webhook: return newer events. Use either after or before.

Request

cURL
curl "https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/events?limit=2" \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL",
      "type": "email.bounced",
      "created_at": "2026-10-12 09:15:05.120371+00",
      "status": "attempting"
    },
    {
      "id": "msg_7pR3sV0wX6yA1bC4dE9fG2hJ5kL",
      "type": "email.delivered",
      "created_at": "2026-10-12 08:47:19.884602+00",
      "status": "success"
    }
  ]
}

status is success (delivered), failed (all retries used), attempting (failed so far, retries left) or pending (not tried yet).

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo webhook with this id in your company.
422validation_errorlimit out of range, both after and before, or a cursor that is not an event of this webhook.

GET /webhooks/{id}/events/{event_id}

Returns one event with the payload that was (or will be) delivered.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.
event_id required string The event id (msg_…).

Request

cURL
curl https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/events/msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook_event",
  "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL",
  "webhook_id": "c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e",
  "type": "email.bounced",
  "created_at": "2026-10-12 09:15:05.120371+00",
  "status": "attempting",
  "attempts": 2,
  "next_attempt_at": "2026-10-12 09:20:10.402233+00",
  "payload": {
    "type": "email.bounced",
    "created_at": "2026-10-12T09:15:04.999Z",
    "data": {
      "created_at": "2026-10-12T09:15:02.431Z",
      "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fa6f60",
      "from": "Acme <hello@yourdomain.com>",
      "to": ["jane@example.com"],
      "subject": "Welcome to Acme",
      "tags": { "category": "welcome" },
      "message_id": "<4ef9a417-02e9-4d39-ad75-9611e0fa6f60@yourdomain.com>",
      "bounce": { "type": "Permanent", "subType": "General", "message": "550 5.1.1 User unknown" }
    }
  }
}
Name Type Description
status string success, failed, attempting or pending, as in the list.
attempts integer How many delivery attempts were made.
next_attempt_at string | null When the next attempt is due; null when none is planned (delivered, out of retries, or the webhook is disabled).
payload object The JSON body that is posted to your endpoint.

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo such event for this webhook in your company ("Webhook event not found").

GET /webhooks/{id}/events/{event_id}/attempts

Lists the delivery attempts of one event, newest first. The last 50 attempts are kept.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.
event_id required string The event id (msg_…).

Query

Name Type Description
limit integer 1 to 100, default 20.
after string An attempt id: return older attempts.
before string An attempt id: return newer attempts. Use either after or before.

Request

cURL
curl https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/events/msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL/attempts \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL_2",
      "http_status_code": 503,
      "response": "Service Unavailable",
      "error": "HTTP 503",
      "duration_ms": 214,
      "sent_at": "2026-10-12T09:15:10.402Z"
    },
    {
      "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL_1",
      "http_status_code": null,
      "response": "",
      "error": "ECONNREFUSED",
      "duration_ms": 37,
      "sent_at": "2026-10-12T09:15:05.311Z"
    }
  ]
}
Name Type Description
id string <event id>_<attempt number>.
http_status_code integer | null Your endpoint's status code; null when no answer was received.
response string The first 500 characters of your endpoint's answer, with the endpoint's own path and query removed.
error string | null null for a successful attempt; otherwise "HTTP <status>" or a connection problem such as a network error code or "timeout".
duration_ms integer How long the attempt took.
sent_at string When the attempt finished (ISO 8601).

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo such event for this webhook in your company.
422validation_errorlimit out of range, both after and before, or a cursor that is not a stored attempt of this event.

POST /webhooks/{id}/events/{event_id}/replay

Delivers an event again, now.

Permission: full access; the key's creator needs the API controls privilege.

Path

Name Type Description
id required string The webhook id.
event_id required string The event id (msg_…).

Request

cURL
curl -X POST https://send.tsmart.ai/webhooks/c7d2e9f0-3a18-4b6c-9e27-5f0a1b2c3d4e/events/msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL/replay \
  -H "Authorization: Bearer $TSMART_API_KEY"

Response

200 OK
{
  "object": "webhook_event",
  "id": "msg_2nV8kQ1rT5xZ0bY7cW3dE9fH4jL"
}

Any event can be replayed, whatever its status: it is queued again and tried at once (its status shows attempting, or pending if it was never tried), with the same id and payload, so your receiver can recognise a duplicate by webhook-id. If the replay fails, the event continues its retry schedule where it left off; an event that had already used all its retries is marked failed again.

Errors

StatusNameWhen
401restricted_api_keyA sending-access key.
403invalid_permissionThe key's creator lacks the API controls privilege.
404not_foundNo such event for this webhook in your company.
422validation_errorThe webhook is disabled; enable it before replaying events.