A password reset email API that tells you what happened.
tinyRelay sends your password reset emails over a REST API or SMTP relay, then posts a signed webhook event back for each one: sent, delivered, bounced or clicked. Attach the user ID as metadata and it comes back on every event.
Reset email simulation · illustrative
The user is waiting. Your app should know why.
A reset email is sent at the exact moment someone is locked out. When it never arrives they try again, give up or open a support ticket. A reset email API is only half the job; the other half is hearing back.
Did it arrive?
A DELIVERED event means the receiving server accepted the message. Show "check your inbox" with confidence, or a different message if it did not come.
Why did it bounce?
A BOUNCED event carries HARD_BOUNCED or SOFT_BOUNCED and one of 19 categories, so a typo'd address and a full mailbox get different answers.
Which user was it?
Attach up to 10 metadata keys, such as user_id, to the send. They come back on every webhook event for that email.
One reset, four steps, start to finish.
Tap a step. Steps 1 and 4 run in your app; tinyRelay handles the sending in between.
Generate a token in your app
Reset tokens are your app's job, not the email service's. Create a random, single-use token, store only a hash of it with an expiry time, and build the reset link from it. tinyRelay never sees your user table; it sends the link you hand it.
- Random and long enough that it cannot be guessed
- Single use: delete it as soon as it is redeemed
- Short expiry, enforced on your server
import crypto from "node:crypto";
// 1. A random token for the link
const token = crypto.randomBytes(32).toString("base64url");
// 2. Store only its hash, with an expiry you choose
const hash = crypto.createHash("sha256").update(token).digest("hex");
await db.resets.insert({ userId, hash, expiresAt });
// 3. The link that goes in the email
const link = `https://acme.com/reset?t=${token}`;Send one request
Call POST /email/campaign with your API key in the X-RELAY-ACCESS-TOKEN header. The first name goes in attributes and is merged into the body; the user ID goes in metadata and is echoed on every event. A 202 response returns a requestId.
- No campaignId: the generated requestId keeps this reset's metadata separate
- X-Link-Wrapping: true hides the destination inside a token
- Works from any language that can make HTTP requests
curl -X POST https://api.tinyemail.com/v1/relay/email/campaign \
-H "X-RELAY-ACCESS-TOKEN: $TINYRELAY_KEY" \
-H "X-Link-Wrapping: true" \
-H "Content-Type: application/json" \
-d '{
"subject": "Reset your Acme password",
"preview": "This link works once.",
"body": "<p>Hi [FIRST_NAME], <a href=\"https://acme.com/reset?t=8f1c…\">reset your password</a>.</p>",
"sender": { "from": { "name": "Acme", "email": "no-reply@acme.com" } },
"recipients": { "to": [
{ "email": "ann@example.com", "attributes": { "FIRST_NAME": "Ann" } }
] },
"metadata": { "user_id": "u_1042", "flow": "password_reset" }
}'Base URL from current site samples; confirm in the docs.
What arrives
The recipient sees your email from your own domain, signed with DKIM through the one CNAME you add at setup. Their first name is merged in from attributes; the metadata stays out of the message entirely.
- [FIRST_NAME] replaced with "Ann"
- The button's link is rewritten for click tracking, unless you turn tracking off
- With link wrapping on, the reset URL sits inside an opaque token
Example email · illustrative
Handle BOUNCED and CLICK
Events arrive at your webhook endpoint, signed with svix-id, svix-timestamp and svix-signature headers. Verify the signature, then act on what the event says.
- BOUNCEDHARD_BOUNCED, category 10: tell the user the address failed and stop sending to itstop
- BOUNCEDSOFT_BOUNCED, e.g. 22 mailbox full: offer a "send again" buttonretry
- CLICKThe link was clicked: log it, but your token check decideslog
{
"eventType": "BOUNCED",
"subType": "HARD_BOUNCED",
"receiver": "ann@example.com",
"requestId": "833c5151-5a98-4bad-a30f-7cc816352b7a",
"metadata": { "user_id": "u_1042", "flow": "password_reset" },
"bounceCategory": "10",
"bounceDescription": "…the receiving server's own words…",
"bounceStatus": "…DSN status…"
}{
"eventType": "CLICK",
"receiver": "ann@example.com",
"requestId": "833c5151-5a98-4bad-a30f-7cc816352b7a",
"metadata": { "user_id": "u_1042", "flow": "password_reset" },
"url": "https://acme.com/reset?t=8f1c…"
}Payloads trimmed for reading; the docs list every field. Example values are illustrative.
Password reset and account verification email best practice.
The same rules apply to an account verification email: it is a one-time link to prove someone owns an address. Here is what to decide, and which part each side owns.
Sending codes instead of links? See verification codes →- 1Expiring, single-use tokens are your app's jobtinyRelay sends what you give it. Expiry, one-time use and rate limiting reset requests all live in your code, before the send request is made.
- 2Decide whether to keep trackingOpen and click tracking are on by default. With tracking on, the link is rewritten through a click URL, and the CLICK event's url field carries the full original link, query string included. That puts the reset token into your webhook data. If you do not want that, send "disableTrackingLinks": true: no rewritten links, no open pixel, for the whole request.
- 3If you track, consider link wrappingBy default a tracked link exposes the destination as a readable redirectUrl parameter. Sending the X-Link-Wrapping: true header hides it in an opaque, tamper-proof token, which keeps the reset URL out of the message body where blocklist scanners can read it. It is off unless you send the header; there is no account default.
- 4Keep each reset's metadata separateMetadata is scoped to the campaignId, and a later request with the same campaignId replaces it. Omit campaignId on resets so each one uses its own requestId.
- 5Verify, then deduplicateDelivery is at least once. Check the signature, then deduplicate on svix-id; repeats are suppressed on our side for 24 hours, and missed events can be replayed for 5 days.
No. Your app creates the token, stores it, expires it and checks it. tinyRelay sends the email you give it and reports back what happened to it.
It is your call. Tracking gives you OPEN and CLICK events, and the CLICK event carries the full URL including its query string. If you would rather keep reset links out of your webhook data, send the request with disableTrackingLinks set to true.
A BOUNCED event arrives at your webhook endpoint with a subType of HARD_BOUNCED or SOFT_BOUNCED and one of 19 documented bounce categories. Your metadata, such as the user ID, is on the event, so your app can tell the right user.
Yes. tinyRelay offers SMTP relay and a REST API on the same infrastructure. If your framework already sends reset emails over SMTP, point it at tinyRelay with the credentials issued at onboarding.
Usually not. Metadata is scoped to the campaignId, and a later request with the same campaignId replaces the earlier metadata. Leave campaignId out and the generated requestId is used, which keeps each reset separate.
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.