openapi: 3.1.0
info:
  title: Khatm Developer Sandbox API
  version: 1.11.0
  summary: Test the Khatm signing workflow before integrating it into your product.
  description: |-
    The Khatm Sandbox API prepares PDF signature requests, issues signer links,
    reports workflow status and exposes completed artifacts.

    Retry `429` and `503 temporarily_unavailable` responses with backoff. Replaying
    a create call with the same `client_reference` never creates a duplicate, and any
    mutating call sent with an `Idempotency-Key` header can be retried safely.

    This environment uses a test/self-signed certificate. It is intended for
    integration testing and does not provide an advanced or qualified signature
    or a production trust service.
servers:
  - url: /v1
    description: Sandbox on the current Khatm origin
tags:
  - name: Templates
    description: Reuse field geometry prepared in the authenticated Khatm workspace.
  - name: Signature requests
    description: Prepare, activate and track a test signing workflow.
  - name: Artifacts
    description: Retrieve completed test documents and evidence.
  - name: Webhooks
    description: Inspect and replay the events sent to your callback URL.
security:
  - sandboxKey: []
components:
  securitySchemes:
    sandboxKey:
      type: http
      scheme: bearer
      bearerFormat: khatm_test_xxx
      description: Use an active Sandbox API key generated in the Khatm workspace.
  headers:
    RateLimitLimit:
      description: Maximum requests allowed in the current ten-minute window.
      schema:
        type: integer
        example: 120
    RateLimitRemaining:
      description: Requests remaining in the current ten-minute window.
      schema:
        type: integer
        example: 119
  schemas:
    ErrorDetail:
      type: object
      additionalProperties: false
      required: [code, message, request_id]
      properties:
        code:
          type: string
          enum:
            - invalid_request
            - invalid_document
            - unauthorized
            - not_found
            - conflict
            - request_not_ready
            - request_expired
            - artifact_not_available
            - rate_limit_exceeded
            - internal_error
            - template_not_found
            - template_not_ready
        message:
          type: string
          example: A valid sandbox API key is required.
        request_id:
          type: string
          description: Identifier to include when contacting Khatm support.
          example: 7f331696-d933-46cd-aeff-1c08151c85c1
    Error:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    SignerInput:
      type: object
      additionalProperties: false
      required: [reference, email, order]
      properties:
        reference:
          type: string
          minLength: 1
          maxLength: 100
          example: customer
        name:
          type: string
          maxLength: 200
          example: Alice Martin
        email:
          type: string
          format: email
          example: alice@example.com
        order:
          type: integer
          minimum: 1
          maximum: 10
          example: 1
          description: Unique position of the signer (1, 2, …). It is enforced only when the request sets `sequential_signing`; otherwise every signer can sign as soon as the request is active.
        locale:
          type: string
          enum: [fr, en, ar]
          description: Language of the signing page for this signer. When omitted, the signer's browser language is used. The signing link carries it as `lang`.
        verification:
          type: string
          enum: [email_otp]
          description: |-
            Identity check before the signer can see the document. With `email_otp`, the signing page
            emails a 6-digit code (valid 10 minutes, 5 attempts, at most 15 wrong codes per 24 hours) to the signer's address; the document,
            signing and declining stay locked until it is entered. The check is recorded in the evidence.
    FieldInput:
      type: object
      additionalProperties: false
      required: [signer_reference, type, page, x, y, width, height]
      properties:
        signer_reference:
          type: string
          description: Must match a signer reference in the same request.
          example: customer
        type:
          type: string
          enum: [signature, date, text, initials]
          example: signature
        field_key:
          type: string
          description: Optional semantic identity for text/date fields (signer_name, signer_email, company, job_title, sign_date, initials).
          enum: [signer_name, signer_email, company, job_title, sign_date, initials]
          example: signer_name
        page:
          type: integer
          minimum: 1
          maximum: 10000
          example: 1
        x:
          type: number
          minimum: 0
          example: 90
        y:
          type: number
          minimum: 0
          example: 610
        width:
          type: number
          exclusiveMinimum: 0
          maximum: 5000
          example: 170
        height:
          type: number
          exclusiveMinimum: 0
          maximum: 5000
          example: 55
        name:
          type: string
          pattern: '^[a-z][a-z0-9_]{0,63}$'
          description: Your name for the field, unique in the request. Use it with `field_values` (for example to fill a template).
          example: contract_amount
        value:
          type: string
          maxLength: 500
          description: Prefilled value for a free text field (no identity `field_key`) or a date field without `field_key`. The signer sees it already filled.
          example: 1 200 EUR
        read_only:
          type: boolean
          default: false
          description: When true (requires `value`), the signer cannot change the value; Khatm keeps the sender value even if a client submits another one.
    CreateRequestPayload:
      type: object
      additionalProperties: false
      required: [client_reference, signers]
      properties:
        client_reference:
          type: string
          minLength: 1
          maxLength: 160
          description: Idempotency key scoped to the authenticated account.
          example: contract-2026-0042
        template_id:
          type: string
          format: uuid
          description: Identifier of a ready template from the Khatm workspace. Cannot be combined with fields.
        signers:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: '#/components/schemas/SignerInput'
        fields:
          type: array
          minItems: 0
          maxItems: 100
          description: May be omitted when fields will be detected and confirmed before activation.
          items:
            $ref: '#/components/schemas/FieldInput'
        callback_url:
          type: string
          format: uri
          description: Optional public HTTPS URL on port 443. Private and loopback destinations are rejected (a local development server can opt in to loopback callbacks with WEBHOOKS_ALLOW_LOOPBACK).
          example: https://example.com/webhooks/khatm
        sequential_signing:
          type: boolean
          default: false
          description: When true, a signer can only sign after every signer with a lower `order` has signed.
        email: { $ref: '#/components/schemas/EmailBranding' }
        embed_origin:
          type: string
          maxLength: 255
          example: https://app.example.com
          description: |-
            Origin of your app (`https://host[:port]`) allowed to show the signing page in an iframe.
            Activation then also returns `embed_url` for each signer: load it in an `<iframe>` on that
            origin. Other sites cannot frame it. The page sends `window.postMessage` events to that origin
            only: `{ source: "khatm", type, signatureRequestId, clientReference, signerReference }` with
            `type` among `signing.loaded`, `signing.verification_required`, `signing.signed`,
            `signing.declined` and `signing.closed` (the link can no longer be used; `status` says why:
            `signed`, `declined`, `cancelled`, `expired`, `revoked`, or `unavailable` for a link already used). In the iframe, `return_url` is not used: react to the events instead,
            and confirm the outcome with the status endpoint or a webhook.
        field_values:
          type: object
          additionalProperties: { type: string, maxLength: 500 }
          description: Values for named fields (`name`), typically those of a template. Unknown names are rejected.
          example: { contract_amount: 1 200 EUR, start_date: 01/11/2026 }
        return_url:
          type: string
          format: uri
          maxLength: 2048
          description: |-
            Where the signer is sent back in your app once they have signed, declined, or reach a closed request.
            HTTPS only (plain HTTP is accepted for loopback hosts during development). Khatm appends
            `khatm_status` (signed, declined, cancelled or expired), `signature_request_id`,
            `client_reference` and `signer_reference`. Treat these parameters as a hint for the UI and
            confirm the outcome with the status endpoint or a webhook.
          example: https://app.example.com/contracts/42
    EmailBranding:
      type: object
      additionalProperties: false
      description: |-
        Brands the invitations and reminders Khatm sends (`send_emails`, reminders, `send_email` on replace).
        Emails are written in each signer's `locale` (English by default) and say they are sent on your behalf.
      properties:
        sender_name: { type: string, maxLength: 120, description: Shown in the header and the text, e.g. your company. No line breaks. }
        subject: { type: string, maxLength: 200, description: Replaces the default subject. No line breaks. }
        message: { type: string, maxLength: 2000, description: Plain text shown above the signing button. Line breaks are kept; HTML is escaped. }
        logo_url: { type: string, format: uri, maxLength: 512, description: HTTPS image shown in the header, about 40 px high. }
      example: { sender_name: Acme HR, message: Please sign before Friday., logo_url: https://example.com/logo.png }
    CreateResponse:
      type: object
      additionalProperties: false
      required: [id, client_reference, status, environment, retention_expires_at, idempotent_replay]
      properties:
        id: { type: string, format: uuid }
        client_reference: { type: string }
        template_id: { type: string, description: Template identifier used for the request, or an empty string. }
        status: { type: string, const: prepared }
        environment: { type: string, const: sandbox }
        retention_expires_at:
          type: string
          format: date-time
          description: PDF content is purged ten days after the original upload.
        sequential_signing:
          type: boolean
          description: Whether signers must sign in their `order`.
        return_url: { type: string, description: The return URL of the request, or an empty string. }
        idempotent_replay:
          type: boolean
          description: True when the same client_reference returned an existing request.
    ActivateInput:
      type: object
      additionalProperties: false
      properties:
        send_emails:
          type: boolean
          default: false
          description: Ask Khatm to send each signer their test signing link.
        link_expiry_days:
          type: integer
          minimum: 1
          maximum: 30
          default: 7
          description: Capped by the document retention deadline.
    FieldSetInput:
      type: object
      additionalProperties: false
      required: [fields]
      properties:
        fields:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: '#/components/schemas/FieldInput' }
    Template:
      type: object
      additionalProperties: false
      required: [id, name, original_filename, fields, field_count, signer_count, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, maxLength: 120, example: Standard customer agreement }
        original_filename: { type: string, example: agreement-reference.pdf }
        fields:
          type: array
          items: { $ref: '#/components/schemas/FieldInput' }
        field_count: { type: integer, minimum: 1 }
        signer_count: { type: integer, minimum: 1, maximum: 10, description: Number of signer references prepared in this template. }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DetectionSuggestion:
      type: object
      additionalProperties: false
      required: [id, type, page, x, y, width, height, confidence, reason]
      properties:
        id: { type: string, example: detected-1 }
        type: { type: string, enum: [signature, date, text, initials] }
        page: { type: integer, minimum: 1 }
        x: { type: number, minimum: 0 }
        y: { type: number, minimum: 0 }
        width: { type: number, exclusiveMinimum: 0 }
        height: { type: number, exclusiveMinimum: 0 }
        confidence: { type: number, minimum: 0.65, maximum: 1 }
        roleHint: { type: string }
        reason: { type: string, enum: [acroform, label_with_geometry, label_with_whitespace] }
        fieldKey: { type: string }
    DetectionPage:
      type: object
      additionalProperties: false
      required: [page, width, height, rotation]
      properties:
        page: { type: integer }
        width: { type: number }
        height: { type: number }
        rotation: { type: integer }
    DetectionResult:
      type: object
      additionalProperties: false
      required: [suggestions, pages, stats, ocrStatus]
      properties:
        suggestions:
          type: array
          items: { $ref: '#/components/schemas/DetectionSuggestion' }
        pages:
          type: array
          items: { $ref: '#/components/schemas/DetectionPage' }
        stats:
          type: object
          additionalProperties: false
          required: [pageCount, totalSuggestions]
          properties:
            pageCount: { type: integer }
            nativeTextPages: { type: integer }
            ocrPages: { type: integer }
            scannedPages: { type: integer }
            totalSuggestions: { type: integer }
            extractionMs: { type: number }
            detectionMs: { type: number }
        ocrStatus: { type: string, enum: [disabled, enabled] }
    ReminderInput:
      type: object
      additionalProperties: false
      properties:
        signer_reference:
          type: string
          description: When omitted, all pending signers are reminded.
    ReminderResponse:
      type: object
      additionalProperties: false
      required: [id, status, reminders]
      properties:
        id: { type: string, format: uuid }
        status: { type: string, const: active }
        reminders:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [reference, email_delivery, signing_url]
            properties:
              reference: { type: string }
              email_delivery: { type: string, description: Delivery outcome of the reminder email (or why none was sent). }
              signing_url: { type: string, format: uri, description: The new signing link; treat it as a bearer credential. }
              embed_url: { type: string, description: The same link for an iframe, when the request has an `embed_origin`. }
    CancellationInput:
      type: object
      additionalProperties: false
      properties:
        reason: { type: string, maxLength: 500 }
    ActivatedSigner:
      type: object
      additionalProperties: false
      required: [id, reference, status, signing_url, email_delivery]
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        status: { type: string, example: pending }
        signing_url:
          type: string
          format: uri
          description: Secret signer-specific URL. Do not expose it in logs.
        email_delivery:
          type: string
          enum: [not_requested, sent, failed]
        embed_url: { type: string, description: Signing page to load in an iframe on `embed_origin`; empty when the request has none. }
    ActivateResponse:
      type: object
      additionalProperties: false
      required: [id, status, environment, signers]
      properties:
        id: { type: string, format: uuid }
        status: { type: string, const: active }
        environment: { type: string, const: sandbox }
        signers:
          type: array
          items: { $ref: '#/components/schemas/ActivatedSigner' }
    SignerStatus:
      type: object
      additionalProperties: false
      required: [id, reference, email, order, status]
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        name: { type: string }
        email: { type: string, format: email }
        order: { type: integer }
        locale: { type: string, description: Signing page language, or an empty string for the browser language. }
        status: { type: string, enum: [pending, viewed, signed, declined, expired], description: '`viewed` once the signer opened the link. A replaced signer no longer appears.' }
        signed_at: { type: [string, 'null'], format: date-time }
        expires_at: { type: [string, 'null'], format: date-time }
        verification: { type: string, description: '`email_otp` when the signer must confirm a code, or an empty string.' }
        verified_at: { type: [string, 'null'], format: date-time }
    ArtifactAvailability:
      type: object
      additionalProperties: false
      required: [signed_pdf, evidence_pdf, evidence_json]
      properties:
        signed_pdf: { type: boolean }
        evidence_pdf: { type: boolean }
        evidence_json: { type: boolean }
    SignatureRequestStatus:
      type: object
      additionalProperties: false
      required: [id, client_reference, environment, status, created_at, updated_at, retention_expires_at, signers, artifacts]
      properties:
        id: { type: string, format: uuid }
        client_reference: { type: string }
        environment: { type: string, const: sandbox }
        status:
          type: string
          enum: [prepared, active, completed, declined, cancelled, expired, purged]
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        retention_expires_at: { type: [string, 'null'], format: date-time }
        sequential_signing: { type: boolean }
        return_url: { type: string }
        signers:
          type: array
          items: { $ref: '#/components/schemas/SignerStatus' }
        fields:
          type: array
          description: Text and date fields with their current value (sender prefill or signer input).
          items:
            type: object
            additionalProperties: false
            required: [id, name, type, field_key, signer_reference, value, read_only, completed]
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              type: { type: string, enum: [text, date] }
              field_key: { type: string }
              signer_reference: { type: string }
              value: { type: string }
              read_only: { type: boolean }
              completed: { type: boolean }
        artifacts: { $ref: '#/components/schemas/ArtifactAvailability' }
    EvidenceTrust:
      type: object
      additionalProperties: false
      required: [certificate, production_trust_service, identity_verified, otp_used, qualified_timestamp, long_term_archive]
      properties:
        certificate: { type: string, const: test/self-signed }
        production_trust_service: { type: boolean, const: false }
        identity_verified: { type: boolean, const: false }
        otp_used: { type: boolean, const: false }
        qualified_timestamp: { type: boolean, const: false }
        long_term_archive: { type: boolean, const: false }
    EvidenceHashes:
      type: object
      additionalProperties: false
      required: [original_pdf_sha256, signed_pdf_sha256, evidence_pdf_sha256]
      properties:
        original_pdf_sha256: { type: string }
        signed_pdf_sha256: { type: string }
        evidence_pdf_sha256: { type: string }
    EvidenceDocument:
      type: object
      additionalProperties: false
      required: [evidence_ref, name, status, sequential_signing, created_at, completed_at]
      properties:
        evidence_ref: { type: string, description: Public reference printed on the evidence report. }
        name: { type: string, description: Original PDF filename. }
        status: { type: string, example: completed }
        sequential_signing: { type: boolean }
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time, description: Time of the last signature once the request is completed. }
    EvidenceSigner:
      type: object
      additionalProperties: false
      required: [id, reference, name, email, signing_order, status, signed_at]
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        name: { type: string }
        email: { type: string, format: email }
        signing_order: { type: [integer, 'null'], description: Position when the request uses a signing order. }
        status: { type: string, example: signed }
        signed_at: { type: [string, 'null'], format: date-time }
        verification: { type: string, description: Identity check passed before signing (`email_otp`), or an empty string. }
        verified_at: { type: [string, 'null'], format: date-time }
    EvidenceEvent:
      type: object
      additionalProperties: false
      required: [id, type, actor_type, actor_email, created_at]
      properties:
        id: { type: integer, description: Sequential audit identifier. }
        type: { type: string, example: request_activated }
        actor_type: { type: string, example: api_client }
        actor_email: { type: string, description: Email of the person behind the event, empty for system events. IP addresses and user agents are never included. }
        signer_id: { type: [string, 'null'], format: uuid }
        created_at: { type: string, format: date-time }
    EvidenceTimestamp:
      type: object
      additionalProperties: false
      required: [provider, hash_algorithm, document_hash, generated_at, verification_status]
      properties:
        provider: { type: string }
        hash_algorithm: { type: string, example: SHA-256 }
        document_hash: { type: string }
        generated_at: { type: [string, 'null'], format: date-time }
        verification_status: { type: string, enum: [verified, unverified, failed], description: '`unverified` means the TSA trust anchors were unavailable, not that the token is invalid.' }
    EvidenceResponse:
      type: object
      additionalProperties: false
      required: [schema_version, generated_at, signature_request_id, client_reference, environment, document, trust, hashes, signers, events, timestamps, retention_expires_at]
      properties:
        schema_version: { type: string, const: khatm.evidence/1.2 }
        generated_at: { type: string, format: date-time }
        signature_request_id: { type: string, format: uuid }
        client_reference: { type: string }
        environment: { type: string, const: sandbox }
        document: { $ref: '#/components/schemas/EvidenceDocument' }
        trust: { $ref: '#/components/schemas/EvidenceTrust' }
        hashes: { $ref: '#/components/schemas/EvidenceHashes' }
        signers:
          type: array
          items: { $ref: '#/components/schemas/EvidenceSigner' }
        events:
          type: array
          items: { $ref: '#/components/schemas/EvidenceEvent' }
        timestamps:
          type: array
          items: { $ref: '#/components/schemas/EvidenceTimestamp' }
        retention_expires_at: { type: [string, 'null'], format: date-time }
    WebhookSigner:
      type: object
      additionalProperties: false
      required: [id, reference, email, status]
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        email: { type: string, format: email }
        status: { type: string }
    WebhookData:
      type: object
      additionalProperties: false
      required: [signature_request_id, client_reference, status]
      properties:
        signature_request_id: { type: string, format: uuid }
        client_reference: { type: string }
        status: { type: string }
        signer:
          $ref: '#/components/schemas/WebhookSigner'
          description: Present on `signer.*` events.
        replaced_signer:
          $ref: '#/components/schemas/WebhookSigner'
          description: Present on `signer.replaced`; the person who no longer has the role.
        reason: { type: string, description: Present on `signer.declined`. }
    WebhookDelivery:
      type: object
      additionalProperties: false
      required: [id, event_id, event_type, signature_request_id, status, attempt_count, created_at, payload]
      properties:
        id: { type: string, format: uuid }
        event_id: { type: string, description: Same value as the `webhook-id` header. }
        event_type: { type: string }
        signature_request_id: { type: string, format: uuid }
        status: { type: string, enum: [pending, delivered, failed], description: '`failed` once every automatic retry was refused.' }
        attempt_count: { type: integer }
        last_status_code: { type: [integer, 'null'] }
        last_error: { type: [string, 'null'] }
        next_attempt_at: { type: [string, 'null'], format: date-time }
        delivered_at: { type: [string, 'null'], format: date-time }
        created_at: { type: string, format: date-time }
        payload: { $ref: '#/components/schemas/WebhookEvent' }
    WebhookEvent:
      type: object
      additionalProperties: false
      required: [id, type, created_at, data]
      properties:
        id: { type: string, example: evt_535d0c }
        type:
          type: string
          enum: [signature_request.activated, signer.viewed, signer.completed, signer.declined, signer.replaced, signer.expired, signature_request.completed, signature_request.declined, signature_request.cancelled, signature_request.expired]
        created_at: { type: string, format: date-time }
        data: { $ref: '#/components/schemas/WebhookData' }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |-
        Unique key (1 to 255 printable ASCII characters, for example a UUID) that makes a retry safe.
        A repeat of the same call with the same key, within 24 hours, returns the stored response with
        the header `Idempotent-Replayed: true` instead of acting twice. Server errors and `429` are not
        stored, so they can be retried with the same key. While the first call is still running, a
        retry gets `409 idempotency_request_in_progress`.
      schema: { type: string, minLength: 1, maxLength: 255 }
  responses:
    IdempotencyKeyReused:
      description: The Idempotency-Key was already used with a different method, path or body.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: The Sandbox API key is missing, invalid or revoked.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: unauthorized
              message: A valid sandbox API key is required.
              request_id: 7f331696-d933-46cd-aeff-1c08151c85c1
    NotFound:
      description: The request does not exist or belongs to another account.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: The Sandbox rate limit was exceeded.
      headers:
        X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InternalError:
      description: The request could not be completed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
