List delivery events

GET/api/v1/email-logs
Requires anorganization API keywith the scopeevents:read

Every event recorded for your organization's mail: accepted, delivered, deferred, bounced, complained, opened, clicked, dropped.

Polling this is the simple way to follow a send. Receiving webhooks is the better one — it costs you no requests and tells you sooner.

Opens and clicks are not reliable, by nature. Open tracking depends on a remote image loading, which privacy proxies fetch on the recipient's behalf and many clients block entirely. Treat them as a weak signal about a population, never as proof about a person.

Request

GET/api/v1/email-logs
curl -X GET 'https://app.mailyte.com/api/v1/email-logs' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY'
The key names its own organization, so no X-Organization-ID header is needed.

Response

Success.

  • dataobject
    • dataarray<object>
      • objectstringevent
      • idstring

        This event, and the value `GET /email-logs/{event}` takes. The ingest dedupe key that used to be published as `event_id` resolved nowhere and is gone.

      • typestringemail.submitted | email.accepted | email.delivered | email.deferred | email.bounced | email.dropped | email.rejected | email.inbound | tracking.open | tracking.click | tracking.unsubscribe | delivery.complaint | spam.reported | storage.quota.warning | storage.quota.exceeded | quota.warning

        What happened. Published verbatim, so the value is exactly what `?event_type=` accepts -- a prettier spelling would not match the filter. `email.submitted` is ours, written when we accept a message; the rest arrive from the mail server. A complaint is `delivery.complaint` and has no second name. `quota.warning` is a legacy alias of `storage.quota.warning` that still occurs on older rows. The `storage.*` and `spam.reported` names concern a mailbox rather than one of your messages and share this stream; filter them out with `?event_type=` if you only want delivery.

      • recipientstring

        Who the event concerns. NULL MEANS UNKNOWN -- we could not read the address -- and never that the event had no recipient. Every event WE write fills it from the address you sent to, so this can only be null on an event ingested from the mail server, where the address is resolved best-effort from the webhook payload's `email`, `recipient`, `mailbox` or `to` key: a producer that names it anything else leaves us with nothing to record. Do not treat a null here as a message with no destination, and do not use it to exclude rows from a per-recipient total -- it undercounts.

      • detailobject

        The extras that only some event types carry. Grouped rather than flat: on a plain delivery all four are null, and flat they read as four missing fields instead of one absent category.

        • ip_addressstring

          Who opened or clicked. Null on everything else.

        • user_agentstring

          The client that opened or clicked, as it identified itself. NULL MEANS NONE: the event reached us carrying no user agent, which is ordinary -- an image proxy or a privacy-preserving client sends none, and every event type other than `tracking.open` and `tracking.click` has no agent to carry. It is NOT a field we failed to read and not one we withhold from a list: this is the whole of what arrived. Null here beside a non-null `ip_address` is the normal shape of an open, not a partial record.

        • urlstring

          The link that was clicked. Null unless `type` is `tracking.click`. This is where a click's target lives -- the detail endpoint's old `clicked_links` array is gone.

        • reasonstring

          Why a bounce bounced or a drop dropped, as the producer worded it. Free text, not an enum: it comes from the receiving mail server. NULL MEANS NONE -- nothing went wrong with this event, so there is no reason to give. A delivery, an open and a click all read null here and that is the success case, not a missing explanation. On a `email.bounced`, `email.dropped` or `email.deferred` event a null is worth noticing: the producer sent us no wording, and there is nowhere else to recover it from.

      • messageobject

        The message this event is about. Byte-identical in shape to `GET /email-logs/{event}` and `GET /email-logs/messages/{id}`, minus the body -- a page of events is not a page of message bodies. ITS CONTENT FIELDS ARE READ OFF THIS ROW'S OWN PAYLOAD, and only an `email.submitted` row carries content, so on a delivered/opened/clicked/dropped row `subject`, `from` and `tags` are null -- NOT INCLUDED, not absent from the message. `GET /email-logs/{event}` resolves the submission and returns them. `id` and `message_id` are real on every row.

        • idstring

          The opaque handle for the message: a BARE ULID, no host and no angle brackets. It is the value `GET /email-logs/messages/{id}` takes. It is NOT the Message-ID -- `message_id` below is a different string for the same message, and the two endpoints do not accept each other's. NULL WOULD MEAN UNKNOWN, and it is not a case you have to code for: this is `COALESCE(message_key, id)`, so an event that carries no message key falls back to its own row id and a persisted event always has one. If you ever do see null we could not identify the message at all, and there is no timeline to fetch -- it never means "this message has no handle".

        • message_idstring

          The RFC 5322 Message-ID, ANGLE BRACKETS INCLUDED -- `<01JBT8XQ2M...@example.com>`. This is the exact header that went out on the wire, so it is the string in the recipient's headers and in every bounce and DSN about the message, and the one `GET /domains/{domain}/messages/{messageId}` accepts (percent-encode it in the path). Handing `id` to the archive, or this to `/email-logs/messages/{id}`, returns 404 -- they are two identifiers doing two jobs. NULL MEANS NONE: the message genuinely never carried a `Message-ID:` header, which is normal for an `email.dropped` event -- we refused it before anything was minted, so there is no header to report and there never will be. It is not a failed lookup and not a withheld value, and a value is never fabricated to fill the field. (This resource did fabricate one on its first run against real data, publishing `<some-row-id@domain>` from the coalesced handle; the un-coalesced key exists to keep the two apart.)

        • subjectstring

          The subject of the original submission. What null means depends on which endpoint you are reading -- see the note on this block. On a list row that is not the submission it is NOT INCLUDED: we hold the subject, this row just is not where it lives, and `GET /email-logs/{event}` or `GET /email-logs/messages/{id}` returns it. Where the submission IS the source, null means NONE -- no submitted event was ever recorded for this message, because every event we have for it arrived by webhook.

        • fromobject
        • tagsarray<string>

          Your own labels, as supplied at send time. THE EMPTY ARRAY AND THE NULL ARE DIFFERENT ANSWERS AND BOTH ARE DELIBERATE. `[]` is NONE and a real answer: this message was submitted with no tags. `null` is NOT INCLUDED: this response was built from an event that is not the submission -- a delivery, an open, a click, a drop -- so we are not telling you what the tags were, and they may well be non-empty. Until 2026-09-18 both cases published `[]`, so a list row flatly denied tags that `GET /email-logs/{event}` returned for the same event. Do not read `[]` as "untagged" without checking you are on an endpoint that reads the submission. Not to be confused with a template's `variables`, which this section used to call `tags` too.

        • metadataobject

          The metadata you set at send time, returned unchanged and never rendered into the message. This is what makes an event correlatable to your own record -- put your order id here and it comes back on every delivery, open and bounce for this message, so you do not have to store our `message_id` against your row first. NOT to be confused with `variables`, which ARE substituted into the subject and bodies and never appear here. `{}` is NONE -- the submission carried no metadata. `null` is NOT INCLUDED -- this event is not the submission, so we are not telling you what it was.

      • created_atstring

        When WE RECORDED the event, which for an ingested webhook is not necessarily when the thing happened. Named `created_at` rather than `occurred_at` because a name promising otherwise is one we cannot keep. NULL WOULD MEAN UNKNOWN and is not a case to code for: the column is `NOT NULL DEFAULT current_timestamp()`, so every stored event has one. It would never mean the event has not happened yet -- there is no such event.

    • pagination_metaany

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.