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.
message
approval
campaign
schedule
mailbox
assistant
webhook
message.accepted: The provider accepted a message (not confirmation it reached anyone).
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
message.failed
When
message.retrying
When
message.needs_review
When
message.recipients_refused
When
message.bounce_reported
When
message.complaint_reported
When
message.delay_reported
When
approval.requested
When
approval.decided
When
campaign.completed
When
campaign.paused
When
schedule.run_failed
When
mailbox.needs_reconnect
When
assistant.send_requested
When
webhook.test
When
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.
{ "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
webhook-timestamp
Value
webhook-signature
Value
user-agent
Value
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.
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.
FAQ
Questions, answered
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.