COSS

Webhooks

Get told when something happens, instead of polling.

Add webhook endpoints per environment in the dashboard (Webhooks) or through the API. Each endpoint subscribes to event types (all of them by default) and has its own whsec_… signing secret.

const { : ,  } = await ..({
  : "https://example.com/webhooks/coss",
  : ["email.delivered", "email.bounced"],
});
// Store the secret now: it is only returned when the endpoint is created (or its secret rolled).
const  = ?.;

Events

TypeSent when
email.sentThe provider accepted the email
email.deliveredA recipient's mail server accepted it
email.delivery_delayedDelivery to a recipient is being retried
email.bouncedA recipient's mail server rejected it
email.complainedA recipient marked it as spam
email.failedThe email could not be sent at all
email.suppressedA recipient was skipped because it is suppressed

Organization events

These concern the whole organization, so every live environment's endpoints receive them (sandbox endpoints don't). data.organization_id names the organization.

TypeSent when
account.reputation_warningA reputation check (bounces, complaints, content) crossed its warning threshold
account.reputation_recoveredThe warning cleared
account.suspendedLive sending was suspended
account.reinstatedThe suspension was lifted
billing.invoice_finalizedAn invoice was issued (data.invoice, without lines)
billing.invoice_paidAn invoice was paid in full
billing.invoice_payment_failedCharging an invoice failed (data.failure.message); it is retried
billing.invoice_voidedAn open invoice was voided
billing.plan_changedThe plan changed or a change was scheduled or undone (change, plan, effective_at)
billing.usage_threshold_reachedUsage reached an alert threshold of the month's credit or Free's allowance (percent)
billing.hold_placedLive sending was paused for billing (reason: delinquent, spend_limit or admin)
billing.hold_liftedThat pause ended
billing.credit_grantedA credit was added (data.credit)

See Plans and billing for what they mean and how to read the rest of an organization's billing.

The request body is a JSON event. Payloads describe the email (email_id, from, to, subject, tags, category) but never include its content, headers, cc or bcc. Events are rendered in the endpoint's API version, named in api_version.

{
  "id": "evt_01k7c8m4q2f3e9v6t5r1n0p8yz",
  "object": "event",
  "type": "email.delivered",
  "api_version": "2026-10-10",
  "created_at": "2026-10-10T12:00:01.000Z",
  "data": {
    "email_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz",
    "from": "Acme <hello@acme.com>",
    "to": ["user@example.com"],
    "subject": "Welcome",
    "tags": [],
    "category": null,
    "sandbox": false,
    "recipient": "user@example.com",
    "occurred_at": "2026-10-10T12:00:00.000Z",
    "delivery": { "smtp_response": "250 2.0.0 OK" }
  }
}

Verifying requests

Requests are signed with Standard Webhooks headers: webhook-id, webhook-timestamp and webhook-signature. Always verify them before trusting the body. With the SDK, use verifyWebhook.

Rolling the secret

Roll an endpoint's secret in the dashboard (Roll secret on the endpoint) or through the API to replace it, for example on a schedule or after it may have leaked. For a grace period (a day by default, up to a week) every request carries two signatures, one per secret, in webhook-signature, so a receiver verifying with either one keeps accepting requests while you deploy the new secret. Choose 0 to stop the old secret at once.

const { :  } = await ..(, {
  : 60 * 60, // the old secret keeps signing for an hour
});
const  = ?.; // deploy it before rolled?.previous_secret_expires_at

Rolling again during the grace period stops the older secret immediately. The SDK never retries a roll: if the response is lost, roll again and deploy that secret.

Delivery and retries

  • Respond with any 2xx status within 10 seconds to acknowledge an event.
  • Failed deliveries are retried 7 times over about 28 hours (5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h).
  • Events can arrive out of order: sort by data.occurred_at (every event has it).
  • An event can arrive more than once: deduplicate by id (equal to the webhook-id header).

Delivery logs

Every event sent to an endpoint is logged as a delivery, with each HTTP attempt (status code, duration, error and the first 1 KB of your response) and the payload as sent. Find them on the endpoint's page in the dashboard, or list them through the API — newest first, filtered by status or event_id:

const { :  } = await ...(, {
  : "failed",
});

// After fixing your receiver, send a failed event again.
for (const  of ?. ?? []) {
  await ...(, .);
}

A retry makes one more attempt within seconds, signed with the endpoint's current secret and sent to its current URL. Deleting an endpoint deletes its delivery log, and deliveries are deleted 30 days after the event (see data retention).

On this page