Webhooks
Create an endpoint that receives Workwae events as signed HTTPS POSTs, and choose which events it gets.
A webhook endpoint is an HTTPS URL that Workwae POSTs to for every event it subscribes to: a message arrives, a send completes, an account needs reauthorization. This page covers creating an endpoint, filtering its events and the request it receives.
Webhooks push events as they happen, signed and retried. The change feed carries the same events, with the same ids, for you to poll instead.
Create an endpoint
Create the endpoint
Use the console's Webhooks page or the API.
curl -X POST "https://api.workwae.com/v1/webhooks" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://app.example.com/hooks/workwae",
"description": "Production",
"filters": { "eventTypes": ["message.created", "account.reauth_required"] }
}'Store the signing secret
The response carries the endpoint's signing secret (whsec_...) this once only. Rotating it issues a new one.
Verify and acknowledge deliveries
Verify each signature, then answer 2xx within 10 seconds. Send a test event to check your receiver before real traffic exists.
Endpoint URL requirements
- The URL must use HTTPS; any other URL answers 400 with reason
https_required. - It must resolve to a public address. Deliveries to private, loopback and link-local networks are refused.
Filters
An endpoint's filters object decides which events it receives. Omitted filters are unset on create and left as they are on update.
| Filter | Delivers | Default |
|---|---|---|
eventTypes | Only these event types. | Empty: every type, including types added later. |
accountIds | Only events of these accounts. Once set, events that concern no account (such as account.purged) are not delivered either. Ids must be accounts of your tenant, else 422 with reason unknown_account. | Empty: every account. |
message | Only events about matching messages; see message criteria. | Unset. |
- Change filters at any time with
PATCH /v1/webhooks/{endpointId}and afiltersobject holding only the filters to change. Each list takes up to 100 entries. - Filters do not apply to test events or to a replay of one picked event. A replay of a time range applies them all.
Message criteria
filters.message narrows message.*, send.* and message.bounced events to some messages; every other event passes. Every criterion you set must match, and any one value within a criterion is enough. Criteria are matched against the event's own data, so a criterion the event cannot answer does not match.
| Criterion | Matches when |
|---|---|
mailboxIds, mailboxRoles | One criterion: the message is in a listed mailbox, or in a mailbox with a listed role (inbox, sent, ...). |
from | The sender is a listed address, or at a listed @example.com domain (exact domain, case-insensitive). |
to | A to or cc recipient matches, compared like from. |
subjectContains | A listed phrase appears anywhere in the subject, case-insensitive. |
hasAttachments | true or false; null clears it. |
direction | outbound: the message is in the sent mailbox or from one of the account's own addresses (its address and identities). inbound: every other message. null clears it. |
- Mailbox ids must be live mailboxes of your tenant (422, reason
unknown_mailbox) and, whenfilters.accountIdsis set, of those accounts (400, reasonmailbox_outside_accounts). message.deletedcarries the message as last indexed and the mailboxes that held it, and is matched on those.send.*events name the queue entry, not the message: they are outbound and answer no other criterion.message.bouncedis outbound, andtois matched against its failed recipients.
What your endpoint receives
Each delivery is a POST with a JSON body. Answer with any 2xx status within 10 seconds to acknowledge it. Redirects are not followed: a 3xx counts as a failure, and failures are retried.
| Header | Value |
|---|---|
webhook-id | The event id, the same as the body's id. Stable across retries and replays: deduplicate on it. |
webhook-timestamp | When this attempt was signed, in Unix seconds. |
webhook-signature | One or more space-separated v1,<base64> signatures. |
content-type | application/json |
user-agent | workwae-webhooks/1 |
{
"id": "1001",
"type": "message.created",
"accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
"createdAt": "2026-05-04T09:30:00.000Z",
"data": { "message": { "id": "c4e6a8b0-…", "subject": "Quarterly report" }, "mailboxes": [] }
}datadepends ontype; the event catalog has a full sample for each.accountIdis the account the event concerns;datadoes not repeat it. It isnullfor events that concern no account.- Test events add
"isTest": true.
Process asynchronously
Acknowledge quickly and do the work in a queue of your own. Retries and replays deliver the same event id again, so make handling idempotent on webhook-id.