Retrieve a email log
/api/v1/email-logs/{event}Parameters
| Name | In | Type | Description |
|---|---|---|---|
eventrequired | path | string | The event identifier. |
Request
/api/v1/email-logs/{event}curl -X GET 'https://app.mailyte.com/api/v1/email-logs/01JBT8XQ2M9WYC3K4F6R7S8T9V' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/email-logs/01JBT8XQ2M9WYC3K4F6R7S8T9V', {
method: 'GET',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
},
});
const { data } = await response.json();import requests
response = requests.get(
"https://app.mailyte.com/api/v1/email-logs/01JBT8XQ2M9WYC3K4F6R7S8T9V",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->get('https://app.mailyte.com/api/v1/email-logs/01JBT8XQ2M9WYC3K4F6R7S8T9V');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/email-logs/01JBT8XQ2M9WYC3K4F6R7S8T9V")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
Success.
dataobjectobjectstringeventidstringThis event.
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.warningWhat 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.
recipientstringWho 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.
detailobjectThe 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_addressstringWho opened or clicked. Null on everything else.
user_agentstringThe 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.
urlstringThe 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.
reasonstringWhy 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.
messageobjectThe message this event is about, WITH its body -- the reason people open this endpoint. The body is the original submission's, not this row's: a click or an open carries no content of its own. `message.id` is the pointer to the full timeline, which is why this response no longer repeats it as `history` and `clicked_links`. THIS ENDPOINT RESOLVES THE SUBMISSION, so `subject`, `from` and `tags` are the message's real values here and null means the submission genuinely had none -- unlike `GET /email-logs`, where the same fields are read off each row's own payload and are null on any row that is not the submission.
idstringThe 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_idstringThe 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.)
subjectstringThe 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.
fromobjectemailstringThe sending address. Same rule as `subject`: NOT INCLUDED on a list row that is not the submission -- fetch the event or the timeline -- and NONE where the submission is the source and no submitted event was ever recorded. Read on its own it is also the one honest source of the message's domain; the `domain` field that used to sit beside it was a second copy that could disagree.
namestringThe sender's display name. Null is NONE where the submission is the source -- the message was sent without one, which is common -- and NOT INCLUDED on a list row that is not the submission, exactly as `from.email` beside it.
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.
metadataobjectThe 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.
htmlstringThe rendered HTML body of the ORIGINAL submission, not of the event you asked about -- a click or an open carries no body of its own. NULL MEANS NONE, and this field is only ever published by an endpoint that resolves the submission, so it is never a withheld value: either no submitted event was ever recorded for this message (every event we hold arrived by webhook) or the submission was text-only. `text` beside it tells you which.
textstringThe plain-text body. NULL MEANS NONE, on the same terms as `html`: no submitted event was recorded, or the submission carried no text part. Note that a normal send cannot produce the second case -- MessageSendService derives a text alternative from the HTML when the caller supplies none -- so null here with a non-null `html` means the row predates that. Spelled `text` on every read in this section; the template WRITE side is the one exception and still takes `plain_text`.
created_atstringWhen 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.
Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The 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. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too 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.