Tether
DocsGitHub

API reference

Mail

Tether sends two kinds of message to a business's registered people: the sign-in link an approval app asks for, and a notice that an action is waiting for their approval. Both are queued with the change that caused them and sent outside the request, with retries, and every attempt is listed for the business. Secret key only for the list.

What is sent, and to whom

MessageSent whenTo
Sign-in linkAn approval app calls POST /v1/user-sessions/sign-inEach active registered user with that address, one message per business, each with its own link to the app's /api/session/verify
Approval waitingA write is staged and waits for a personEach active registered user of the business, with an address, whose role the business's current policy lets approve this action, its conditions evaluated over the staged arguments

Nothing is ever sent to an address that is not registered for an active user of that business, but the embed instructions below, to the one address the owner typed. A message is written in the business's language, its domain pack's language (ka for Georgian, otherwise English), and names the business, so a person registered at two businesses can tell them apart. An approval notice names the action by its capability, says when it expires, and links to the approval app; it carries no sign-in link and none of the action's arguments.

Who receives a message is read again when it is sent. A notice for an action that no longer waits, or to a person whose role may no longer approve it, is skipped. A sign-in link is minted when its message is sent, so it exists only in that message.

An invitation (kind invitation) goes to a person a business invited by email. It names the business, says they were added as someone who approves its actions, and carries a sign-in link to the approval app, minted when it is sent and good once for 24 hours. At most Mail:MaxInvitationsPerHour (20) invitations a business an hour.

The embed instructions (kind embed_instructions, console loop O7) are the one message sent to an address that is not a registered user: the person the owner named under "Send to my developer" (POST /v1/embed/instructions). Tether writes it: the snippet with the workspace's widget key (publishable, read when the message is sent), where to paste it on the common site builders, and the addresses it answers on. It carries no secret. It waits for the confirmed address like any mail to someone else, and a business may ask for it at most Mail:MaxEmbedInstructionsPerHour (5) times an hour.

A self-serve workspace whose address is not confirmed yet mails nobody but that address. An approval notice or an invitation to anyone else is held, and sent when the address is confirmed (a notice only if the action still waits); a sign-in link for anyone else is refused with 403 email_unconfirmed. Held messages are not attempts, so they are not listed here; GET /v1/account/confirmation counts them (heldMail). The confirmation link itself is a message too (kind confirm_address), sealed when it is sent. See Signup.

Where it comes from

Mail is configured by whoever runs Tether, never by a business or a request. One transport:

Mail:TransportEffect
none (default)Nothing is sent or queued. POST /v1/user-sessions/sign-in answers 503 mail_not_configured
consoleDevelopment only: each message, link included, is written to standard output. Refused outside Development
smtpAny SMTP relay
SettingFor smtp
Mail:FromRequired. The sender address
Mail:FromNameOptional display name
Mail:Smtp:Host, Mail:Smtp:PortRequired
Mail:Smtp:SecurityRequired: starttls (refuses a server that does not offer it), tls (implicit TLS) or none
Mail:Smtp:Username, Mail:Smtp:PasswordTogether or not at all, and never with Security none
Mail:Smtp:TimeoutSecondsPer message, default 30
Mail:Smtp:HeloNameOptional: the name sent in EHLO; the machine's name when unset
Mail:MaxSignInMailsPerWindowAt most this many sign-in messages for one person at one business within one link lifetime, default 5; further requests are answered the same and send nothing
Mail:MaxInvitationsPerHourAt most this many invitations (POST /v1/users/invitations) for one business within an hour, default 20; beyond it the route answers 429 invitation_limit
Mail:MaxEmbedInstructionsPerHourAt most this many embed instructions (POST /v1/embed/instructions) for one business within an hour, default 5; beyond it the route answers 429 embed_mail_limit
Mail:Inbox:BaseUrlRequired unless none: the approval app's address, which every link points at. https, or http to the same machine
Mail:Inbox:BaseUrls:<tenant id>Optional: a different address for that tenant and every business it created, for a provider that runs its own approval app

An unknown transport, a missing setting, credentials over plain text or a link address that is not https stops Tether when it starts, with the setting named. A mail server's 5xx is final; a 4xx, a timeout or a lost connection is tried again on the event delivery schedule.

GET /v1/mail/deliveries

Every attempt, newest first. A row names the recipient, never the message.

QueryFilters to
kindsign_in, approval_waiting, confirm_address, invitation or embed_instructions
statusdelivered, retrying, failed or skipped
actionIdThe notices about one action
messageIdEvery attempt at one message. It is also the message's Message-ID
limitAt most 200, default 50
{
  "deliveries": [
    {
      "kind": "approval_waiting",
      "to": "manager@example.com",
      "actionId": "4a025360-68e2-4e81-a355-8bae07cebbf8",
      "status": "delivered",
      "replyCode": 250,
      "error": null,
      "durationMs": 14,
      "attemptedAt": "2026-10-14T21:05:03Z",
      "messageId": "b0d1…",
      "attempt": 1,
      "retryAt": null
    }
  ],
  "count": 1
}

replyCode is the mail server's answer. error says why an attempt did not arrive: refused, timeout, unreachable, tls_required, tls_failed, or skipped with the reason. Mail is not listed by GET /v1/events/deliveries.