Перейти к документации
API

Отслеживание

Каждая операция этой группы: что она принимает, что возвращает и какими ошибками может ответить.

Операции

Who read the mail and what they followed. Off unless the workspace turned it on, per user and per send.

Every count here is a floor rather than a total, and that is the mechanism rather than a defect: an open is inferred from a mail client fetching an image, so a reader whose client blocks images reads without being counted, and Gmail fetches the image once through its own proxy and serves every later view from cache. Mail from one OpenEmail mailbox to another never reports an open at all. OpenEmail strips 1×1 images out of what its own users read, and it makes no exception for its own pixel. A message with no opens has not been shown to be unread; a click is the stronger evidence, because links go unfollowed far less often than images go unloaded.

GET/emails/{id}/tracking

How a message was read

Разрешенияemails:readЧитает

The same document /tracking/{id} serves, reached from the id a caller already holds. A message that was never tracked is a 404 here rather than an empty report, because "we were not recording" and "nobody opened it" are different answers and a client that renders them the same way makes a claim about a reader on no evidence.

Requires the emails:read scope.

Параметры пути

idstringОбязательно

The msg_ send id returned by send.

Возвращает

The full report, with per-recipient and per-link detail.

Ошибки

404

No such message, or nothing was tracked for it.

Ошибки, которые может вернуть любая операция400401403422500Каталог ошибок

Также доступно в

SDK
emails.getTracking()
CLI
openemail emails get-tracking
MCP
getEmailTracking

GET/tracking

List tracked messages

Разрешенияemails:readЧитает

Every message the workspace tracked in the window, newest first, one page at a time, not only the ones sent through this API. The composer, the MCP tools and the assistant mostly send through the mailbox agent, which writes the send record itself and never links it to the tracking row, so those messages carry no counts on /emails and only this list holds them.

opened=false means tracked and not opened. Messages that carried no pixel are absent from this list entirely and never appear as a zero. Follow nextCursor with the same filters to reach every tracked message in the window.

Requires the emails:read scope.

Параметры запроса

openedboolean

Omit for both. false narrows to tracked-and-unopened.

clickedboolean

Omit for both. true keeps messages with a counted click and false keeps those without.

daysinteger

How far back to look. The window starts at the beginning of that day rather than at this time of day, and ends now.

Не меньше 1Не больше 365По умолчанию30
minutesinteger

The window in minutes, which wins over days when both are sent. A whole number of days cannot say "the last hour", which is the report worth having while a send is going out.

Не меньше 1Не больше 527040
grainstring

Only used to floor the start of the window, so this list can cover the same window as /tracking/stats read at the same grain. It shapes nothing in the response.

Одно из"minute""hour""day"По умолчанию"day"
limitinteger

Rows per page, 1 to 200.

Не меньше 1Не больше 200По умолчанию50
cursorstring

A tmsg_ tracking id. Keyset, not offset: pass the previous page's nextCursor. One that names nothing in this list is a 400 invalid_cursor.

Возвращает

A page of tracked messages, newest first.

Ошибки

Ошибки, которые может вернуть любая операция400401403404422500Каталог ошибок

Также доступно в

SDK
tracking.list()tracking.listAll()tracking.iterate()
CLI
openemail tracking list
MCP
listTrackedEmails

GET/tracking/stats

Engagement over a window

Разрешенияemails:readЧитает

The numbers behind an engagement panel, in one request. Rates are over tracked messages and count distinct messages; the totals count hits. Read the field descriptions before charting any of it. The two are easy to mix and the result is an open rate above 100%.

A key is the workspace's own authority and sees the whole workspace. The per-member address grants that narrow this in the app belong to a session, and a key has none.

Requires the emails:read scope.

Параметры запроса

daysinteger

How far back to look. The window starts at the beginning of that day in the offset you asked for, so the oldest byDay bucket is a whole one, and ends now, so the newest is partial.

