Webhooks let your app react to changes in a connected XOXO account without polling the REST API. When something happens — a new subscriber signs up, a tag is added, an email finishes sending — XOXO sends an HTTPDocumentation Index
Fetch the complete documentation index at: https://help.xoxo.email/llms.txt
Use this file to discover all available pages before exploring further.
POST to a URL you control.
Webhooks are scoped to OAuth apps. Personal API keys don’t deliver webhooks — they’re for one-off scripts and integrations against your own account.
Registering a webhook URL
OAuth apps are configured by the XOXO team. When you request your client credentials, include:- Webhook URL — an HTTPS endpoint that accepts
POSTrequests - Contact emails — addresses to notify if your endpoint starts failing
When events fire
Your app receives an event for an account when all of the following are true:- The account has authorized your app and the access token is still valid
- The event’s resource type is one of: account, subscriber, tag, field, group, email
- Your app has a webhook URL set and the webhook isn’t disabled (see Failures)
Event types
Events follow a<resource>.<action> naming convention.
| Event | When it fires |
|---|---|
account.deleted | Account is deleted |
account.updated | Account name, slug, sender address, physical address, image, or logo changes |
email.created | A draft email is created |
email.deleted | A draft is removed |
email.scheduled | An email is scheduled to send |
email.sent | An email finishes sending to all recipients |
email.unscheduled | A scheduled send is cancelled |
email.updated | An email is edited |
field.created | A custom field is added |
field.deleted | A custom field is removed |
field.updated | A custom field is renamed |
group.created | A group is created |
group.deleted | A group is removed |
group.updated | A group’s name, description, color, or filters change |
subscriber.created | A subscriber is added |
subscriber.deleted | A subscriber is removed |
subscriber.updated | A subscriber’s fields or tags change |
tag.created | A tag is created |
tag.deleted | A tag is removed |
tag.updated | A tag’s name or color changes |
Payload format
Every webhook is aPOST with a JSON body in this envelope:
| Field | Description |
|---|---|
id | Unique, time-ordered UUID v7 for this delivery. Use it to deduplicate retries. |
event | The event name from the table above. |
occurred_at | ISO 8601 timestamp of when the event happened. |
account_id | The XOXO account the event belongs to. |
data | The full resource as it appears in the REST API. For *.deleted events, data contains the resource as it was just before deletion. |
data matches the corresponding REST API resource exactly — see the endpoint reference for each resource’s fields.
Headers
Each delivery includes:| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | xoxo-webhooks/1.0 |
XOXO-Event | The event name, e.g. subscriber.created |
XOXO-Event-Id | The event id (also in the body) |
XOXO-Signature | HMAC signature — see below |
Verifying signatures
Always verify theXOXO-Signature header before trusting a payload. The header has the form:
- Split the header into the timestamp (
t) and the signature (v1) - Build the signed string:
<timestamp>.<raw_request_body> - Compute
HMAC-SHA256using your app’sclient_secretas the key - Compare the hex digest to
v1in constant time
t timestamp is older than a few minutes.
Responding
Reply with any2xx status within 5 seconds to acknowledge the event. The body is ignored.
If you need to do slow work in response to a webhook, queue it on your end and return 200 immediately.
Failures
Failed deliveries are retried with exponential backoff up to 12 times over roughly 10 hours. A delivery counts as failed if it:- returns a non-
2xxstatus code (other than410— see below) - times out after 5 seconds
- can’t be reached at all
Returning
410 Gone immediately disables the webhook with no further retries. Use it when the URL is permanently retired.Idempotency
Retries replay the sameid. Treat that ID as the idempotency key on your end so a redelivered event isn’t processed twice.