paths:
  /templates:
    get:
      tags: [Templates]
      operationId: listTemplates
      summary: List reusable signature templates
      description: Returns templates created and prepared in the authenticated Khatm workspace. Use an ID as template_id when creating a request whose PDF follows the same layout.
      responses:
        '200':
          description: Templates available to the API key owner.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Template' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
    post:
      tags: [Templates]
      operationId: createTemplate
      summary: Create a reusable template
      description: |-
        Upload a reference PDF and its complete field set. Each distinct `signer_reference` becomes a
        template role: requests created with this `template_id` must use the same signer references.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [document, name, fields]
              properties:
                document: { type: string, format: binary, description: Reference PDF (8 MiB max). }
                name: { type: string, maxLength: 120 }
                fields: { type: string, description: JSON-encoded array of FieldInput. }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '201':
          description: The template was created and is ready to use.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Template' }
        '400':
          description: The PDF, name or fields are invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /templates/{id}:
    delete:
      tags: [Templates]
      operationId: archiveTemplate
      summary: Archive a template
      description: Archived templates can no longer be used; requests already created from them are unaffected.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '204': { description: The template was archived. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: The template does not exist for this account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /signature-requests:
    get:
      tags: [Signature requests]
      operationId: listSignatureRequests
      summary: List signature requests
      description: Newest first. Pass `next_cursor` back as `cursor` to read the next page.
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [prepared, active, completed, declined, cancelled, expired, purged] } }
        - { name: client_reference, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200':
          description: A page of requests.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required: [id, client_reference, status, created_at, updated_at, retention_expires_at]
                      properties:
                        id: { type: string, format: uuid }
                        client_reference: { type: string }
                        status: { type: string }
                        created_at: { type: string, format: date-time }
                        updated_at: { type: string, format: date-time }
                        retention_expires_at: { type: [string, 'null'], format: date-time }
                  next_cursor: { type: string, description: Empty when there is no further page. }
        '400':
          description: Invalid filter, limit or cursor.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Signature requests]
      operationId: createSignatureRequest
      summary: Create a prepared signature request
      description: |-
        Upload a PDF and its signing instructions. Fields may be provided directly,
        loaded from a reusable `template_id`, or omitted when using the detection
        and field-confirmation endpoints before activation. `client_reference` is
        idempotent for the authenticated account: replaying it returns the
        existing request with HTTP 200 instead of creating another request.

        The PDF must be at most 8 MiB. A request can contain up to 10 signers
        and 100 fields. Every signer needs at least one confirmed field before activation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [document, request]
              properties:
                document:
                  type: string
                  format: binary
                  description: A complete PDF file with a .pdf filename.
                request:
                  type: string
                  description: JSON-encoded CreateRequestPayload.
                  example: '{"client_reference":"contract-2026-0042","template_id":"7f331696-d933-46cd-aeff-1c08151c85c1","signers":[{"reference":"customer","name":"Alice Martin","email":"alice@example.com","order":1}],"callback_url":"https://example.com/webhooks/khatm"}'
            encoding:
              document: { contentType: application/pdf }
              request: { contentType: application/json }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '201':
          description: A new prepared request was created.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateResponse' }
        '200':
          description: An existing request was returned after an idempotent replay.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateResponse' }
        '400':
          description: The multipart request, JSON instructions or PDF is invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: The template does not exist for this account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: The template does not contain a confirmed field set.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/InternalError' }
  /signature-requests/{id}/activate:
    post:
      tags: [Signature requests]
      operationId: activateSignatureRequest
      summary: Activate a prepared request
      description: Issue one secret signing URL per signer and optionally send the test invitations by email.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          description: Signature request identifier returned by the create call.
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ActivateInput' }
            example: { send_emails: false, link_expiry_days: 7 }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200':
          description: The request was activated and signer URLs were issued.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ActivateResponse' }
        '400':
          description: The activation settings are invalid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The request is already active or is not ready.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/InternalError' }
  /signature-requests/{id}/detect-fields:
    post:
      tags: [Signature requests]
      operationId: detectSignatureFields
      summary: Detect candidate fields in a prepared PDF
      description: Returns suggestions only. Review them, assign each one to a signer and confirm the final set with the fields endpoint. Detection never activates the request.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200':
          description: Candidate fields and PDF page geometry.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DetectionResult' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Detection is only available before activation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/InternalError' }
  /signature-requests/{id}/fields:
    put:
      tags: [Signature requests]
      operationId: replaceSignatureFields
      summary: Confirm the complete field set
      description: Replaces every field of a prepared request. Every signer must receive at least one field.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FieldSetInput' }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200': { description: Fields confirmed. }
        '400':
          description: The field set is invalid or incomplete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The request is no longer prepared.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /signature-requests/{id}/reminders:
    post:
      tags: [Signature requests]
      operationId: remindSignatureRequest
      summary: Send fresh signing links to pending signers
      description: Rotates the selected signer links before sending reminders. Omit signer_reference to remind every pending signer.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReminderInput' }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200':
          description: Reminder delivery results. Each reminded signer gets a new link; their previous link stops working.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReminderResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The request is not active.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-sends:
    post:
      tags: [Signature requests]
      operationId: bulkSend
      summary: Send one template to many recipients
      description: |-
        Creates and activates one signature request per recipient (up to 50), using the template's PDF
        and fields. Each row succeeds or fails on its own; read `results`. Replaying the same batch is
        safe: a `client_reference` that already exists is reported as `already_exists` and nothing is
        created or emailed again. Limited to 10 batches per hour.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [template_id, recipients]
              properties:
                template_id: { type: string, format: uuid }
                recipients:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    additionalProperties: false
                    required: [client_reference, signers]
                    properties:
                      client_reference: { type: string, maxLength: 160 }
                      signers: { type: array, items: { $ref: '#/components/schemas/SignerInput' } }
                      field_values: { type: object, additionalProperties: { type: string, maxLength: 500 } }
                      return_url: { type: string, format: uri, description: Overrides the batch return_url. }
                callback_url: { type: string, format: uri }
                return_url: { type: string, format: uri }
                email: { $ref: '#/components/schemas/EmailBranding' }
                sequential_signing: { type: boolean, default: false }
                send_emails: { type: boolean, default: false }
                link_expiry_days: { type: integer, minimum: 1, maximum: 30, default: 7 }
                embed_origin: { type: string, maxLength: 255, description: Same as on a single request. }
      responses:
        '200':
          description: Per-recipient outcome.
          content:
            application/json:
              schema:
                type: object
                required: [template_id, total, created, failed, results]
                properties:
                  template_id: { type: string, format: uuid }
                  total: { type: integer }
                  created: { type: integer }
                  failed: { type: integer }
                  results:
                    type: array
                    items:
                      type: object
                      required: [client_reference, status]
                      properties:
                        client_reference: { type: string }
                        status: { type: string, enum: [active, already_exists, error] }
                        id: { type: string, format: uuid }
                        signers:
                          type: array
                          items:
                            type: object
                            required: [id, reference, email, status, signing_url, email_delivery]
                            properties:
                              id: { type: string, format: uuid }
                              reference: { type: string }
                              email: { type: string, format: email }
                              status: { type: string }
                              signing_url: { type: string, format: uri, description: Secret signer-specific URL. }
                              embed_url: { type: string }
                              email_delivery: { type: string, enum: [not_requested, sent, failed] }
                        error:
                          type: object
                          required: [code, message]
                          properties:
                            code: { type: string, enum: [invalid_request, temporarily_unavailable, activation_failed] }
                            message: { type: string }
        '400':
          description: Invalid batch (no recipients, more than 50, invalid options).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: Template not found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: The template has no confirmed fields.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /webhook-deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: List webhook events and their delivery state
      description: Newest first. Pass `next_cursor` back as `cursor` to read the next page.
      parameters:
        - { name: signature_request_id, in: query, schema: { type: string, format: uuid } }
        - { name: event_type, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [pending, delivered, failed] } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200':
          description: A page of deliveries.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookDelivery' }
                  next_cursor: { type: string }
        '400':
          description: Invalid filter, limit or cursor.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /webhook-deliveries/{id}/replay:
    post:
      tags: [Webhooks]
      operationId: replayWebhookDelivery
      summary: Send an event again now
      description: |-
        Sends the event immediately with the same `webhook-id` and a fresh signature, then
        restarts its retry schedule if your endpoint does not answer 2xx. Works for delivered
        events too, for example after fixing a bug in your receiver.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Outcome of this attempt.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookDelivery' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /signature-requests/{id}/signers/{reference}/replace:
    post:
      tags: [Signature requests]
      operationId: replaceSigner
      summary: Give a signer's role to another person
      description: |-
        Before activation, the signer is simply corrected. Once the request is active, the previous
        signer is revoked (their link stops working), and a new signer takes the same reference,
        order and fields, with a fresh signing link. Signers who already signed or declined cannot be replaced.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: reference, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [email]
              properties:
                name: { type: string, maxLength: 200 }
                email: { type: string, format: email }
                locale: { type: string, enum: [fr, en, ar] }
                send_email: { type: boolean, default: false, description: Email the new signing link to the new signer. }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200':
          description: The signer was replaced. `signing_url` is empty before activation.
          content:
            application/json:
              schema:
                type: object
                required: [id, signer]
                properties:
                  id: { type: string, format: uuid }
                  signer:
                    type: object
                    required: [id, reference, email, status, signing_url, email_delivery]
                    properties:
                      id: { type: string, format: uuid }
                      reference: { type: string }
                      email: { type: string, format: email }
                      status: { type: string }
                      signing_url: { type: string }
                      embed_url: { type: string }
                      email_delivery: { type: string }
        '400':
          description: Invalid email or locale, or the email is already used by another signer.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: No active signer has this reference.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: The signer already signed or declined, or the request is closed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /signature-requests/{id}/cancel:
    post:
      tags: [Signature requests]
      operationId: cancelSignatureRequest
      summary: Cancel an active or prepared request
      description: Invalidates all unsigned signer links, records the reason and emits signature_request.cancelled when a callback URL exists.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CancellationInput' }
      responses:
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '200': { description: Request cancelled. }
        '400':
          description: Invalid cancellation payload.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The request is completed, cancelled, purged or expired.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /signature-requests/{id}:
    get:
      tags: [Signature requests]
      operationId: getSignatureRequest
      summary: Get request and signer status
      description: Return workflow status without access tokens, IP addresses or user agents.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Current workflow, signer and artifact status.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SignatureRequestStatus' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/InternalError' }
  /signature-requests/{id}/artifacts/{type}:
    get:
      tags: [Artifacts]
      operationId: downloadSignatureArtifact
      summary: Download a completed artifact
      description: |-
        Artifacts become available after completion and are purged at the
        request retention deadline. Use the status endpoint to check availability.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: type
          in: path
          required: true
          schema:
            type: string
            enum: [signed-pdf, evidence.pdf, evidence.json]
      responses:
        '200':
          description: The selected PDF or JSON evidence artifact.
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/json:
              schema: { $ref: '#/components/schemas/EvidenceResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The artifact is not available yet.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '410':
          description: The content was purged after its retention deadline.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/InternalError' }
webhooks:
  statusEvent:
    post:
      operationId: receiveKhatmStatusEvent
      summary: Receive a signed Khatm status event
      description: |-
        Khatm sends the raw JSON body to the `callback_url` supplied at creation.
        It retries after 1 minute, 5 minutes, 30 minutes and 2 hours until a 2xx
        response is received. `GET /webhook-deliveries` shows every event and its
        delivery state; `POST /webhook-deliveries/{id}/replay` sends one again.

        Events: `signature_request.activated`, `signer.viewed` (first opening of the
        link), `signer.completed`, `signer.declined`, `signer.replaced`, `signer.expired`,
        `signature_request.completed`, `signature_request.declined`,
        `signature_request.cancelled` and `signature_request.expired` (no signer can sign
        any more). Signer events carry the signer in `data.signer`. Events can arrive out of
        order: rely on `GET /signature-requests/{id}` for the current state.

        Verify `webhook-signature` using HMAC-SHA256 and the webhook secret linked
        to the API key. The signed bytes are:
        `webhook-id + "." + webhook-timestamp + "." + raw_request_body`.
        Compare the expected value `v1,` followed by standard Base64 HMAC output
        using a constant-time comparison. Reject stale timestamps and previously
        processed `webhook-id` values in your application.
      security: []
      parameters:
        - name: webhook-id
          in: header
          required: true
          schema: { type: string, example: evt_535d0c }
        - name: webhook-timestamp
          in: header
          required: true
          schema: { type: string, example: '1790694709' }
        - name: webhook-signature
          in: header
          required: true
          schema: { type: string, example: 'v1,base64-hmac-sha256' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: Delivery accepted. Any 2xx response stops retries. }
x-sandbox-limits:
  pdf_max_bytes: 8388608
  signers_per_request: 10
  fields_per_request: 100
  request_rate: 120 requests per 10 minutes per API key and IP
  detection_rate: 12 analyses per hour per API key and IP
  reminder_rate: 20 reminder calls per hour per API key and IP
  content_retention: 10 days from the original PDF upload
