New usersFree DNS and CDN plans for every new account.Create your account

Events and webhooks

Follow message statuses in the sending log and receive signed delivery, bounce, complaint, open and click events on your own endpoint.

6 min read Updated Oct 20, 2026

Every message you send gets a message ID and moves through a series of statuses as it's queued, delivered, retried or bounced. You can follow this in the console's sending log, query it with the API, or have each event pushed to your own server with a webhook.

Message statuses

Each recipient copy has one status. A status only moves forward: for example, a delivered message can still become bounced or complained, but never goes back to queued.

StatusConsole labelMeaning
queuedQueuedAccepted and waiting to be delivered
deferredDeferredThe receiving server returned a temporary error. Delivery is retried for up to 72 hours.
deliveredDeliveredThe receiving server accepted the message
bouncedBouncedDelivery failed permanently, either during delivery or through a bounce report received later. See the bounce types below.
complainedComplaintsThe recipient marked the message as spam
rejectedRejectedRefused before sending, for example because the recipient is suppressed, isn't allowed in the sandbox, or the content was rejected
failedFailedStill undeliverable after 72 hours of retries, or couldn't be queued

Opens, clicks and unsubscribes aren't statuses. They're recorded on the message (opened_at, clicked_at, unsubscribed_at) and sent as webhook events. In the sending log, an opened message has an extra Opened tag.

Bounce types

TypeWhenEffect
HardA permanent 5xx rejection, such as an unknown userThe address is added to the global suppression list
SoftA full mailbox (x.2.2) or a system, network or policy rejection (x.3.x to x.7.x, for example an IP reputation block)The address isn't suppressed

The sending log

The Message log tab of a stream lists the messages from the last 30 days with their time, recipient, subject, category, source (API or SMTP) and status. You can filter by Recipient, Status, Source, Category and a date range.

Click a message to open Message details: the status, sender, subject, category, tags, the receiving server's last response, and the list of events with the SMTP code, response, bounce type, MX host and IP of each delivery attempt.

The Overview tab adds up the same data for 7 or 30 days: sent, delivery rate, bounce rate, complaint rate, opens and clicks.

Webhooks

A webhook sends each event to an HTTPS endpoint of yours as it happens, so your application can, for example, mark a user's address as invalid after a hard bounce.

Set up a webhook

  1. Open the stream and go to the Webhook tab.
  2. In Endpoint URL, enter an https:// URL on the public internet. Private and local addresses aren't accepted.
  3. Under Events, select the events you want. All events are selected by default.
  4. Click Save. The first time you save a URL, the Signing secret is shown once. It starts with whsec_. Store it on your server.
  5. Click Send test to send a test event and check that your endpoint answers.

To get a new secret, click Reset signing secret. The old secret stops working immediately. To stop receiving events, clear the URL and save.

Events

EventConsole labelSent when
deliveredDeliveredThe receiving server accepted the message
deferredDelay (tarpit)A delivery attempt failed temporarily. Sent once per attempt.
bouncedBouncedPermanent failure. data.bounce_type is hard or soft.
complainedComplaintsThe recipient reported the message as spam
failedFailedDelivery was given up after 72 hours
rejectedDenyThe sending server refused a recipient
openedOpenThe first time the message is opened (requires open tracking)
clickedClicksEvery click on a tracked link (requires click tracking)
unsubscribedUnsubscribedThe recipient unsubscribed

Recipients refused by the HTTP API itself are returned in the rejected field of the API response and don't produce a webhook.

Request format

Each event is sent as a POST with a JSON body:

http
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Yeap-TM-Webhook/1
X-Yeap-Signature: t=1760955304,v1=5f2b9c...e81a

{
  "id": "evt_01JAQ40B2C3D4E5F6G7H8J9K0M",
  "type": "bounced",
  "stream_id": 12,
  "message_id": "01JAQ3Z9V8K4M2N6P7R5S1T0XW",
  "created_at": "2026-10-20T10:15:04+00:00",
  "data": {
    "rcpt": "alice@example.org",
    "smtp_code": 550,
    "smtp_response": "5.1.1 The email account that you tried to reach does not exist",
    "bounce_type": "hard",
    "mx": "mx.example.org"
  }
}

id is unique per event: use it to ignore duplicates. What data contains depends on the event:

Eventdata fields
delivered, deferred, bounced, complained, failed, rejectedrcpt, and when available smtp_code, smtp_response, bounce_type, mx, at
openedrcpt, ip, user_agent
clickedrcpt, url, ip, user_agent
unsubscribedrcpt, category, via
testmessage

Verify the signature

The X-Yeap-Signature header has the form t=<timestamp>,v1=<signature>. The signature is the hex HMAC-SHA256 of <timestamp>.<raw request body>, using your full signing secret (including whsec_) as the key. To check a request:

  1. Read the raw body exactly as received, before parsing the JSON.
  2. Compute the HMAC over t + "." + body and compare it with v1 using a constant-time comparison.
  3. Reject requests whose timestamp is more than 5 minutes old, to block replays.

Python:

python
import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Node.js:

javascript
const crypto = require('crypto');

function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const ts = Number(parts.t);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Responses and retries

  • Answer with any 2xx status within 10 seconds. Anything else counts as a failure.
  • Failed deliveries are retried with increasing delays, starting at 1 minute and growing to at most 4 hours between attempts. Retries stop 24 hours after the event.
  • Events can arrive more than once or out of order. Use the event id to skip duplicates and the message status to decide what's current.

The Recent deliveries table on the Webhook tab shows the last 100 deliveries with their event, status (Pending pending, Success succeeded, Retrying retrying, Gave up given up), number of attempts, last error and next attempt time.

Was this page helpful?