Create a webhook endpoint

POST/api/v1/webhooks
Requires anorganization API keywith the scopewebhooks:write

Registers a URL to receive delivery events as they happen.

Delivery is at least once, so the same event can arrive twice — key your handling on the event id and make it idempotent. Events can also arrive out of order; a delivered before the accepted that preceded it is normal and not a bug.

Verify the signature on every request before trusting the body. See Receiving webhooks for the full list of events and their payloads.

What an email.inbound event carries. The sender, the recipients and the subject of an email that arrived, and for some messages its body. It goes to every webhook in your account that subscribes to it and whose filters it passes — so an unfiltered webhook gets the incoming mail of every mailbox on every domain in your account, not only the mailbox your integration uses. domain narrows it to one domain's mailboxes; there is no filter for a single mailbox, so check the recipient on your side if you need one.

Incoming mail needs a domain when your account has more than one. A webhook subscribed to email.inbound must then set domain (one of yours, exact match). Leaving it out answers 422 on domain — on create, on an update that would remove it, and when turning an old webhook back on. With one domain it is optional; when you add a second domain, a webhook without one is tied to the domain you had, so it keeps getting exactly what it got before.

Filters, three ways, one result. The domain can be sent as domain, filters.domain or targeting.domain; the top-level domain wins when it is sent. filters uses the stored names (senderIncludes, subjectContains, bodyContains); targeting uses the names the response returns (sender_includes, ...), so you can send back what you read. Send one of the two, not both. Every call answers with targeting: read it back to confirm what is applied. Anything unknown is a 422 that names it — a field, a filter key, or an event name — never a 200 that quietly ignored it.

email.deferred usually means we are still trying. The receiving server asked us to wait, the message is in our queue, and we retry it for you. One is not ours to retry: when a marketing SMTP key sends to a contact whose inbox is full and Bounce protection is holding marketing to them, the message is refused before it is queued (a 452 that names a date). It is logged deferred, but Mailyte never queued it, so your app must send it again after the date in the reason.

Three account events are about your account, not about a message, so a webhook's domain, sender, subject and body filters never hold them back. They are sent only once Bounce protection is on for your account.

  • contact.checked: a contact's address was checked and the answer changed, once per change. {contact_id, email, email_status, reason, suggestion, checked_at, email_full_until}. email_status is the contact's new value; reason and suggestion read as on a verification result (suggestion is the corrected address for a misspelt domain, else null). unknown with a checker that could not confirm yet is asked again later and may be announced again. email_full_until is the end of a hold on marketing to a full inbox while it runs, else null.
  • checks.running_low: {trigger, plan_left, bought, credit_fallback, forecast_days}. trigger is forecast (under 7 days of checks left at your recent pace) or plan_80 (80% of this month's plan checks used, and none bought). The counts are checks, as on GET /billing/usage.
  • checks.ran_out: {since, buy_path}. A new contact could not be checked because no checks were left; it was added as not_checked. buy_path is the dashboard page that buys the checks we suggest.

The two checks.* events are sent at most once per cycle: a new month, a purchase or checks added by us start a new one.

Request body

  • namestringrequired

    A name for your team, like Production events.

  • eventsarray<string>required

    The events to receive. Each one's payload is in the Receiving webhooks guide.

  • urlstringrequired

    The full HTTPS address we send to. It must be reachable from the internet.

  • filtersobject

    Optional. Send only some events, by what is in the email. A list matches if any of its words appears (case does not matter); leave a list out to match everything. Other keys are refused with a 422. On update this replaces all the filters; leave it out to keep them.

    • domainstring

      The same as the top-level domain.

    • senderIncludesarray<string>

      Only mail whose sender contains one of these.

    • subjectContainsarray<string>

      Only mail whose subject contains one of these.

    • bodyContainsarray<string>

      Only mail whose body contains one of these.

  • targetingobject

    The same filters, spelt as the response returns them, so you can send back what you read. Send filters or targeting, not both.

    • domainstring

      The same as the top-level domain.

    • sender_includesarray<string>

      As filters.senderIncludes.

    • subject_containsarray<string>

      As filters.subjectContains.

    • body_containsarray<string>

      As filters.bodyContains.

  • domainstring

    Only events for this domain, such as yourdomain.com: one of your domains, exact match. Required with email.inbound when your account has more than one domain. Can also be sent as filters.domain or targeting.domain; this field wins.

Request

POST/api/v1/webhooks
curl -X POST 'https://app.mailyte.com/api/v1/webhooks' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Production events",
    "events": [
      "email.delivered",
      "email.bounced"
    ],
    "url": "https://example.com/webhooks/mailyte"
  }'
The key names its own organization, so no X-Organization-ID header is needed.

Response

Success.

  • dataobject
    • objectstringwebhook
    • idstring

      Unique identifier for the webhook.

    • namestring

      Your name for the webhook.

    • urlstring

      Where we send events. It must be a public HTTPS address.

    • statusstringactive | inactive

      An inactive webhook is skipped by the dispatcher. It is not deleted and keeps its delivery history and its secret.

    • eventsarray<string>

      The events this webhook receives. A name not on this list is refused with a 422. quota.warning is an older name for storage.quota.warning and still works, but do not use it for new webhooks. contact.checked, checks.running_low and checks.ran_out are about your account, not one email, so filters never hold them back; they are sent only while Bounce protection is on.

    • targetingobject

      Which events reach this webhook. A field that is null does not narrow anything.

      • domainstring

        Only events for this domain, exact match (acme.com does not match notacme.com). Null means every domain. Always set on an email.inbound webhook in an account with more than one domain.

      • sender_includesarray<string>

        Any-of substring match on the sender. null = not narrowed.

      • subject_containsarray<string>

        Any-of substring match on the subject. null = not narrowed.

      • body_containsarray<string>

        Any-of substring match on the body. null = not narrowed.

    • deliveriesobject

      The delivery history, grouped so the numbers cannot disagree.

      • succeededinteger

        Lifetime tally of accepted deliveries.

      • failedinteger

        Lifetime tally of failed deliveries.

      • consecutive_failuresinteger

        Resets to zero on any success. This is what drives auto-disable, so it answers a different question from failed.

      • success_ratenumber

        The percentage of deliveries that succeeded, 0 to 100. Null when no delivery has been tried yet; show it as "no deliveries yet", not as 0.

    • last_delivered_atstring

      When a delivery to this webhook last succeeded, in ISO 8601 UTC. Null when none has succeeded yet; check deliveries to see whether any were tried.

    • created_atstring

      ISO 8601 UTC.

    • secretstring

      The secret for checking our signatures. **Shown only in the response to POST /webhooks**, and never again: save it now. If you lose it, delete the webhook and make a new one. Check the signature on every request before you trust it.

Returned inside the standard envelope.

Errors

StatusWhen
401The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist.
403The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys.
404No such resource in this organization.
422The request was understood but the values were not acceptable.
429Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait.

Every status, with what causes it and what to do, is on the error reference.