Не меньше 1Не больше 365По умолчанию30
minutesinteger

The window in minutes, which wins over days when both are sent. A whole number of days cannot say "the last hour", which is the report worth having while a send is going out.

Не меньше 1Не больше 527040
grainstring

How wide one byDay bucket is. An hour of mail bucketed by day is a single entry, so a window shorter than a day is only worth asking for alongside a grain that can describe it. The bucket keys change shape with it: YYYY-MM-DD for a day, YYYY-MM-DDTHH for an hour, YYYY-MM-DDTHH:MM for a minute.

Одно из"minute""hour""day"По умолчанию"day"
offsetMinutesinteger

Minutes east of UTC to bucket byDay in, so days break where the reader's day breaks. Left at zero, this morning's mail lands on yesterday's bar for anyone west of UTC.

Не меньше -840Не больше 840По умолчанию0

Возвращает

The window, summarised. byDay is sparse.

Ошибки

Ошибки, которые может вернуть любая операция400401403404422500Каталог ошибок

Также доступно в

SDK
tracking.getStats()
CLI
openemail tracking get-stats
MCP
getEngagementStats

GET/tracking/{id}

Retrieve one message's tracking

Разрешенияemails:readЧитает

The whole report: per-recipient counts where the transport could attribute them, and every rewritten link with its clicks.

Requires the emails:read scope.

Параметры пути

idstringОбязательно

A tmsg_ tracking id or the msg_ send id the message went out as. Both work, because a caller who sent through this API holds the second and has no reason to know the first exists.

Возвращает

The report.

Ошибки

404

Nothing was tracked for that message. Not the same as nobody having opened it.

Ошибки, которые может вернуть любая операция400401403422500Каталог ошибок

Также доступно в

SDK
tracking.get()
CLI
openemail tracking get
MCP
listTrackedEmails

GET/tracking/{id}/opens

The individual opens

Разрешенияemails:readЧитает

The raw hits behind openCount, newest first, one page at a time. Every stored hit is reachable by following nextCursor.

A msg_ send id is resolved first, and one that names nothing is a 404 rather than an empty list. An empty list reads as "nobody opened it", which is the one answer this endpoint must never give by accident. A tmsg_ is taken as given and costs no lookup, so an id that never existed, or one belonging to another workspace, does come back here as an empty list. Only ids this API handed you can be read as an answer about a reader.

Requires the emails:read scope.

Параметры пути

idstringОбязательно

A tmsg_ tracking id or the msg_ send id the message went out as. Both work, because a caller who sent through this API holds the second and has no reason to know the first exists.

Параметры запроса

includeMachineboolean

Include the hits that did not count: Apple Mail Privacy Protection, corporate scanners, and the repeats collapsed by the thirty-second window. Off by default, and that default is the honest one. Those fetches are stored so the gap between the raw and counted totals stays inspectable, not because anybody read anything. Gmail's image proxy is not among them and is never hidden by this. Send true or false: ?includeMachine=false means false here, which is worth saying because the obvious coercion would make it true.

По умолчаниюfalse
limitinteger

Rows per page, 1 to 200.

Не меньше 1Не больше 200По умолчанию50
cursorstring

An opn_ open id from this message. Keyset, not offset: pass the previous page's nextCursor. One that names nothing in this list is a 400 invalid_cursor.

Возвращает

A page of opens, newest first.

Ошибки

Ошибки, которые может вернуть любая операция400401403404422500Каталог ошибок

Также доступно в

SDK
tracking.listOpens()tracking.listAllOpens()tracking.iterateOpens()
CLI
openemail tracking list-opens
MCP
getEmailTracking

GET/tracking/{id}/clicks

The individual clicks

Разрешенияemails:readЧитает

As the opens, plus which link was followed, one page at a time. Only links in the new part of the body were rewritten, so nothing here can be a click on quoted history.

Requires the emails:read scope.

Параметры пути

idstringОбязательно

