# TRUE Sign API - LLM / agent guide > TRUE Sign is a document e-signing service. Upload a PDF, define signers and fields, send signing links, collect signatures, and receive a Vault-stamped sealed artifact. All /v1/* routes require Bearer authentication (create a key via POST /v1/api-keys). Public signing routes (/sign/:token/*) are browser-facing and use one-time link tokens. Public, no authentication required to read this file (same as /openapi.json). It is generated from docs/api.md and the OpenAPI spec, so it stays in sync with the running API. - Interactive API docs: http://www.trueoriginal.com/docs - OpenAPI spec (JSON): http://www.trueoriginal.com/openapi.json --- # TRUE Sign API - Integrator Reference Base URL (API): `https://sign-api.trueoriginal.com` Dev API URL: `https://sign-api.true.gate1.dev` / Dev Dash URL: `https://sign.true.gate1.dev` Interactive docs (Swagger UI): `GET /docs` OpenAPI spec: `GET /openapi.json` AI-agent quickstart (llms.txt): `GET /llms.txt` - a plain-text map of every endpoint, generated from the OpenAPI spec. From the dashboard, the `Docs` link (`sign.trueoriginal.com/docs`) opens this interactive documentation. --- ## Authentication All `/v1/*` routes require a Bearer API key: ``` Authorization: Bearer tsign_ ``` Obtain a key via `POST /v1/api-keys` (requires the admin bootstrap token set in `ADMIN_BOOTSTRAP_TOKEN`). --- ## Error Envelope All error responses use the same envelope: ```json { "error": { "code": "string", "message": "string (optional)", "details": {} } } ``` Error codes: | Code | Meaning | |------|---------| | `unauthorized` | Missing or invalid API key | | `not_found` | Resource not found or belongs to another org | | `already_signed` | Signer has already submitted a signature | | `wrong_signer` | This link belongs to a different signer (sequential order) | | `expired` | Signing link has expired | | `invalid_pdf` | File is not a valid PDF, too large (>25 MB), or missing | | `invalid_fields` | Field coordinates out of bounds, or validation failed | | `provider_unavailable` | Vault Stamp API or SMS provider unavailable | | `internal` | Unexpected server error | | `conflict` | Idempotency-Key reused with a different request body | | `otp_required` | Signer's security level requires a live SMS OTP verification before `/sign` | | `otp_expired` | No active OTP code (never sent, or its 10-minute TTL passed) | | `otp_invalid` | Submitted code did not match (`attemptsRemaining` in the response) | | `otp_too_many_attempts` | 5 wrong attempts on the current code - send a new one | | `otp_cooldown` | Resend requested inside the 60s cooldown (`retryAfterSeconds` in the response) | | `rate_limited` | Too many OTP sends for this signer+IP in the current window | --- ## Idempotency `POST /v1/requests` and `POST /sign/:token/sign` accept an `Idempotency-Key` header. The same key within 24 hours returns the cached response. A reused key with a different body returns `409 conflict`. --- ## Full Lifecycle - Curl Examples ### (a) Bootstrap: create an org ```bash curl -X POST https://sign-api.trueoriginal.com/v1/admin/orgs \ -H "X-Admin-Token: $ADMIN_BOOTSTRAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{"true_org_id": "acme-corp", "name": "Acme AB"}' ``` Response: ```json { "id": "11111111-1111-1111-1111-111111111111", "trueOrgId": "acme-corp", "name": "Acme AB", "createdAt": "2026-07-07T10:00:00.000Z" } ``` ### (b) Create an API key ```bash curl -X POST https://sign-api.trueoriginal.com/v1/api-keys \ -H "X-Admin-Token: $ADMIN_BOOTSTRAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{"org_id": "11111111-1111-1111-1111-111111111111", "label": "production"}' ``` Response (raw key shown once): ```json { "id": "22222222-2222-2222-2222-222222222222", "orgId": "11111111-1111-1111-1111-111111111111", "label": "production", "key": "tsign_aGVsbG8...base64url", "createdAt": "2026-07-07T10:00:01.000Z" } ``` Set `API_KEY=tsign_aGVsbG8...` for subsequent calls. ### (c) Upload a PDF and create a draft request ```bash curl -X POST https://sign-api.trueoriginal.com/v1/requests \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: my-contract-2026-001" \ -F "pdf=@./service-agreement.pdf" \ -F "title=Service Agreement - Acme AB 2026" \ -F "signing_order=sequential" ``` Response: ```json { "id": "33333333-3333-3333-3333-333333333333", "status": "draft", "title": "Service Agreement - Acme AB 2026", "signingOrder": "sequential", "pdfPages": 4, "pdfSha256": "e3b0c44298fc1c149...", "createdAt": "2026-07-07T10:00:02.000Z" } ``` ### (d) Add signers ```bash curl -X POST https://sign-api.trueoriginal.com/v1/requests/33333333-3333-3333-3333-333333333333/signers \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "signers": [ {"name": "Alice Anderson", "email": "alice@example.com", "role": "signer", "authMethod": "manual", "requiredAuthLevel": "email"}, {"name": "Bob Berg", "email": "bob@example.com", "role": "signer", "authMethod": "manual", "requiredAuthLevel": "email_sms", "mobile": "+46701234567"}, {"name": "Carol Clerk", "email": "carol@example.com", "role": "cc", "authMethod": "manual"} ] }' ``` Response: array of inserted signer rows with generated UUIDs. `requiredAuthLevel` (issue #31) is the per-signer security level, set at signer-add time: | Level | Meaning | Requirement | |-------|---------|-------------| | `email` (default, "Bas") | Mejllänk + full evidence package | none - today's flow | | `email_sms` ("Standard") | + SMS one-time code | `mobile` required, E.164 format (e.g. `+46701234567`) | `bankid` and `freja` ("Hög") are reserved enum values for Fas 2 (AdES via eID) and are rejected at signer-add time today - never silently downgraded to a lower level. `achievedAuthLevel` is set on the signer row once they actually sign, reflecting what was verified at that moment (not just what was required). ### (e) Place signature fields ```bash curl -X POST https://sign-api.trueoriginal.com/v1/requests/33333333-3333-3333-3333-333333333333/fields \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fields": [ {"signerId": "", "type": "signature", "page": 3, "x": 0.1, "y": 0.8, "w": 0.35, "h": 0.08}, {"signerId": "", "type": "signature", "page": 3, "x": 0.55, "y": 0.8, "w": 0.35, "h": 0.08} ] }' ``` Coordinates are fractions of page dimensions in [0, 1]. Constraint: `x+w <= 1`, `y+h <= 1`, `page < pdf_pages`. ### (f) Preview with field overlays (optional) ```bash curl -X POST "https://sign-api.trueoriginal.com/v1/requests/33333333-3333-3333-3333-333333333333/preview?page=3" \ -H "Authorization: Bearer $API_KEY" \ --output preview.pdf ``` Returns an annotated PDF with TRUE brand (#d82a80) field-box overlays. Field metadata available in `X-Sign-Fields` response header. ### (g) Send - transitions to sent, generates signing links ```bash curl -X POST https://sign-api.trueoriginal.com/v1/requests/33333333-3333-3333-3333-333333333333/send \ -H "Authorization: Bearer $API_KEY" ``` Response (raw tokens returned once - store them securely or let signers receive by email): ```json { "requestId": "33333333-3333-3333-3333-333333333333", "signingLinks": [ { "signerId": "", "name": "Alice Anderson", "email": "alice@example.com", "role": "signer", "token": "Gk3mN...43-char-base64url", "link": "https://sign.trueoriginal.com/sign/Gk3mN...43-char-base64url" }, { "signerId": "", "name": "Bob Berg", "email": "bob@example.com", "role": "signer", "token": "Xp9qR...43-char-base64url", "link": "https://sign.trueoriginal.com/sign/Xp9qR...43-char-base64url" } ] } ``` For `signing_order=sequential`: only Alice is emailed at send time. After Alice signs, a fresh link is generated for Bob and he is emailed automatically. For `signing_order=parallel`: all signers are emailed simultaneously. CC recipients (role=cc) are always emailed at send time regardless of order. ### (h) Signer opens the signing page Signer clicks their link: `https://sign.trueoriginal.com/sign/` The signing page is a React app (served at `sign.trueoriginal.com/sign/:token`). The API backend redirects any non-JSON `GET /sign/` requests to this React frontend, so legacy email links continue to work automatically. The React page fetches signing data from the API (`GET /sign/` with `Accept: application/json`). The signer can (no auth required): - View the document (`GET /sign//pdf` - streams from API) - Mark as viewed (`POST /sign//mark-viewed`) - Draw or type their signature and submit (`POST /sign//sign`) - Decline (`POST /sign//decline`) ### (h.1) SMS OTP second factor (issue #32, when `requiredAuthLevel=email_sms`) `GET /sign/` includes `signer.requiredAuthLevel`, `signer.otpVerified` and `signer.mobileLast4` (never the full number) so the signing page knows whether to show the OTP step. ```bash # Send a 6-digit code to the signer's mobile (10-minute expiry, 60s resend cooldown) curl -X POST https://sign.trueoriginal.com/sign/Gk3mN...43-char-base64url/otp/send # Verify the code (max 5 attempts per code) curl -X POST https://sign.trueoriginal.com/sign/Gk3mN...43-char-base64url/otp/verify \ -H "Content-Type: application/json" \ -d '{"code": "123456"}' ``` A successful verify opens a 15-minute window during which `POST /sign//sign` is accepted. Outside that window (or if OTP was never verified), `/sign` rejects with `403 otp_required` - enforced server-side regardless of what the UI shows. `otp_sent`, `otp_verified` and `otp_failed` events are recorded in the audit trail and surface in the signature log evidence. ### (i) Signer submits signature ```bash curl -X POST https://sign.trueoriginal.com/sign/Gk3mN...43-char-base64url/sign \ -H "Content-Type: application/json" \ -d '{ "fields": [ {"field_id": "", "signature_data": "data:image/png;base64,..."} ] }' ``` Response (signer confirmed): ```json { "ok": true } ``` When the last signer signs, the request transitions to `awaiting_seal` and sealing begins automatically (fire-and-forget). ### (j) Sealing and Vault Stamp After the last signature: 1. A signature log PDF is rendered with the full event audit trail. 2. The original PDF + log are submitted to the Vault Stamp API (POST `vault.trueoriginal.com/v1/stamps`). 3. A forsattsblad (cover page) is rendered with a QR code pointing to the vault verify URL. 4. Final artifact: cover + original + log - merged and stored in S3. 5. `request.status` transitions to `completed`. 6. All signers receive an email with the vault verify URL. ### (k) Poll for completion ```bash curl https://sign-api.trueoriginal.com/v1/requests/33333333-3333-3333-3333-333333333333 \ -H "Authorization: Bearer $API_KEY" ``` When `status=completed`, the response includes: ```json { "status": "completed", "vaultAnchorId": "test-anchor", "vaultProofUrl": "https://vault.trueoriginal.com/v/test-anchor", "completedAt": "2026-07-07T10:05:00.000Z" } ``` --- ## Request Statuses | Status | Meaning | |--------|---------| | `draft` | Created, not yet sent | | `sent` | Sent to signers | | `in_progress` | At least one signer has signed | | `awaiting_seal` | All signers done, sealing in progress | | `completed` | Sealed artifact ready, vault anchor issued | | `sealing_failed` | Vault Stamp API failed (support contact required) | | `declined` | A signer declined | | `cancelled` | Cancelled by the sending org | | `expired` | Signing deadline passed | --- ## Signing Link Security - Tokens are 32 bytes of CSPRNG output, base64url-encoded (43 chars, no padding). - Only the HMAC-SHA256 hash of the token is stored in the database. - Tokens are single-use per signing event; sequential order generates a fresh token for each subsequent signer. - Links expire when the request expires or is cancelled. --- ## All endpoints (generated from the OpenAPI spec) ### Auth - `POST /v1/auth/accept-terms` - Record that the current user accepted the TRUE Sign terms - `POST /v1/auth/logout` - End the current session - `POST /v1/auth/magic` - Request a magic-link login email Sends a one-time login link to the provided email. Always returns 200 - even if the email is unknown - to prevent enumeration. - `GET /v1/auth/me` - Get the current session user - `PATCH /v1/auth/me` - Update the current user's display name - `POST /v1/auth/verify` - Verify a magic-link token and start a session Consumes the token, creates a session cookie, returns user info. ### Capabilities - `GET /v1/capabilities` - Server capability flags Runtime flags the frontend needs before offering a feature - currently just whether SMS OTP sending is available. No auth required; exposes no sensitive data, only whether the Standard (email + SMS) security level can be offered right now (issue #42). ### admin - `POST /v1/admin/orgs` - Create an org (bootstrap) Bootstrap endpoint secured by ADMIN_BOOTSTRAP_TOKEN (X-Admin-Token header). Creates an org linked to a TRUE org ID. Use this before creating API keys. - `POST /v1/api-keys` - Create an API key for an org Bootstrap endpoint secured by ADMIN_BOOTSTRAP_TOKEN (X-Admin-Token header). Returns the raw key once - it is not stored in plain text and cannot be recovered. ### billing - `POST /billing/checkout` - Create a Stripe Checkout session for the Team plan Creates a hosted Checkout session (card + Klarna) for the per-user Team price at the requested interval and returns its URL. Returns 503 when billing is not configured on this deploy, or when the price id for the requested interval is unset. - `GET /billing/plan` - Current plan, quota and billing capability for the authenticated org Returns the org plan, its Stripe subscription status, how many signing requests it has sent against the free allowance, and whether this deploy can offer checkout at all (billingEnabled=false when STRIPE_SECRET_KEY is unset). - `POST /billing/portal` - Create a Stripe customer portal session Returns a URL to the Stripe-hosted customer portal for the org, where the customer manages payment method, invoices and cancellation. Requires the org to have a Stripe customer (i.e. to have been through checkout at least once). - `POST /billing/redeem` - Redeem a campaign code for extra free sends Redeems a campaign code (currently LAUNCH3, which grants 3 extra free sends) against the authenticated org. Codes are recorded on the org, so redeeming the same code twice is rejected. Works regardless of whether Stripe is configured. - `POST /billing/webhook` - Stripe webhook receiver Receives Stripe events (checkout.session.completed, customer.subscription.*). The stripe-signature header is verified against STRIPE_WEBHOOK_SECRET; unsigned or mis-signed payloads are rejected with 400 and never parsed. Handling is idempotent - a repeated event id is acknowledged without being applied twice. ### orgs - `GET /v1/orgs/me` - Get the authenticated org Returns the org associated with the Bearer API key. - `PATCH /v1/orgs/me` - Update the authenticated org - `GET /v1/orgs/me/members` - List the authenticated org's members ### requests - `GET /v1/requests` - List signing requests Returns all signing requests for the authenticated org, newest first. - `POST /v1/requests` - Create a signing request (draft) Upload a PDF and create a new signing request in draft state. Use Idempotency-Key header to deduplicate retries (24h TTL). - `DELETE /v1/requests/{id}` - Delete a draft request Permanently deletes a draft request along with its signers, fields, and events. Best-effort deletes the uploaded PDF object in storage. Only allowed while status=draft - sent requests must be cancelled instead, and completed requests are audit records that cannot be deleted. - `GET /v1/requests/{id}` - Get a signing request Returns the full request including signers and fields. - `PATCH /v1/requests/{id}` - Update a draft request Updates the title of a request. Only allowed while status=draft. - `GET /v1/requests/{id}/artifact` - Download sealed artifact PDF Streams the sealed artifact PDF for a completed request. - `POST /v1/requests/{id}/cancel` - Cancel a signing request Cancels a draft, sent, or in_progress request. Sets status to cancelled and records a cancelled event. - `POST /v1/requests/{id}/fields` - Set signature fields on a draft request Replace-all upsert: deletes existing fields and inserts the provided list. Coordinates are fractions of page dimensions in [0,1]. Only allowed while status=draft. - `GET /v1/requests/{id}/pdf` - Stream the original uploaded PDF Streams the original PDF bytes as uploaded (clean, no field overlays, not sealed). Session or API-key auth. Returns 404 if the request is missing, belongs to another org, or its stored bytes cannot be found. - `POST /v1/requests/{id}/pdf` - Replace the document on a draft request Replaces the uploaded PDF on a draft request, recomputing its storage key, sha256 fingerprint, and page count. Same validation as upload at create time (PDF magic bytes, 25 MB limit). Placed fields whose page still exists in the new document are kept; fields on pages beyond the new page count are dropped. Only allowed while status=draft - after send, signatures bind to the exact bytes and the document must be immutable. - `POST /v1/requests/{id}/preview` - Preview a request page with field overlays Returns an annotated copy of the PDF with TRUE brand (#d82a80) field-box overlays for the given page (default: 0). Field metadata also returned in X-Sign-Fields response header as JSON. - `GET /v1/requests/{id}/preview-pdf` - Stream the document with saved signatures overlaid Streams the original PDF with all currently-saved signature/field values rendered onto the pages (same overlay as sealing). Owner-scoped. Returns the plain original when nothing has been signed yet. Not cached - values change as signers sign. - `POST /v1/requests/{id}/reseal` - Retry sealing a request stuck in sealing_failed Re-runs the seal step for a request whose previous sealing attempt failed (e.g. a Vault Stamp outage or misconfiguration). Only valid when status is sealing_failed. Sealing runs asynchronously - poll GET /v1/requests/:id for the resulting status. - `POST /v1/requests/{id}/send` - Send a signing request to signers Transitions status from draft to sent, generates one-time signing link tokens per signer, dispatches emails. Raw tokens are returned once in this response - they are not stored in plain text and cannot be retrieved again. For sequential signing_order, only the first signer is emailed; subsequent signers receive their link after the previous one signs. - `POST /v1/requests/{id}/sign-now` - Get a signing link for the signed-in sender When the dashboard user is also a signer on this request, rotates that signer's link token and returns a fresh signing URL for immediate redirect. Session auth only. - `POST /v1/requests/{id}/signers` - Set signers on a draft request Replace-all upsert: deletes existing signers and inserts the provided list. Only allowed while status=draft. - `POST /v1/requests/{id}/signers/{signerId}/remind` - Send a reminder to a pending signer Generates a fresh signing token (invalidates the old one), sends a reminder email, and records a reminder_sent event. Only valid when the request is sent or in_progress and the signer is pending or viewed.