Skip to content
SecureThreadDevelopers

Event webhooks

Signed calls when something happens

SecureThread can call your HTTPS endpoint when a provider accepts or refuses a message, an approval is decided, a campaign pauses, or a mailbox needs reconnecting. Every call is signed, and every payload carries identifiers only.

Signed with Standard Webhooks

Each call carries a webhook-id, a timestamp and an HMAC-SHA256 signature. Your endpoint checks them against the signing secret before it acts on a call.

Events for each part of the work

Messages, approvals, campaigns, schedules, mailboxes and AI assistant send requests. Subscribe to exact event types or to a pattern such as message.*.

Identifiers, never content

Payloads carry identifiers. They never carry message content or email addresses.

Retries over about two days

A call that gets no 2xx answer is tried again, with eight attempts in all, before it is marked failed. Calls stay listed for 30 days, and a finished call can be replayed.

Workspace-wide by design

An endpoint gets events for every member’s messages, not only its creator’s. Only administrators can add endpoints.

Accepted is not the same as reached

message.accepted means the provider took responsibility for the message. It does not mean the message reached anyone, and SecureThread never reports that it did.

Explore

Every event, and the call it makes

Pick an event type to see when it is sent and exactly what reaches your endpoint.

16 event types

message

approval

campaign

schedule

mailbox

assistant

webhook

message.accepted: The provider accepted a message (not confirmation it reached anyone).

The call to your endpointHTTP
POST /your-endpointwebhook-id: msg_<unique id>webhook-timestamp: 1792920600webhook-signature: v1,<base64 HMAC-SHA256>Content-Type: application/json {  "type": "message.accepted",  "timestamp": "2026-10-25T09:30:00.000Z",  "workspaceId": "0192f0a0-0000-7000-8000-000000000001",  "data": { "messageId": "0192f0a0-0000-7000-8000-0000000000aa" }}

The data object holds the event's identifiers, never message content or email addresses. Check the signature on the raw body before acting on a call.

01

Endpoint rules

Administrators, meaning anyone with Manage settings, set up webhooks under Administration › Webhooks. Your plan must include webhooks. Each endpoint must follow these rules.

  • Use https only, on the standard port (443), with a domain name rather than an IP address.
  • Leave out any user name or password in the URL. A query string is allowed.
  • The name must resolve only to public addresses. SecureThread connects to the address it checked, verifies your TLS certificate and does not follow redirects.
  • Up to 10 endpoints per workspace, each subscribed to the events you choose.

02

Event types

Event names are exact. Each type is sent for the event described in its When column. An endpoint can subscribe to single types or to a pattern such as message.*. message.accepted means the provider took responsibility for the message. It is not confirmation that the message reached anyone.

message.accepted

When

The provider accepted a message (not confirmation it reached anyone)

message.failed

When

A message could not be sent

message.retrying

When

A send failed for a passing reason and will be tried again

message.needs_review

When

The outcome of a send is unclear and a person should check it

message.recipients_refused

When

The provider refused some of a message’s recipients

message.bounce_reported

When

After accepting a message, the provider reported a bounce

message.complaint_reported

When

The provider reported that a recipient marked a message as spam

message.delay_reported

When

The provider reported that a message is delayed

approval.requested

When

Someone asked for approval to send

approval.decided

When

An approval request was approved or rejected

campaign.completed

When

A campaign finished sending

campaign.paused

When

A campaign was paused

schedule.run_failed

When

A scheduled or recurring send could not run

mailbox.needs_reconnect

When

A connected mailbox’s sign-in stopped working

assistant.send_requested

When

An AI assistant asked to send a message

webhook.test

When

Sent only by Send test. It reports nothing that happened

03

The payload

Each call is an HTTP POST with a JSON body. The fields are type, timestamp, workspaceId and data. The data object holds the identifiers for the event, and it never holds message content or email addresses.

  • type is one of the event types listed above.
  • Use the raw body exactly as it arrives. Re-serializing the JSON changes the bytes that were signed.