A tmsg_ tracking id or the msg_ send id the message went out as. Both work, because a caller who sent through this API holds the second and has no reason to know the first exists.

Параметры запроса

includeMachineboolean

Include the hits that did not count: Apple Mail Privacy Protection, corporate scanners, and the repeats collapsed by the thirty-second window. Off by default, and that default is the honest one. Those fetches are stored so the gap between the raw and counted totals stays inspectable, not because anybody read anything. Gmail's image proxy is not among them and is never hidden by this. Send true or false: ?includeMachine=false means false here, which is worth saying because the obvious coercion would make it true.

По умолчаниюfalse
limitinteger

Rows per page, 1 to 200.

Не меньше 1Не больше 200По умолчанию50
cursorstring

A clk_ click id from this message. Keyset, not offset: pass the previous page's nextCursor. One that names nothing in this list is a 400 invalid_cursor.

Возвращает

A page of clicks, newest first.

Ошибки

Ошибки, которые может вернуть любая операция400401403404422500Каталог ошибок

Также доступно в

SDK
tracking.listClicks()tracking.listAllClicks()tracking.iterateClicks()
CLI
openemail tracking list-clicks
MCP
getEmailTracking

Объекты

Clickobject

objectstring
Одно из"click"
idstring
trackedMessageIdstring
recipientstring

Null for the same reason recipients[].email is: shared bytes, unknowable reader.

Может быть null
kindstring

human looked like somebody reading. proxy is an image proxy, Gmail's above all: a genuine reading by a reader we cannot see, counted once and then cached out of our sight. machine is a scanner or Apple Mail Privacy Protection, which fetches on delivery and means only that the message arrived.

Одно из"human""proxy""machine"
countedboolean

Whether this hit moved the numbers. False for every machine hit, and false again for a hit landing within thirty seconds of the last counted one on the same copy. A preview pane redrawing is the same reading rather than a second one. includeMachine filters on this field and not on kind, so it is what hides those collapsed repeats as well.

clientstring
Может быть null
devicestring
Может быть null
osstring

Read out of the User-Agent, which is a claim rather than a fact and is absent altogether on plenty of hits.

Может быть null
countrystring

What the edge already knew about the request, so this is as precise as the location will ever be, and on a proxied hit it is the proxy's country rather than the reader's. There is no ip field on this resource, but the address is not discarded: it is written into the event log for the message, readable at GET /emails/{id}/events, and into the email.opened and email.clicked webhook payloads.

Может быть null
regionstring
Может быть null
citystring
Может быть null
createdAtstring
Форматdate-time
linkIdstring
urlstring

The destination that was followed.

ClickListobject

objectstring
Одно из"list"
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

The id of the last row on this page, or null on the last page.

Может быть null

Openobject

objectstring
Одно из"open"
idstring
trackedMessageIdstring
recipientstring

Null for the same reason recipients[].email is: shared bytes, unknowable reader.

Может быть null
kindstring

human looked like somebody reading. proxy is an image proxy, Gmail's above all: a genuine reading by a reader we cannot see, counted once and then cached out of our sight. machine is a scanner or Apple Mail Privacy Protection, which fetches on delivery and means only that the message arrived.

Одно из"human""proxy""machine"
countedboolean

Whether this hit moved the numbers. False for every machine hit, and false again for a hit landing within thirty seconds of the last counted one on the same copy. A preview pane redrawing is the same reading rather than a second one. includeMachine filters on this field and not on kind, so it is what hides those collapsed repeats as well.

clientstring
Может быть null
devicestring
Может быть null
osstring

Read out of the User-Agent, which is a claim rather than a fact and is absent altogether on plenty of hits.

Может быть null
countrystring

What the edge already knew about the request, so this is as precise as the location will ever be, and on a proxied hit it is the proxy's country rather than the reader's. There is no ip field on this resource, but the address is not discarded: it is written into the event log for the message, readable at GET /emails/{id}/events, and into the email.opened and email.clicked webhook payloads.

