Email

Send a one-time code in a custom header

Put a one-time code in a custom header for your own tooling and tests, and keep the code readable in the email body.

A sign-in or address-confirmation email can carry its code in two places: the body a person reads, and a header your own system reads. email.headers on a send copies your header onto the outgoing message, so an inbox test, a support tool, or an automation you run can pick the code up from the message it received.

The header is metadata for your tooling. The code a person types is the one in the body, and the sections below keep both in the same email.

What the header is for

Use the header when something on your side reads the delivered message: a test that asserts the code the API sent, a script that matches a delivery to a challenge row, or an internal mailbox that routes a code to the right team.

Samva copies the header and does not read it. Whether a mail client shows, hides, or rewrites a header is that client's decision, not Samva's, so do not depend on a client surfacing the code from a header, and do not wait for a client to offer a Copy code button. The sections below keep the code visible in the body so the recipient never needs the header.

Send the code

Pass the code as a header value beside the body that shows it. This example uses X-OTP-Token; any name that passes the rules below works.

send-code-header.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const code = "483921";
const lines = [
  code,
  "",
  "Use this code to sign in.",
  "This code stops working 15 minutes after it was sent.",
];

await samva.email.send({
  to: "ada@example.com",
  subject: "Your sign-in code",
  text: lines.join("\n"),
  html: lines
    .map((line, index) =>
      index === 0
        ? `<p style="font-size:28px;letter-spacing:0.08em">${line}</p>`
        : `<p>${line}</p>`,
    )
    .join(""),
  headers: { "X-OTP-Token": code },
});

The same send over REST nests the map under email:

curl -X POST https://api.samva.dev/v1/messages \
  -H "X-API-Key: $SAMVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{ "email": "ada@example.com" }],
    "channel": "email",
    "email": {
      "subject": "Your sign-in code",
      "text": "483921\n\nUse this code to sign in. It stops working 15 minutes after it was sent.",
      "headers": { "X-OTP-Token": "483921" }
    }
  }'

From the CLI, repeat --header 'Name: Value' for each entry:

samva email send --to ada@example.com \
  --subject "Your sign-in code" \
  --text "Your sign-in code is 483921." \
  --header 'X-OTP-Token: 483921'

What the value may contain

Samva hands the value to the outgoing message exactly as you wrote it. It is not re-quoted, folded, or encoded on the way, so a value written with quotation marks keeps them:

send-quoted-code.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

await samva.email.send({
  to: "ada@example.com",
  subject: "Your sign-in code",
  text: "Your code is 123 456.",
  headers: { "X-OTP-Token": '"123 456"' },
});

That send carries X-OTP-Token: "123 456", quotation marks included. Spaces and commas need no escaping and change nothing about how the value travels.

A value must be one line of printable ASCII, and an empty value is rejected rather than sent as an empty header. A tab, a carriage return, a line feed, or any character outside ASCII is rejected with a ValidationError (422) and is never encoded or repaired on the way. That rule is what keeps a value from starting another header.

The CLI trims the name and value it receives before sending, so --header 'X-OTP-Token: 483921 ' sends 483921, and a trailing tab or newline is trimmed instead of reaching that rejection. The CLI also treats two entries with the same name as one header, keeping the last value; the API rejects a repeated name. Write the value without surrounding whitespace and name each header once when the exact bytes and the duplicate check matter.

A value sent straight to the API, through the SDK or a curl call, keeps whitespace inside the line and is judged by the rule above as written.

Names keep the casing you send, so X-OTP-Token stays as written on the message. A second name that matches an earlier one while ignoring case is rejected, and the fields entry names the duplicate.

Names Samva or the transport own are reserved, and matching ignores case. A send carrying one of these names is rejected:

  • From, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, Sender, Return-Path
  • MIME-Version, DKIM-Signature, Received
  • List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To, References
  • Any name that starts with Content- or ARC-

A name and value also have length caps, and one email carries a bounded number of custom headers. Set custom headers states those limits and the shape of the 422 that a rejected header returns.

Keep the code in the body

The header travels with the message, and the body is what a person reads. Write one code, put it in the first lines of the plain-text and HTML parts, name the action, and state the expiry your server enforces. A reader who never sees a header, or whose client drops it, still has everything needed to finish.

  • text and html both contain the code, so the inbox preview and the opened message agree.
  • The subject names the action without repeating the code, which keeps the code off lock screens.
  • Anything else in the message, such as an order id or a support number, sits below the code.

The one-time passcode template starts a code-only sign-in email with the code above the fold and an expiry you supply. To send a published template instead of inline content, pass templateId or templateSlug with templateData and omit subject, html, and text; headers stays available on the same send.

Next steps

Related documentation

On this page