---
title: "Send OTP to signer email"
method: POST
path: "/api/envelopes/{envelope_id}/otp"
---

# Send OTP to signer email

`POST /api/envelopes/{envelope_id}/otp`

## Request

- Base URL: `https://free-sign.com`
- URL: `https://free-sign.com/api/envelopes/{envelope_id}/otp`
- Auth: none declared. Required headers: `x-fsig-session-signature`, `x-fsig-session-nonce`, `x-fsig-session-timestamp`, `x-fsig-session-action`

Requires the four session-binding headers (see x-fsig-session-* component parameters). 401 session_signature_required when absent. When the deployment has Cloudflare Turnstile configured (TURNSTILE_SECRET_KEY set), the request must also carry a valid `turnstile_token`; the browser solves the challenge in the OTP modal before this call. A short per-email cooldown (default 30s) plus 15-minute sliding-window rate limits apply.

## Path parameters

- `envelope_id` string, required

## Headers

- `x-fsig-session-signature` string, required
- `x-fsig-session-nonce` string, required
- `x-fsig-session-timestamp` string, date-time, required
- `x-fsig-session-action` 'otp.request' | 'otp.verify' | 'sign' | 'seal' | 'platform-seal' | 'finalize' | 'webauthn.register' | 'webauthn.authenticate', required

## Request body

- object
  - `email` string, email, required
  - `turnstile_token` string: Cloudflare Turnstile token from the browser challenge. Required when the deployment has TURNSTILE_SECRET_KEY set; omitted otherwise. The @smoke.free-sign.com test channel is exempt.

## Response `200`

OTP challenge created.

## Other responses

- `403`: turnstile_required (token missing) / turnstile_failed (token rejected by siteverify).
- `409`: envelope_not_otp_eligible — the envelope is finalized or expired and can no longer accept an OTP. No code was sent either way. The body carries a `phase`: "precheck" (the up-front gate) refused before this route spent anything of its own — no Turnstile token, no cooldown or rate-limit tick, no challenge row — but on THIS route the session nonce in the x-fsig-session-* headers was already consumed by the session check that runs before the gate, so a retry needs a freshly signed proof (reusing the same headers answers 401 session_nonce_replayed). Only /api/batch/otp, which gates before consuming nonces, is free to retry unchanged. "race" (the envelope turned terminal mid-flight) additionally charged the rate limit and, where the deployment enforces it, the per-email cooldown, so an immediate retry can answer 429 otp_cooldown.
- `429`: otp_cooldown (another OTP request for this email was accepted less than OTP_COOLDOWN_SECONDS ago) or otp_rate_limited (15-minute bucket exceeded). otp_cooldown is NOT proof that a code reached the inbox: the cooldown is checked and ticked up front, so a round that then failed — a `race` 409, a mail-provider failure — leaves it charged with nothing delivered. Only a 200 proves a code was sent.
- `503`: turnstile_unavailable — Cloudflare siteverify unreachable; the gate fails closed.

---

[API](https://skmtc.dev/free-sign/apis/freesign-free-e-signature-api.md) · [All operations](https://skmtc.dev/free-sign/apis/freesign-free-e-signature-api/llms.txt) · [OpenAPI document](https://skmtc.dev/free-sign/apis/freesign-free-e-signature-api/revisions/5d8e9854ab85?raw)
