Email OTP and verification codes, without the tracking.
tinyRelay is a verification email API and SMTP relay for one-time codes. Send each code with a single request, turn tracking off for that request with disableTrackingLinks, and get a signed DELIVERED or BOUNCED event back.
Hi Ann, enter this code to finish signing in. It works once.
If you did not try to sign in, you can ignore this email.
Example email and codes · illustrative
A 2FA email in three moves.
Your app owns the code. tinyRelay owns the delivery, and tells you how it went.
Your app makes the code
Generate it, store a hash with an expiry, and rate-limit how often a user can ask. None of that touches the email service.
One request sends it
POST /email/campaign with the code in the recipient's attributes and disableTrackingLinks: true. A 202 returns a requestId.
An event comes back
DELIVERED when the receiving server accepts it, BOUNCED with a category if it does not. Your user ID rides along as metadata.
Why sign-in code emails go out with tracking off.
Open and click tracking are on by default. For a code email they add little and change what you send.
- With tracking on, every link is rewritten through a click URL and an open pixel is added. A magic sign-in link in a code email is rewritten too.
- Without link wrapping, the rewritten link exposes the destination as a readable redirectUrl parameter.
- A CLICK event's url carries the full original link, query string included, so a sign-in token in the link ends up in your webhook data.
- With disableTrackingLinks: true, links are not rewritten and no pixel is inserted. One switch covers both, for the whole request.
Keep tracking on for your other email if you want OPEN and CLICK events there. The setting is per request, so code emails and receipts can differ.
Try the switches
per requestSee what happens to a link in the email as you change the request.
The example uses a reset-style link; the same rewriting applies to any link in a code email. [tracking-domain] stands in for the real tracking host.
Verification email API request, per code.
The code goes in attributes and is merged into the body with a [CODE] tag. Attributes are never echoed on webhooks. Metadata is echoed on every event, so it carries the user ID and never the code.
Rendered into the body. Not sent back to you.
Never rendered. Up to 10 keys, echoed on each webhook.
curl -X POST https://api.tinyemail.com/v1/relay/email/campaign \
-H "X-RELAY-ACCESS-TOKEN: $TINYRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Your Acme sign-in code",
"preview": "Enter this code to finish signing in.",
"body": "<p>Your sign-in code is <strong>[CODE]</strong>. It works once.</p>",
"sender": { "from": { "name": "Acme", "email": "no-reply@acme.com" } },
"recipients": { "to": [
{ "email": "ann@example.com", "attributes": { "CODE": "482913" } }
] },
"metadata": { "user_id": "u_1042", "flow": "sign_in_code" },
"disableTrackingLinks": true
}' import os, requests
resp = requests.post(
"https://api.tinyemail.com/v1/relay/email/campaign",
headers={"X-RELAY-ACCESS-TOKEN": os.environ["TINYRELAY_KEY"]},
json={
"subject": "Your Acme sign-in code",
"preview": "Enter this code to finish signing in.",
"body": "<p>Your sign-in code is <strong>[CODE]</strong>.</p>",
"sender": {"from": {"name": "Acme", "email": "no-reply@acme.com"}},
"recipients": {"to": [{"email": user.email, "attributes": {"CODE": code}}]},
"metadata": {"user_id": user.id, "flow": "sign_in_code"},
"disableTrackingLinks": True,
},
)
# 202 → {"requestId": "…", "message": "SUCCESS"}
request_id = resp.json()["requestId"]Base URL from current site samples; confirm in the docs.
Know when the code landed, or why it did not.
With tracking off there is no pixel to fire OPEN and no rewritten link to fire CLICK. Plan your code flow around SEND, DELIVERED and BOUNCED.
All seven webhook events →- SENDSent to the receiving serverstart the timer
- DELIVEREDThe receiving server accepted it"check your inbox"
- BOUNCEDSOFT_BOUNCED, e.g. category 22, mailbox fulloffer another way
- BOUNCEDHARD_BOUNCED, category 10, bad addressask for a new address
Every event is signed with svix-id, svix-timestamp and svix-signature, and carries your metadata.
Sign-in code email and 2FA email checklist.
Tick what your code emails already do.
Read more on transactional email deliverability, and see all 19 categories in the bounce code reference. Sending reset links rather than codes? See password reset emails. Already have a framework that sends over SMTP? Use the SMTP relay.
Yes. Tracking is set per request. Send your code emails with disableTrackingLinks set to true and leave it out of your other requests. It turns off both link rewriting and the open pixel for that whole request.
No. disableTrackingLinks is a single switch that covers both. There is no documented way to disable one and keep the other, and it cannot be set per recipient.
In the recipient's attributes, rendered into the body with a merge tag such as [CODE]. Attributes are never echoed on webhooks. Metadata is echoed on every event, so keep codes out of it.
No. Your app generates the code, stores it, expires it and checks it. tinyRelay delivers the email and reports what happened to it.
A DELIVERED event tells you the receiving server accepted the message. If it bounced, a BOUNCED event tells you whether it was hard or soft, with one of 19 documented categories.
Send transactional email with a service that reports back.
SMTP relay or REST API, standalone or alongside tinyEmail. Tell us what you send and our team sets you up.