Use case · Password resets

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.

Your app
POST /email/campaign
tinyRelay
DKIM-signed
Inbox
ann@example.com
Reset your Acme password
metadata · user_id: u_1042

    Reset email simulation · illustrative

    The problem

    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.

    The flow

    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
    your-app/reset.js · illustrativeCopy
    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
    The same request in Node.js →Send Email reference in the docs →
    cURL · send a reset emailCopy
    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
    How to verify webhook signatures →
    BOUNCED · hard bounceCopy
    {
      "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…"
    }
    CLICK · the link was clickedCopy
    {
      "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.

    Best practice

    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 →
    1. 1
      Expiring, single-use tokens are your app's job
      tinyRelay 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.
    2. 2
      Decide whether to keep tracking
      Open 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.
    3. 3
      If you track, consider link wrapping
      By 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.
    4. 4
      Keep each reset's metadata separate
      Metadata 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.
    5. 5
      Verify, then deduplicate
      Delivery 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.
    FAQ

    Password reset email questions.

    The full field list is in the webhook reference.

    Does tinyRelay create or expire reset tokens?

    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.

    Get started

    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.