openapi: 3.1.0
info:
  title: IDfier Sign API
  version: '1'
  description: |
    Create documents, invite signers who sign with a verified identity (IDfier
    or BankID), and follow the signing from your own platform.

    **Access.** API keys are created by a company's owner or admins in
    IDfier Sign (Company → API), on the Business, Enterprise and API plans. A key
    acts as one person in the company and can do what they can do; it can be
    limited to one workspace. Send it as `Authorization: Bearer isk_live_…`.

    **Signing.** Signers get an email invitation (POST …/send, or `send=true`)
    or you deliver each signer's `signingUrl` yourself. Signers verify with
    IDfier or BankID and sign on a focused page; with a `returnUrl` they come
    back to you afterwards, with `idfiersign_document` and `idfiersign_status`
    (`signed`, `declined` or `viewed`) added to it. Poll `GET /documents/{id}`
    (or find it by your `reference`) until `status` is `completed`, then
    download `/signed-file`.

    **Usage.** On the API plan you pay per verified signature (IDfier or
    BankID) on your documents per calendar month; `GET /usage` shows where you
    are.

    **Retries.** Send an `Idempotency-Key` header on POSTs. Retrying with the
    same key returns the first response (with `Idempotent-Replayed: true`)
    instead of doing the work twice.

    **Limits.** 120 requests per minute per key (429 with `Retry-After`).
    Files up to 20 MB.

    **Errors** always look like `{ "error": { "code": "...", "message": "..." } }`.
servers:
  - url: https://sign.idfier.com/api/v1
security:
  - apiKey: []