Может быть null
regionstring
Может быть null
citystring
Может быть null
createdAtstring
Форматdate-time

OpenListobject

objectstring
Одно из"list"
dataOpen[]
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

The id of the last row on this page, or null on the last page.

Может быть null

Trackingobject

Every /tracking endpoint returns this whole, its list included. It arrives trimmed in exactly one place, under tracking on a GET /emails row, where it is the counts half only: opens, clicks, opened, clicked, openCount, clickCount and firstOpenAt. A page of fifty sends each carrying its recipients and its links is a report nobody asked to have expanded. The trimmed form has no id on it either, so /emails/{id}/tracking rather than /tracking/{id} is the way back to the rest of it.

objectstring

Present when the report is the whole response body. Absent under an email's tracking field, which is part of that email rather than a resource in its own right.

Одно из"tracking"
idstring

The tracking record, tmsg_ + 24 hex. Not the message id and not the send id.

sendIdstring

The msg_ this went out as, when the send service handled it. Null for mail the mailbox agent sent on its own behalf, which is most composer, MCP and assistant traffic. Those messages do get a send record, but nothing links this tracking row to it. Tracking covers the mailbox rather than only the traffic that came through this API.

Может быть null
threadIdstring
Может быть null
messageIdstring

RFC 5322 Message-ID. Not a correlation key. The header is rewritten on the way out. Correlate on id.

Может быть null
subjectstring
Может быть null
fromstring
sourcestring
Одно из"api""oauth""composer""mcp""ai""form"
sentAtstring
Может быть nullФорматdate-time
opensboolean

