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

Body
{
  "id": "uuid of this event",
  "type": "email.delivered",
  "created_at": "ISO 8601 time",
  "site_id": "uuid of the site",
  "data": { ... }
}
HeaderValue
Content-Typeapplication/json
User-AgentSecureSMTP-Webhooks/1.0
X-QCS-Signaturesha256= and the hex HMAC-SHA256 of the raw body, keyed with the webhook secret (including whsec_).
X-QCS-EventThe event type.
X-QCS-DeliveryA 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, to is the email’s first recipient and subject is left out when the email had none.
  • New fields may be added to data. Ignore fields you do not know.
EventSent when
email.sentWe accepted the email and handed it to our mail server.
email.deliveredThe receiving mail server accepted the email.
email.delayedThe receiving server asked us to try again later.
email.bouncedDelivery failed for good.
email.complainedThe recipient marked the email as spam.
email.openedThe open-tracking image was loaded.
email.clickedA tracked link was clicked.
email.unsubscribedA recipient used the unsubscribe link or header.
email.suppressedNot sent: every recipient had bounced, complained or unsubscribed before.
email.rejectedNot sent: the content was flagged as spam, or your plan’s limit was reached.
email.heldNot sent: a contact-form notification was held as spam.
email.failedNot sent: the send attempt failed.
form.submittedA submission to POST /forms/submit was stored.
form.spamA submission was stored but classified as spam.
domain.verifiedA sending domain passed its DNS check.
domain.unverifiedA verified sending domain no longer passes its DNS check.
webhook.testYou clicked Test in the dashboard.
A webhook that has All events selected also gets events added later. Pick events one by one if you only want some.

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_id the API returned.
tostringrequired
The first recipient.
subjectstringoptional
The email’s subject.
modestringrequired
relay or domain: which sender was used.
sourcestringoptional
sequence when a follow-up sequence sent it.
Example
{
  "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.
Example
{
  "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_id returned by POST /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) or unknown.
suppressedbooleanoptional
True when the address was added to the suppression list because of this bounce. Only recipient bounces can do that.
Example
{
  "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.
Example
{
  "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.
Example
{
  "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.
Example
{
  "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.
Example
{
  "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) or page (the link in the email).
Example
{
  "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_complained or unsubscribed.
Example
{
  "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) or over_quota (your plan’s email limit is reached). See the error catalogue.
Example
{
  "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.
Example
{
  "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
sequence when a follow-up sequence sent it.
Example
{
  "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_id you sent.
form_namestringoptional
The form_name you sent.
classificationstringrequired
legitimate (under 30) or suspicious (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.
Example
{
  "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.
Example
{
  "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.
Example
{
  "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.

Example
{
  "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.

HeaderValue
Content-Typemessage/rfc822
User-AgentSecureSMTP-Inbound/1.0
X-QCS-Eventemail.inbound
X-QCS-Signaturesha256= and the hex HMAC-SHA256 of the raw body, keyed with the inbound secret.
X-QCS-DeliveryA UUID for this delivery, the same on the retry.
X-QCS-DomainThe inbound domain.
X-QCS-RecipientThe envelope recipient.
X-QCS-SenderThe envelope sender.
X-QCS-TimestampUnix 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.