paths:
  /usage:
    get:
      summary: Verified signatures this month (or last), and their cost on the API plan
      parameters:
        - { name: month, in: query, description: 'previous for last month; leave out for this month', schema: { type: string, enum: [previous] } }
      responses:
        '200':
          description: Usage
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Usage' }
        default: { $ref: '#/components/responses/Error' }

  /workspaces:
    get:
      summary: Workspaces the key can use
      responses:
        '200':
          description: Workspaces, with the key's role in each
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Workspace' }
        default: { $ref: '#/components/responses/Error' }

  /templates:
    get:
      summary: Active templates
      parameters:
        - { name: workspaceId, in: query, schema: { type: string } }
      responses:
        '200':
          description: Templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Template' }
        default: { $ref: '#/components/responses/Error' }

  /documents:
    get:
      summary: List documents, newest first
      parameters:
        - { name: status, in: query, schema: { $ref: '#/components/schemas/DocumentStatus' } }
        - { name: workspaceId, in: query, schema: { type: string } }
        - { name: reference, in: query, description: 'Your own id, as given when creating', schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, description: '`nextCursor` from the previous page', schema: { type: string } }
      responses:
        '200':
          description: A page of documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DocumentSummary' }
                  nextCursor: { type: [string, 'null'] }
        default: { $ref: '#/components/responses/Error' }
    post:
      summary: Upload a document, optionally with signers
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: 'At most 20 MB' }
                title: { type: string, description: 'Defaults to the file name' }
                description: { type: string }
                workspaceId: { type: string, description: "Defaults to the key's workspace, or the acting person's default" }
                templateId: { type: string, description: "Apply a template's settings and recipients" }
                expiresAt: { type: string, format: date-time }
                signers:
                  type: string
                  description: 'JSON array of SignerInput. With a template, replaces its recipients.'
                  example: '[{"email":"anna@customer.com"},{"email":"cc@customer.com","role":"viewer"}]'
                send: { type: string, enum: ['true', 'false'], description: 'Email the invitations now' }
                returnUrl: { type: string, format: uri, description: 'https URL signers go to after signing or declining' }
                reference: { type: string, maxLength: 200, description: 'Your own id for the document, e.g. a booking' }
      responses:
        '201':
          description: The document
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentWithDelivery' }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    get:
      summary: A document, its signers and their progress
      responses:
        '200':
          description: The document
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/signers:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    post:
      summary: Add signers or viewers
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [signers]
              properties:
                signers:
                  type: array
                  items: { $ref: '#/components/schemas/SignerInput' }
                send: { type: boolean, description: 'Email the new invitations now' }
      responses:
        '201':
          description: The document
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentWithDelivery' }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/send:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    post:
      summary: Email the invitations that haven't gone out yet
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The document, and who was emailed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentWithDelivery' }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/void:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    post:
      summary: Cancel a document that isn't completed
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        '200':
          description: The document (status void, open invitations revoked)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/file:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    get:
      summary: The original upload
      responses:
        '200': { description: The file, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/signed-file:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    get:
      summary: The signed document (once completed; 409 before)
      responses:
        '200': { description: The signed file, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
        default: { $ref: '#/components/responses/Error' }

  /documents/{id}/certificate:
    parameters: [{ $ref: '#/components/parameters/DocumentId' }]
    get:
      summary: The signing certificate (once issued)
      responses:
        '200': { description: The certificate, content: { text/plain: { schema: { type: string } } } }
        default: { $ref: '#/components/responses/Error' }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: 'isk_live_…'

  parameters:
    DocumentId:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Any unique string (at most 255 characters). Retries with it return the first response.
      schema: { type: string, maxLength: 255 }

  responses:
    Error:
      description: |
        400 invalid input · 401 missing, wrong or revoked key · 403 not allowed
        (plan_has_no_api, acting_user_removed, not_allowed, workspace_read_only) ·
        404 not found or out of the key's reach · 409 conflict, request_in_progress,
        not_completed · 413 file_too_large · 422 cannot be done in the document's
        state, idempotency_key_reused · 429 rate_limited · 500 internal_error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }

    DocumentStatus:
      type: string
      enum: [draft, in_signature, completed, declined, void]

    Workspace:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        role: { type: string, enum: [manager, member, read_only] }
        canCreateDocuments: { type: boolean }

    Template:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: [string, 'null'] }
        workspaceId: { type: string }
        defaultExpiresInDays: { type: [integer, 'null'] }
        hasDefaultFile: { type: boolean }
        recipients:
          type: array
          items:
            type: object
            properties:
              email: { type: string }
              role: { type: string, enum: [signer, viewer] }
              order: { type: integer }

    SignerInput:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        personalNumber: { type: string, description: 'Only this person (verified by IDfier or BankID) can sign' }
        role: { type: string, enum: [signer, viewer], default: signer }
        message: { type: string }

    DocumentSummary:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        workspace: { type: object, properties: { id: { type: string }, name: { type: [string, 'null'] } } }
        owner: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        links: { type: object, properties: { self: { type: string } } }

    Document:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: [string, 'null'] }
        reference: { type: [string, 'null'] }
        returnUrl: { type: [string, 'null'] }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        verificationMethods:
          type: array
          description: "How signers verify themselves: the company's choice (Company → API), as available"
          items: { type: string, enum: [idfier, bankid] }
        workspace: { type: [object, 'null'], properties: { id: { type: string }, name: { type: [string, 'null'] } } }
        owner: { type: string }
        fileName: { type: [string, 'null'] }
        fileHash: { type: [string, 'null'], description: SHA-256 of the original }
        signedFileHash: { type: [string, 'null'], description: SHA-256 of the signed file }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        expiresAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        signers:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              email: { type: [string, 'null'] }
              role: { type: string, enum: [signer, viewer] }
              order: { type: integer }
              status: { type: string, enum: [pending, accepted, signed, declined, revoked] }
              invitedAt: { type: string, format: date-time }
              notifiedAt: { type: [string, 'null'], format: date-time }
              signedAt: { type: [string, 'null'], format: date-time }
              signingMethod: { type: [string, 'null'], enum: [idfier, bankid, manual, null] }
              signingUrl: { type: [string, 'null'], description: "The signer's personal link" }
        certificate:
          type: [object, 'null']
          properties:
            issuedAt: { type: string, format: date-time }
            verificationUrl: { type: [string, 'null'] }
        links:
          type: object
          properties:
            self: { type: string }
            file: { type: [string, 'null'] }
            signedFile: { type: [string, 'null'] }
            certificate: { type: [string, 'null'] }

    Usage:
      type: object
      properties:
        plan: { type: string }
        month: { type: string, example: '2026-10' }
        verifiedSignatures: { type: integer }
        includedSignatures: { type: [integer, 'null'], description: 'API plan only' }
        overageSignatures: { type: integer }
        overageCost: { type: number }
        basePrice: { type: number }
        total: { type: number }

    DocumentWithDelivery:
      allOf:
        - $ref: '#/components/schemas/Document'
        - type: object
          properties:
            delivery:
              type: object
              description: Present when invitations were sent
              properties:
                sent: { type: array, items: { type: string } }
                notSent:
                  type: array
                  items:
                    type: object
                    properties:
                      email: { type: string }
                      reason: { type: string, enum: [email_not_configured, plan_has_no_email_invitations, email_failed] }
