Webhooks
Endpoints, signing secrets, the delivery log, test events and replays.
Guides: Webhooks, Signatures, Delivery and replay, Events.
| Method | Path | Operation | Scope |
|---|---|---|---|
GET | /v1/webhooks | List webhook endpoints | read |
POST | /v1/webhooks | Create a webhook endpoint | write |
PATCH | /v1/webhooks/{endpointId} | Update a webhook endpoint | write |
DELETE | /v1/webhooks/{endpointId} | Delete a webhook endpoint | write |
POST | /v1/webhooks/{endpointId}/rotate-secret | Rotate the signing secret | write |
POST | /v1/webhooks/{endpointId}/test | Send a test event | write |
GET | /v1/webhooks/{endpointId}/attempts | Read the delivery log | read |
POST | /v1/webhooks/{endpointId}/replay | Replay a time range | write |
POST | /v1/webhooks/{endpointId}/events/{eventId}/replay | Replay one event | write |
GET | /v1/webhooks/{endpointId}/failed-deliveries/count | Count failed deliveries | read |
POST | /v1/webhooks/{endpointId}/replay-failed | Replay failed deliveries | write |
Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Required scope: read.
OK
application/json- response
items*array<>curl -X GET 'https://api.workwae.com/v1/webhooks'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "url": "https://app.example.com/", "description": "Production", "filters": { "eventTypes": [ "account.connected" ], "accountIds": [ "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b" ], "message": { "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "mailboxRoles": [ "inbox" ], "from": [ "string" ], "to": [ "string" ], "subjectContains": [ "string" ], "hasAttachments": true, "direction": "inbound" } }, "isEnabled": true, "disabledReason": "string", "lastSuccessAt": "2026-05-04T09:30:00.000Z", "failingSince": "2026-05-04T09:30:00.000Z", "previousSecretExpiresAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z" } ]}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
The signing secret is returned once.
Required scope: write.
application/json- body
url*stringuridescription?stringlength <= 500filters?OK
application/json- response
endpoint*secret*stringcurl -X POST 'https://api.workwae.com/v1/webhooks' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://app.example.com/"}'{ "endpoint": { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "url": "https://app.example.com/", "description": "Production", "filters": { "eventTypes": [ "account.connected" ], "accountIds": [ "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b" ], "message": { "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "mailboxRoles": [ "inbox" ], "from": [ "string" ], "to": [ "string" ], "subjectContains": [ "string" ], "hasAttachments": true, "direction": "inbound" } }, "isEnabled": true, "disabledReason": "string", "lastSuccessAt": "2026-05-04T09:30:00.000Z", "failingSince": "2026-05-04T09:30:00.000Z", "previousSecretExpiresAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z" }, "secret": "string"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
The mailbox check runs whenever filters.message.mailboxIds or filters.accountIds changes.
Required scope: write.
endpointId*stringuuidapplication/json- body
url?stringuridescription?|nullfilters?isEnabled?booleanOK
application/json- response
id*stringuuidurl*stringuridescription*string|nullfilters*isEnabled*booleandisabledReason*string|nulllastSuccessAt*|nullfailingSince*|nullpreviousSecretExpiresAt*|nullcreatedAt*stringdate-timecurl -X PATCH 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "url": "https://app.example.com/", "description": "Production", "filters": { "eventTypes": [ "account.connected" ], "accountIds": [ "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b" ], "message": { "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "mailboxRoles": [ "inbox" ], "from": [ "string" ], "to": [ "string" ], "subjectContains": [ "string" ], "hasAttachments": true, "direction": "inbound" } }, "isEnabled": true, "disabledReason": "string", "lastSuccessAt": "2026-05-04T09:30:00.000Z", "failingSince": "2026-05-04T09:30:00.000Z", "previousSecretExpiresAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Required scope: write.
endpointId*stringuuidOK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c'nullRuns against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Deliveries are signed with the new secret at once. For graceSeconds (default 86400, at most 604800) they also carry a signature by the old one, in the same webhook-signature header, so receivers can switch secrets without refusing a delivery; 0 stops the old secret at once. A rotation during another's grace ends that grace: only the secret it replaces is kept.
Required scope: write.
endpointId*stringuuidapplication/json- body
graceSeconds?integer0 <= value <= 60480086400OK
application/json- response
secret*stringpreviousSecretExpiresAt*|nullcurl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/rotate-secret' \ -H 'Content-Type: application/json' \ -d '{}'{ "secret": "string", "previousSecretExpiresAt": "2026-05-04T09:30:00.000Z"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Queues one event of eventType for delivery to this endpoint only, whatever its filters, signed like any other, with "isTest": true in its body; its attempts show in the delivery log. It carries the sample data for its type unless data is given, which must match the type's data. Test events are never in the change feed.
Required scope: write.
endpointId*stringuuidapplication/json- body
eventType?EventType"webhook.test""account.connected""account.updated""account.deleted""account.reauth_required""account.initialized""account.purged""mailbox.created""mailbox.updated""mailbox.deleted""message.created""message.updated""message.deleted""message.bounced""message.classified""message.opened""message.clicked""send.sent""send.retrying""send.failed""send.rescheduled""suppression.created""calendar.initialized""calendar.created""calendar.updated""calendar.deleted""calendar_event.created""calendar_event.updated""calendar_event.deleted""contacts.initialized""contact.created""contact.updated""contact.deleted""export.completed""export.failed""meeting_connection.connected""meeting_connection.reauth_required""booking.created""booking.cancelled""webhook.test"accountId?stringuuiddata?OK
application/json- response
eventId*EventId^[0-9]{1,19}$curl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/test' \ -H 'Content-Type: application/json' \ -d '{}'{ "eventId": "string"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Delivery attempts to this endpoint, newest first: of the last 500 within 7 days, those matching the filters. A cursor stays valid while its attempt is in the log.
Required scope: read.
endpointId*stringuuidlimit?integer1 <= value <= 50050cursor?string1 <= length <= 512eventType?string"account.connected""account.updated""account.deleted""account.reauth_required""account.initialized""account.purged""mailbox.created""mailbox.updated""mailbox.deleted""message.created""message.updated""message.deleted""message.bounced""message.classified""message.opened""message.clicked""send.sent""send.retrying""send.failed""send.rescheduled""suppression.created""calendar.initialized""calendar.created""calendar.updated""calendar.deleted""calendar_event.created""calendar_event.updated""calendar_event.deleted""contacts.initialized""contact.created""contact.updated""contact.deleted""export.completed""export.failed""meeting_connection.connected""meeting_connection.reauth_required""booking.created""booking.cancelled""webhook.test"eventId?string^[0-9]{1,19}$accountId?stringuuidresult?string"succeeded""failed"since?stringdate-timeuntil?stringdate-timeOK
application/json- response
items*array<>nextCursor*|nullcurl -X GET 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/attempts'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "eventId": "string", "eventType": "account.connected", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "attempt": 0, "statusCode": 0, "error": "string", "responseExcerpt": "string", "durationMs": 0, "isReplay": true, "isTest": true, "occurredAt": "2026-05-04T09:30:00.000Z" } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Queues every event in [since, until) that the endpoint would receive with its current filters again, with the same id and body as the first delivery and a fresh signature. Events the endpoint already has queued are not queued twice. Events past the tenant's eventRetentionDays cannot be replayed.
Required scope: write.
endpointId*stringuuidapplication/json- body
since*stringdate-timeuntil*stringdate-timeOK
application/json- response
queued*integeralreadyQueued*integercurl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/replay' \ -H 'Content-Type: application/json' \ -d '{ "since": "2026-05-04T09:30:00.000Z", "until": "2026-05-04T09:30:00.000Z"}'{ "queued": 0, "alreadyQueued": 0}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Queues the event again for this endpoint, whatever its filters, with the same id and body as the first delivery and a fresh signature; not twice if the endpoint already has it queued. Events past the tenant's eventRetentionDays cannot be replayed.
Required scope: write.
endpointId*stringuuideventId*string^[0-9]{1,19}$OK
application/json- response
queued*integeralreadyQueued*integercurl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/events/string/replay'{ "queued": 0, "alreadyQueued": 0}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
How many events replay-failed would queue now: those the endpoint gave up on (retries exhausted, or given up when it was disabled) that are still retained and not queued again, optionally only those created in [since, until).
Required scope: read.
endpointId*stringuuidsince?stringdate-timeuntil?stringdate-timeOK
application/json- response
count*integercurl -X GET 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/failed-deliveries/count'{ "count": 0}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Queues every event the endpoint gave up on again, once each, optionally only those created in [since, until), with the same id and body as the first delivery and a fresh signature. The failed deliveries are replaced by the replays (their attempts stay in the delivery log), so calling again does not send them twice. Events past the tenant's eventRetentionDays cannot be replayed.
Required scope: write.
endpointId*stringuuidapplication/json- body
since?stringdate-timeuntil?stringdate-timeOK
application/json- response
queued*integeralreadyQueued*integercurl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/replay-failed' \ -H 'Content-Type: application/json' \ -d '{}'{ "queued": 0, "alreadyQueued": 0}