---
title: "Submit browser-generated signature receipt"
method: POST
path: "/api/envelopes/{envelope_id}/sign"
---

# Submit browser-generated signature receipt

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

## Request

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

The request must contain a browser-generated public key and signature over the canonical payload. It must not contain PDF bytes. Requires the four session-binding headers (see x-fsig-session-* component parameters).

## 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: The browser-signed receipt. canonical_payload carries EXACTLY ONE identity proof: an OTP signer includes otp_challenge_id + otp_verified_at inside canonical_payload; a passkey (WebAuthn) signer includes a `webauthn` object inside canonical_payload AND a top-level webauthn_assertion. The two are mutually exclusive — sending both is rejected 400 identity_proof_ambiguous, sending neither 400 identity_proof_required. No PDF bytes are ever accepted here.
  - `signer_id` string, required
  - `signer_name` string, required: Typed legal name; becomes the leaf-certificate Subject CN.
  - `signer_email` string: Required for the WebAuthn path — binds the passkey credential to this signer.
  - `consent_accepted` boolean, required
  - `canonical_payload` object, required: The object the browser ECDSA-signed. Full shape: https://free-sign.com/evidence/v2/schema.json ($defs/canonicalPayload).
  - `public_key_jwk` object, required: Browser-generated ECDSA P-256 public key (EC / P-256 JWK).
  - `signature_base64url` string, required: base64url ECDSA P-256 / SHA-256 signature over canonicalJson(canonical_payload).
  - `webauthn_assertion` object: WebAuthn (passkey) path only — the navigator.credentials.get() result. Its challenge equals SHA-256(canonicalJson(canonical_payload)); the server verifies it against the registered credential's stored COSE key.
    - `authenticator_data` string: base64url raw authenticatorData.
    - `client_data_json` string: base64url raw clientDataJSON.
    - `signature` string: base64url DER ECDSA-Sig-Value.

## Response `200`

Signature receipt stored.

## Other responses

- `400`: identity_proof_required / identity_proof_ambiguous / signature_mismatch / payload_* validation error.
- `401`: webauthn_assertion_failed / webauthn_credential_unknown — passkey assertion rejected.
- `409`: signer_already_signed (one receipt per signer) or envelope_not_signable (the envelope is finalized or expired and can no longer record a signature).

---

[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)