Example payloadJSON
{  "type": "message.accepted",  "timestamp": "2026-10-25T09:30:00.000Z",  "workspaceId": "0192f0a0-0000-7000-8000-000000000001",  "data": { "messageId": "0192f0a0-0000-7000-8000-0000000000aa", "afterReview": false }}

04

Headers and signatures

Calls follow the Standard Webhooks scheme. The signing secret is shown once, when you add an endpoint or replace its secret. It looks like whsec_ followed by base64 text.

webhook-id

Value

The call’s id. A replay sends the same body with a new id

webhook-timestamp

Value

Unix seconds when this attempt was signed

webhook-signature

Value

v1,<base64 HMAC-SHA256>

user-agent

Value

SecureThread-Webhooks/1

05

Verifying a call

The signature is HMAC-SHA256. The key is the base64-decoded part after whsec_. The signed text is the webhook-id, the webhook-timestamp and the raw body, joined with dots. Any Standard Webhooks library can check it. The example below checks it by hand in Node.js.

  • The example rejects a call whose timestamp is more than 300 seconds from the current time.
  • webhook-signature can hold several signatures separated by spaces. The example accepts a match against any of them.
  • The example compares signatures with timingSafeEqual rather than ===.
  • Treat webhook-id as an idempotency key. A call can arrive more than once.
Verify a call in Node.jsJavaScript
import { createHmac, timingSafeEqual } from 'node:crypto' function verify(secret, headers, rawBody) {  const id = headers['webhook-id']  const timestamp = Number(headers['webhook-timestamp'])  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false  const key = Buffer.from(secret.slice('whsec_'.length), 'base64')  const expected = `v1,${createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64')}`  return headers['webhook-signature']    .split(' ')    .some(      (candidate) =>        candidate.length === expected.length &&        timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)),    )}

06

Answers and retries

Any 2xx answer that arrives within 10 seconds counts as success. The body of the answer is not kept.

  • A 410 Gone answer switches the endpoint off at once.
  • Any other answer, or no answer, is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
  • That is 8 attempts in all, about two days. After the last one, the call is marked failed.

07

Switching off and back on

SecureThread switches an endpoint off in two cases. A 410 Gone answer does it at once. The other case is automatic, as described below.

  • After 5 calls in a row fail every attempt, SecureThread switches the endpoint off and writes the reason to the workspace’s audit log.
  • Fix the endpoint, then switch it on again in the console.
  • While an endpoint is paused or switched off, events are not queued for it.
  • When it comes back on, events from the previous 24 hours are sent. Older events are not.

08

Replay and the audit log

The console lists each call for 30 days, and a finished call can be replayed. A replay sends the same body with a new webhook-id.

  • Adding, editing, pausing, removing, replacing a secret, testing and replaying an endpoint are each written to the workspace’s audit log.
  • Audit entries show the endpoint’s host, never its full URL or its secret.
Can a webhook go to a local or private address?

No. The endpoint must use https, have a domain name rather than an IP address, and resolve only to public addresses.

Does a call include the email content or recipient addresses?

No. Payloads carry identifiers, never message content or email addresses.

Can my endpoint receive the same call twice?

Yes. A call can arrive more than once, so use webhook-id to recognize a call you have already processed.

Does message.accepted mean the recipient got the email?

No. It means the provider took responsibility for the message. It does not mean the message reached anyone, and SecureThread never reports that it did.

What happens when my endpoint is down?

Calls that get no 2xx answer are tried again over about two days. If every attempt fails for 5 calls in a row, the endpoint is switched off and the reason goes to the audit log. Events are not queued while it is off. When it is switched back on, events from the previous 24 hours are sent.

Build on SecureThread

Create a workspace key, send from a published template, and get signed events back. Every API send passes the same checks and approvals as one from the app.

Send from the mailboxes you already useA person approves what needs approvalCredentials encrypted with AES-256-GCMEvery action in an append-only audit logAI assistants governed over MCP1,499 starter templatesHonest status: accepted is never called delivered