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.
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:
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-PathMIME-Version,DKIM-Signature,ReceivedList-Unsubscribe,List-Unsubscribe-Post,In-Reply-To,References- Any name that starts with
Content-orARC-
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.
textandhtmlboth 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.