Webhooks

Endpoints, signing secrets, the delivery log, test events and replays.

Guides: Webhooks, Signatures, Delivery and replay, Events.

MethodPathOperationScope
GET/v1/webhooksList webhook endpointsread
POST/v1/webhooksCreate a webhook endpointwrite
PATCH/v1/webhooks/{endpointId}Update a webhook endpointwrite
DELETE/v1/webhooks/{endpointId}Delete a webhook endpointwrite
POST/v1/webhooks/{endpointId}/rotate-secretRotate the signing secretwrite
POST/v1/webhooks/{endpointId}/testSend a test eventwrite
GET/v1/webhooks/{endpointId}/attemptsRead the delivery logread
POST/v1/webhooks/{endpointId}/replayReplay a time rangewrite
POST/v1/webhooks/{endpointId}/events/{eventId}/replayReplay one eventwrite
GET/v1/webhooks/{endpointId}/failed-deliveries/countCount failed deliveriesread
POST/v1/webhooks/{endpointId}/replay-failedReplay failed deliverieswrite

List webhook endpoints

GET
/v1/webhooks

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.

Response Body

OK

application/json
  1. response
items*array<>
Example Request
curl -X GET 'https://api.workwae.com/v1/webhooks'
Example Response
{  "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"    }  ]}
POST
/v1/webhooks

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.

Request Body

application/json
  1. body
url*string
Formaturi
description?string
Lengthlength <= 500
filters?

Response Body

OK

application/json
  1. response
endpoint*
secret*string
Example Request
curl -X POST 'https://api.workwae.com/v1/webhooks' \  -H 'Content-Type: application/json' \  -d '{  "url": "https://app.example.com/"}'
Example Response
{  "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"}
PATCH
/v1/webhooks/{endpointId}

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.

Path Parameters

endpointId*string
Formatuuid

Request Body

application/json
  1. body
url?string
Formaturi
description?|null
filters?
isEnabled?boolean

Response Body

OK

application/json
  1. response
id*string
Formatuuid
url*string
Formaturi
description*string|null
filters*
isEnabled*boolean
disabledReason*string|null
lastSuccessAt*|null
failingSince*|null
previousSecretExpiresAt*|null
createdAt*string
Formatdate-time
Example Request
curl -X PATCH 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c' \  -H 'Content-Type: application/json' \  -d '{}'
Example Response
{  "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"}
DELETE
/v1/webhooks/{endpointId}

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.

Path Parameters

endpointId*string
Formatuuid

Response Body

OK

application/json
  1. response
Example Request
curl -X DELETE 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c'
Example Response
null
POST
/v1/webhooks/{endpointId}/rotate-secret

Runs 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.

Path Parameters

endpointId*string
Formatuuid

Request Body

application/json
  1. body
graceSeconds?integer
Range0 <= value <= 604800
Default86400

Response Body

OK

application/json
  1. response
secret*string
previousSecretExpiresAt*|null
Example Request
curl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/rotate-secret' \  -H 'Content-Type: application/json' \  -d '{}'
Example Response
{  "secret": "string",  "previousSecretExpiresAt": "2026-05-04T09:30:00.000Z"}

Send a test event

POST
/v1/webhooks/{endpointId}/test

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.

Path Parameters

endpointId*string
Formatuuid

Request Body

application/json
  1. body
eventType?EventType
Default"webhook.test"
Value in"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?string
Formatuuid
data?

Response Body

OK

application/json
  1. response
eventId*EventId
Match^[0-9]{1,19}$
Example Request
curl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/test' \  -H 'Content-Type: application/json' \  -d '{}'
Example Response
{  "eventId": "string"}

Read the delivery log

GET
/v1/webhooks/{endpointId}/attempts

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.

Path Parameters

endpointId*string
Formatuuid

Query Parameters

limit?integer
Range1 <= value <= 500
Default50
cursor?string
Length1 <= length <= 512
eventType?string
Value in"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
Match^[0-9]{1,19}$
accountId?string
Formatuuid
result?string
Value in"succeeded""failed"
since?string
Formatdate-time
until?string
Formatdate-time

Response Body

OK

application/json
  1. response
items*array<>
nextCursor*|null
Example Request
curl -X GET 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/attempts'
Example Response
{  "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"}

Replay a time range

POST
/v1/webhooks/{endpointId}/replay

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.

Path Parameters

endpointId*string
Formatuuid

Request Body

application/json
  1. body
since*string
Formatdate-time
until*string
Formatdate-time

Response Body

OK

application/json
  1. response
queued*integer
alreadyQueued*integer
Example Request
curl -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"}'
Example Response
{  "queued": 0,  "alreadyQueued": 0}

Replay one event

POST
/v1/webhooks/{endpointId}/events/{eventId}/replay

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.

Path Parameters

endpointId*string
Formatuuid
eventId*string
Match^[0-9]{1,19}$

Response Body

OK

application/json
  1. response
queued*integer
alreadyQueued*integer
Example Request
curl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/events/string/replay'
Example Response
{  "queued": 0,  "alreadyQueued": 0}

Count failed deliveries

GET
/v1/webhooks/{endpointId}/failed-deliveries/count

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.

Path Parameters

endpointId*string
Formatuuid

Query Parameters

since?string
Formatdate-time
until?string
Formatdate-time

Response Body

OK

application/json
  1. response
count*integer
Example Request
curl -X GET 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/failed-deliveries/count'
Example Response
{  "count": 0}

Replay failed deliveries

POST
/v1/webhooks/{endpointId}/replay-failed

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.

Path Parameters

endpointId*string
Formatuuid

Request Body

application/json
  1. body
since?string
Formatdate-time
until?string
Formatdate-time

Response Body

OK

application/json
  1. response
queued*integer
alreadyQueued*integer
Example Request
curl -X POST 'https://api.workwae.com/v1/webhooks/19a0c947-96ff-4eb7-98ff-bbd69758555c/replay-failed' \  -H 'Content-Type: application/json' \  -d '{}'
Example Response
{  "queued": 0,  "alreadyQueued": 0}