CATALOGUES · Published 2026-10-06 · Updated 2026-10-08 · SecureSMTP
Event catalogue
Every webhook event, when it is sent and what it contains.
Webhooks are set per site in Webhooks (/app/forms/webhooks). Each event is a POST with a JSON body. How to check the signature: Webhooks.
Every event
{
"id": "uuid of this event",
"type": "email.delivered",
"created_at": "ISO 8601 time",
"site_id": "uuid of the site",
"data": { ... }
}| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | SecureSMTP-Webhooks/1.0 |
X-QCS-Signature | sha256= and the hex HMAC-SHA256 of the raw body, keyed with the webhook secret (including whsec_). |
X-QCS-Event | The event type. |
X-QCS-Delivery | A UUID for this delivery. The same on every retry, so you can drop duplicates. |
- Any 2xx answer counts as received. Anything else, or no answer within 10 seconds, is a failure.
- A failed delivery is tried 3 times in all, waiting 1.5 seconds and then 3 seconds. Then it is dropped.
- In email events,
tois the email’s first recipient andsubjectis left out when the email had none. - New fields may be added to
data. Ignore fields you do not know.
| Event | Sent when |
|---|---|
email.sent | We accepted the email and handed it to our mail server. |
email.delivered | The receiving mail server accepted the email. |
email.delayed | The receiving server asked us to try again later. |
email.bounced | Delivery failed for good. |
email.complained | The recipient marked the email as spam. |
email.opened | The open-tracking image was loaded. |
email.clicked | A tracked link was clicked. |
email.unsubscribed | A recipient used the unsubscribe link or header. |
email.suppressed | Not sent: every recipient had bounced, complained or unsubscribed before. |
email.rejected | Not sent: the content was flagged as spam, or your plan’s limit was reached. |
email.held | Not sent: a contact-form notification was held as spam. |
email.failed | Not sent: the send attempt failed. |
form.submitted | A submission to POST /forms/submit was stored. |
form.spam | A submission was stored but classified as spam. |
domain.verified | A sending domain passed its DNS check. |
domain.unverified | A verified sending domain no longer passes its DNS check. |
webhook.test | You clicked Test in the dashboard. |
email.sent
Sent when we accept an email and hand it to our mail server, from POST /mail/send, SMTP or a follow-up sequence. It is the same moment the API answers sent: true. email.delivered or email.bounced follows.
- message_idstringrequired
- The same
message_idthe API returned. - tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
- modestringrequired
relayordomain: which sender was used.- sourcestringoptional
sequencewhen a follow-up sequence sent it.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.sent",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"subject": "Your order has shipped",
"message_id": "<[email protected]>",
"mode": "domain"
}
}email.delivered
Sent once, when the receiving mail server accepts the email.
- message_idstringrequired
- The email’s message_id.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.delivered",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "Your order has shipped"
}
}email.bounced
Sent when the receiving server rejects the email for good, or when retries for a temporary problem run out. When the bounce can be matched to an email, the event has its message_id:
- message_idstringrequired
- The
message_idreturned byPOST /mail/send. - tostringrequired
- The first recipient of the email.
- subjectstringoptional
- The email’s subject.
- detailstringoptional
- The receiving server’s reply, up to 500 characters.
- bounce_kindstringoptional
recipient(the address does not exist),message(rejected because of the content or a policy),transient(retries ran out) orunknown.- suppressedbooleanoptional
- True when the address was added to the suppression list because of this bounce. Only
recipientbounces can do that.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.bounced",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "Your invoice",
"detail": "550 5.1.1 The email account that you tried to reach does not exist.",
"bounce_kind": "recipient",
"suppressed": true
}
}When it cannot be matched to an email, data has only to, subject (from the latest email to that address) and detail.
email.complained
Sent when the recipient’s mailbox provider reports that they marked the email as spam. The address is normally added to the suppression list; when suppressed is present it says whether it was.
- message_idstringoptional
- Present when the report could be matched to an email.
- tostringrequired
- The recipient address.
- subjectstringoptional
- The email’s subject.
- detailstringoptional
- What the report said.
- suppressedbooleanoptional
- True when the address was added to the suppression list. Present when message_id is.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.complained",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "October newsletter",
"detail": "feedback-loop complaint",
"suppressed": true
}
}email.delayed
Sent when the receiving server answers with a temporary error. We keep retrying, so the email may still be delivered or bounce later. It can be sent more than once for the same email.
- message_idstringrequired
- The email’s message_id.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
- reasonstringrequired
- The receiving server’s reply, up to 300 characters.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.delayed",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "Your order has shipped",
"reason": "451 4.7.1 Greylisted, please try again later"
}
}email.opened
Sent each time the open-tracking image in the email is loaded, so one email can produce several. Some mail programs block images, and some load them without a person opening the email. See Opens and clicks.
- message_idstringrequired
- The email’s message_id.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.opened",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "Your order has shipped"
}
}email.clicked
Sent each time a tracked link in the email is clicked. The data does not include the link. A click also counts as an open in the statistics, but does not send an email.opened event.
- message_idstringrequired
- The email’s message_id.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.clicked",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message_id": "<[email protected]>",
"to": "[email protected]",
"subject": "Your order has shipped"
}
}email.unsubscribed
Sent the first time a recipient unsubscribes from your site, using the one-click button in their mail app or the link in the email. Later emails to that address from this site are not sent (see email.suppressed). See Unsubscribe links.
- tostringrequired
- The address that unsubscribed.
- sourcestringrequired
one_click(the mail app’s button) orpage(the link in the email).
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.unsubscribed",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"source": "one_click"
}
}email.suppressed
Sent when an email is not sent because every recipient is on the suppression list or has unsubscribed from your site. When only some recipients are, the others still get the email and no event is sent.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
- reasonstringrequired
bounced_or_complainedorunsubscribed.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.suppressed",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"subject": "Your invoice",
"reason": "bounced_or_complained"
}
}email.rejected
Sent when we refuse to send an email. The API answers with the same reason, and the WordPress plugin sends the email with WordPress’s own mailer instead.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
- reasonstringrequired
content_flagged(the content looks like spam),ai_flagged(our spam check flagged it) orover_quota(your plan’s email limit is reached). See the error catalogue.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.rejected",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"subject": "Monthly newsletter",
"reason": "over_quota"
}
}email.held
Sent when a contact-form notification is held because our spam check is at least 90% sure it is spam. It is not sent, and the API still answers sent: true so the plugin does not send it another way. See Spam protection.
- tostringrequired
- Where the notification would have gone.
- subjectstringoptional
- The notification’s subject.
- reasonstringrequired
- Always
held_as_spam. - confidencenumberrequired
- How sure the check was, from 0.9 to 1.
- detailstringoptional
- Why it looked like spam, up to 200 characters.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.held",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"subject": "New contact form message",
"reason": "held_as_spam",
"confidence": 0.97,
"detail": "Unsolicited SEO offer with a link shortener"
}
}email.failed
Sent when the send attempt itself fails. The API answers send_failed.
- tostringrequired
- The first recipient.
- subjectstringoptional
- The email’s subject.
- errorstringrequired
- What went wrong, up to 300 characters.
- sourcestringoptional
sequencewhen a follow-up sequence sent it.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "email.failed",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"to": "[email protected]",
"subject": "Your order has shipped",
"error": "connection timed out"
}
}form.submitted
Sent when a submission to POST /forms/submit is stored, before the notification email goes out. See Forms API.
- submission_idstringrequired
- Our id for the submission.
- form_idintegerrequired
- The
form_idyou sent. - form_namestringoptional
- The
form_nameyou sent. - classificationstringrequired
legitimate(under 30) orsuspicious(30 to 59).- spam_scoreintegerrequired
- Our spam score, 0 to 100. 60 or more is spam.
- fieldsobjectrequired
- The fields exactly as you sent them.
- referrerstringoptional
- The page the form was sent from, when you passed it.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "form.submitted",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"submission_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"form_id": 3,
"form_name": "Contact",
"classification": "legitimate",
"spam_score": 4,
"fields": {
"name": "Ana Lopez",
"email": "[email protected]",
"message": "Do you ship to Spain?"
},
"referrer": "https://example.com/contact"
}
}form.spam
Sent instead of form.submitted when the submission scores 60 or more. It is stored, but no notification email is sent. The data is the same, with classification set to spam.
domain.verified
Sent when a sending domain passes its DNS check: when you click Verify DNS, or in the hourly check we run for domains that are not verified yet. Emails then go out from your own domain. See Domains and DNS.
- domainstringrequired
- The domain.
- from_addressstringoptional
- The address emails are sent from.
- statusstringrequired
- Always
verified.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "domain.verified",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"domain": "example.com",
"from_address": "[email protected]",
"status": "verified"
}
}domain.unverified
Sent when a domain that was verified fails a later check, for example after its DKIM record was removed. Emails go out through our shared sender again until it passes. Verified domains are only re-checked when you click Verify DNS.
- domainstringrequired
- The domain.
- from_addressstringoptional
- The address emails were sent from.
- statusstringrequired
- The new status, usually
pending.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "domain.unverified",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"domain": "example.com",
"from_address": "[email protected]",
"status": "pending"
}
}webhook.test
Sent when you click Test next to a webhook in the dashboard. It is signed the same way, but it has no X-QCS-Delivery header and is tried only once. The HTTP status your server returns is saved as the webhook’s last status.
{
"id": "0b6a5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
"type": "webhook.test",
"created_at": "2026-10-07T09:30:12.345Z",
"site_id": "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d",
"data": {
"message": "This is a test event from SecureSMTP.",
"to": "[email protected]"
}
}email.inbound
Sent for each email received on an inbound domain. It is different from the events above: the body is the raw email (RFC 822), not JSON, and it uses the inbound domain’s own secret (starting with qcs_in_). See Receiving email.
| Header | Value |
|---|---|
Content-Type | message/rfc822 |
User-Agent | SecureSMTP-Inbound/1.0 |
X-QCS-Event | email.inbound |
X-QCS-Signature | sha256= and the hex HMAC-SHA256 of the raw body, keyed with the inbound secret. |
X-QCS-Delivery | A UUID for this delivery, the same on the retry. |
X-QCS-Domain | The inbound domain. |
X-QCS-Recipient | The envelope recipient. |
X-QCS-Sender | The envelope sender. |
X-QCS-Timestamp | Unix time in seconds. Not covered by the signature. |
- Tried twice, with a 12-second timeout each. Redirects are not followed.
- If your server does not answer 2xx, our mail server answers the sender with a temporary error (
450), so the sending server tries again later.