How to receive webhooks
1
Configure the URL in the dashboard
Open your project and go to Settings → Integrations → Webhooks. Paste the public URL of your endpoint. The per-project URLs and what each one receives are covered in Routing live / test / environment.
2
Expose your local endpoint
In development, create a tunnel to your local server with ngrok (
ngrok http 3000) and use the generated public URL as the webhook URL.3
Validate the signature
Every POST includes the
X-RQE-Signature header. Verify the HMAC over the raw body before processing the event. Learn more in HMAC verification.4
Respond 200
Return a
2xx status as soon as possible. Any other status, timeout, or network error triggers retries. See Retry and expected response.5
Move to production
Replace the tunnel URL with your server’s. The payload’s
is_test flag distinguishes traffic from sends with sk_test_*.Events
Outbound payload
All outbound events (email.send, email.delivery, email.bounce, email.complaint, email.reject, email.deliverydelay, email.open, email.click) share the same top-level structure. Only data changes.
data fields
The
email.send and email.delivery events do not include a separate delivered_at field — use event_timestamp.email.suppressed
Emitted when you send via API to an address that is on the project’s suppression list (a previous hard bounce or complaint): the email is blocked before going out and the /v1/send-email response already indicates it with suppressed: true (see Suppressed recipient). Use it to flag the address in your system and stop trying.
Differences vs the other outbound events:
- Only emitted for sends via API — campaigns and automations do not trigger it.
- Its payload is reduced: the top level does not include
is_testorenvironment, and there is nomessage_id— the email never went out.datacarriesemail,activity_id,email_type,suppressed_at, and the reason (suppression_reason,suppression_source,suppression_description).
suppression.added / suppression.removed
email.suppressed tells you when you try to send to an already-suppressed address. These two tell you the moment an address enters or leaves the list, without waiting for a send.
That difference matters if you keep your own CRM: without them a contact dies silently and you only find out on the next attempt, which can be weeks later.
suppression.added fires when RQE automatically suppresses an address after a hard bounce or a spam complaint.
reason is complaint and complaint_feedback_type arrives instead of the bounce fields.
suppression.removed fires when an address is taken off the list from the dashboard or the API — useful because someone else on your team may have reactivated it, not your system.
suppression.added is not emitted for bounces with subtype OnAccountSuppressionList: that is an echo of the provider’s own list, not a new bounce, and it does not create a suppression in your project.On a bulk removal one event is emitted per address up to a cap of 200; going over that is recorded in the logs.domain.send_ready
Emitted once, when the domain becomes able to send. It arrives before domain.verified: SES enables sending as soon as it recognizes the domain, while DKIM may still be propagating.
Use it to unblock the sender without pretending authentication already finished. If you automate domain onboarding, this is the event that lets you enable sending in your own UI; domain.verified arrives later and confirms aligned DKIM signing is also in place.
A domain that can send but has no verified DKIM yet delivers worse: without aligned signing, DMARC won’t pass via DKIM. Enable sending if you need to, but wait for
domain.verified before sending volume.domain.verified / domain.failed
Emitted when a sending domain changes verification state. They’re the alternative to polling GET /domains/{domain}/status in a loop: if you automate domain onboarding, listen for these events instead of polling.
Only on transition. Pending domains are re-checked periodically, but the webhook fires once, when the state actually changes. A domain that stays
verified does not re-emit. previous_verification_status tells you which state it came from.domain.failed arrives in the same shape, with verification_status: "failed" and can_send: false. Check details to see which part failed (domain_verification or dkim_verification), and call POST /domains/{domain}/recreate if DKIM is stuck.Request headers
RQE POSTs to your URL with:HMAC verification
Routing live / test / environment
Each project has configurable URLs:
Without a custom
environment, each outbound event is delivered to all configured URLs (Production and Development at once): routing does not filter by the send’s mode. The X-RQE-Environment header indicates each POST’s slot (live / dev); the payload’s is_test flag tells whether the send used sk_test_*.
If you configure inbound_webhook_url / inbound_webhook_url_dev, those URLs replace webhook_url / webhook_url_dev and receive all events (outbound + inbound) — a “one URL receives everything” model.
Inbound webhooks (replies) go to a single URL based on the mode of the original send (see fallback below). The per-environment override uses inbound_webhook_environments[<key>].
Automatic fallback
Applies only toemail.inbound; outbound events have no fallback because they go to all configured URLs. RQE uses the first non-empty URL in this chain:
- Original send with
sk_test_*:inbound_webhook_url_dev→webhook_url_dev→inbound_webhook_url→webhook_url - Original live send:
inbound_webhook_url→webhook_url→inbound_webhook_url_dev→webhook_url_dev
environment declared in the send is not configured, the entire send fails with 400 ENVIRONMENT_NOT_CONFIGURED. See Webhook environments.
is_test in the payload
webhook_url is enough to receive everything on a single URL. Distinguish test traffic with the is_test flag.
Inbound (replies with attachments)
When a recipient replies to your email, RQE captures the reply and fires it asemail.inbound.
When it fires
Only when the original outbound email was sent via API (POST /v1/send-email, POST /send-email, or POST /v1/send-batch). In that case the Reply-To carries a unique token:
Branded reply domain (optional, token-less)
A project can configure its own reply domain (reply.yourdomain.com) so the Reply-To comes out clean and branded, without the encoded token:
Message-ID identifies the thread (In-Reply-To / References headers). The email.inbound event you receive is identical: you keep reading activity_id and thread_id from the payload, not the address. The token was always RQE’s internal plumbing, not something your integration parses — so enabling a reply domain changes nothing in your code.
Enabled per project by publishing 2 DNS records (an SES verification TXT at
_amazonses.reply.yourdomain.com + an MX reply.yourdomain.com → inbound-smtp.us-east-1.amazonaws.com) and coordinating with RQE. Self-service configuration is coming. Without a reply domain configured, the usual token is used.Inbound payload body
is_test and, if the original send used a custom environment, also environment.
Key fields
Size limit
150 KB total per reply (including attachments in base64 MIME). Replies that exceed it bounce with"Message length exceeds limit set by recipient". Workaround: ask the contact to share large files via a Drive/WeTransfer link.
Retry and expected response
- Timeout: 10 seconds per attempt.
- Attempts (outbound events): 5 in total — 1 initial + 4 retries with exponential backoff.
email.inbound: a single attempt, no retries.- Idempotency: retries may re-deliver the same event; if you deliver to both URLs and only one fails, the retry re-delivers to both. Use the tuple
(event, data.activity_id, data.event_timestamp)to deduplicate client-side.
Next steps
- Tracking — which events trigger a webhook and how they are generated.
- Test mode — separate dev/prod traffic via
sk_test_*. - Webhook environments — N URLs per project via the
environmentfield. - Send email — how to trigger emails that generate these events.