What was APPLIED to this message, resolved when it was sent from the setting of the address it was sent from (its own, else its domain catch-all's, else off, while a broadcast copy is on unless the broadcast or its address turned it off) and any per-send override, not what is switched on now. Turning tracking on today does not make yesterday's mail start reporting, and a report that implied otherwise would read as "nobody opened it".

clicksboolean

As opens, for link rewriting. The two are independent switches.

openedboolean
clickedboolean
attributableboolean

Whether every reading on this message can be pinned to a named recipient. False as soon as an unattributed copy has activity of its own, which is what happens whenever one body went to the whole list rather than a separate one per person. This is the flag that decides whether "Bob has not opened it" is a sentence a client is entitled to write, or whether all it may say is that somebody did. recipients carries the same fact one row at a time, and one row at a time is where it gets missed.

openCountinteger

Opens that looked like a person, with repeat fetches within thirty seconds collapsed. A preview pane redrawing is not a second reading. Through Gmail this is a floor and not a total: its proxy fetches the image once and caches it, so later readings never reach us.

clickCountinteger

Counted clicks. Stronger evidence than an open, and worth weighting as such: images are blocked far more often than links go unfollowed, so a message with clicks and no opens was certainly read.

openCountRawinteger

Every open hit, the automated ones included. openCountRaw - openCount is everything that was filtered out: Apple Mail Privacy Protection and corporate link scanners, which fetch on delivery whether or not a person ever looks, and alongside them the repeat fetches collapsed by the thirty-second window. Both are recorded and neither is counted, because discarding them outright would leave a gap in the log that nothing could explain. Do not read the difference as a machine count on its own. A message reopened twice in a minute lands in it too.

clickCountRawinteger

As openCountRaw, for clicks.

firstOpenAtstring
Может быть nullФорматdate-time
lastOpenAtstring
Может быть nullФорматdate-time
firstClickAtstring
Может быть nullФорматdate-time
lastClickAtstring
Может быть nullФорматdate-time
recipientsobject[]

One entry per tracked copy, which is not always one entry per person. Absent only from the trimmed form on a GET /emails row; every /tracking response carries it, list included.

emailstring

Null where the bytes could not be varied per person: an encrypted message, one too large to rebuild for each recipient, or a fallback carrier that takes the whole recipient list in a single call. The reading is real; which of the recipients did it is not knowable, and the only honest rendering is "someone on this message", never a name chosen out of the list.

Может быть null
kindstring
Может быть nullОдно из"to""cc""bcc"
attributedboolean

False on exactly the rows described above. Branch on this rather than on email being a string, and show nothing where it is false: attributing an unattributed open to a named recipient invents evidence about a specific person.

openCountinteger
clickCountinteger
firstOpenAtstring
Может быть nullФорматdate-time
lastOpenAtstring
Может быть nullФорматdate-time
firstClickAtstring
Может быть nullФорматdate-time
lastClickAtstring
Может быть nullФорматdate-time
linksobject[]

The rewritten links, in the order they appeared in the message. Only links in the new part of the body are here: the quoted history under a reply belongs to whoever wrote it, and routing their URLs through our redirector would both rewrite their message and record the recipient "clicking" something we did not put there. Repeated destinations share one entry, because a campaign page linked from a header image, a button and a footer is one question asked three times. Absent only from the trimmed form on a GET /emails row.

idstring
urlstring

Where it actually goes: the original href.

labelstring

The text the link read as in the message, where it had any. A bare URL rarely tells the sender which of five links somebody followed.

Может быть null
clickCountinteger
clickCountRawinteger

TrackingListobject

objectstring
Одно из"list"
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

The id of the last row on this page, or null on the last page.

Может быть null

TrackingStatsobject

objectstring
Одно из"tracking_stats"
trackedinteger

Messages in the window that carried a pixel or a rewritten link.

openedinteger

Of those, how many a person opened at least once. Distinct MESSAGES, not hits. A message opened five times is one opened message, and conflating the two is how open rates above 100% get published.

clickedinteger
trackedForOpensinteger

The denominator of openRate. Tracking is two switches rather than one, so this is the messages that actually carried a pixel, not every tracked message.

trackedForClicksinteger

The denominator of clickRate, and the reason it is published. A message with no links is never click-tracked, so counting it against the click rate makes that rate a measure of how much of your mail contains a link. Most mail is a reply with no links, so the difference from tracked is usually large.

openRatenumber

A percentage of trackedForOpens, deliberately not of everything sent. A workspace that tracks one message in ten has an open rate for those ten; dividing by all its mail produces a number that falls every time somebody sends an untracked reply, which is not a fact about how anyone is reading.

clickRatenumber

A percentage of trackedForClicks, as openRate is of trackedForOpens.

totalOpensinteger

Hits rather than messages. Labelled as a total because it is the easy one to misread.

totalClicksinteger
machineOpensinteger

Hits excluded from every number above: the sum of openCountRaw - openCount, so Apple Mail Privacy Protection and corporate scanners together with the repeats the thirty-second window collapsed. Reported rather than hidden, because a reader who cannot see how much of the traffic was machinery has no way to judge the rest of the panel. Gmail's image proxy is not in this number: that fetch is a real person displaying the message, and it is counted.

medianTimeToOpenSecondsinteger

Median seconds from send to first counted open, over the messages that were opened at all. Null when none were. A median over an empty set is not zero.

Может быть null
byDayobject[]

SPARSE. A day on which nothing was sent has no entry rather than a row of zeroes, so a chart has to fill the gaps itself. Days break at offsetMinutes east of UTC, so that they break where the reader's day does rather than where the database's does.

daystring

The bucket, in the requested offset, written in the shape grain asked for: YYYY-MM-DD for a day, YYYY-MM-DDTHH for an hour, YYYY-MM-DDTHH:MM for a minute. The field keeps its name at every width because it is the bucket key whatever the bucket is.

sentinteger
openedinteger
clickedinteger
topLinksobject[]
urlstring
labelstring
Может быть null
clickCountinteger
clientsobject[]

Mail clients by counted opens, as far as a User-Agent can be trusted to name one.

clientstring
countinteger
countriesobject[]
countrystring
countinteger