openapi: 3.1.0
info:
  title: IDfier Identity API
  version: '3'
  description: |
    Log customers in with a verified identity, check their age, or send them a
    link to verify. The customer approves in the IDfier app (or an EU Digital
    Identity Wallet) and you receive exactly the fields in your scopes.

    **Access.** IDfier issues a business client: a `clientId`, a
    `clientSecret` and the scopes it may receive. Send both on every request
    as the `clientid` and `clientsecret` headers, from your server only.

    **Responses** are JSON with a numeric `status` (100 on success) and
    `data`. Errors use standard HTTP codes and carry the reason in
    `data.error`.
servers:
  - url: https://api.idfier.com/api/v3
security:
  - clientId: []
    clientSecret: []

tags:
  - name: Login
    description: Login with IDfier. Create a session, show its QR code, read the result.
  - name: Verification links
    description: Send a customer a link to verify. Each link uses one verification credit.

paths:
  /business/auth/request:
    post:
      tags: [Login]
      operationId: createLoginSession
      summary: Create a login session
      description: |
        Starts a login session, valid for 5 minutes. Build the QR code (or, on
        a phone, the app link) from the three values in the response; see the
        QR code payload guide. The session's scopes are fixed now, from your
        business client.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                webhookUrl:
                  type: string
                  format: uri
                  maxLength: 500
                  description: Optional `https` URL that receives the session's events
                  example: https://example.com/idfier/webhook
                webhookSecret:
                  type: string
                  minLength: 16
                  maxLength: 255
                  description: Required with `webhookUrl`; signs each event (HMAC-SHA256)
      responses:
        '201':
          description: The session
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data:
                    type: object
                    required: [referenceId, qrReferenceSecret, createdAt]
                    properties:
                      referenceId: { type: string, format: uuid, description: The session's id }
                      qrReferenceSecret: { type: string, description: 'Signs the QR code payload. Keep it for showing the QR code; never log it.' }
                      createdAt: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/auth/result:
    post:
      tags: [Login]
      operationId: getLoginResult
      summary: Get a session's status and result
      description: |
        Poll this from your server (every 2 s, backing off to 10 s) until the
        status is `approved`, `denied` or `error`, or `expiresAt` has passed.
        `user` is set once approved: always `id`, plus the fields in your
        scopes that IDfier has for the customer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [referenceId]
              properties:
                referenceId: { type: string, format: uuid }
      responses:
        '200':
          description: The session, and the customer once approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data:
                    type: object
                    properties:
                      user:
                        oneOf:
                          - $ref: '#/components/schemas/Customer'
                          - type: 'null'
                      session: { $ref: '#/components/schemas/Session' }
              example:
                status: 100
                data:
                  user: { id: u_3f9a2c, fullname: Anna Lindqvist, ageOver18: true, authLevel: Gold }
                  session:
                    referenceId: 550e8400-e29b-41d4-a716-446655440000
                    status: approved
                    method: idfier
                    expiresAt: '2026-10-31T10:05:00.000Z'
                    createdAt: '2026-10-31T10:00:00.000Z'
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/auth/wallet:
    post:
      tags: [Login]
      operationId: startWalletLogin
      summary: Offer an EU Digital Identity Wallet
      description: |
        For a session that is still `pending`, returns a link that opens the
        customer's EU Digital Identity Wallet instead of the IDfier app. Show
        it as a QR code, or as a link on a phone. Calling it again while the
        request is open returns the same `walletUrl`. Only available when
        IDfier has enabled EU wallets.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [referenceId]
              properties:
                referenceId: { type: string, format: uuid }
      responses:
        '200':
          description: The wallet link
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data:
                    type: object
                    properties:
                      walletUrl: { type: string, example: 'haip-vp://?client_id=…&request_uri=…' }
                      expiresAt: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/links:
    post:
      tags: [Verification links]
      operationId: createLink
      summary: Create a verification link
      description: |
        Uses one verification credit. The `url` is only returned here and on
        resend: IDfier stores only a hash of it.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                recipientName: { type: string, maxLength: 200, description: Shown on the link page and in the email }
                recipientEmail: { type: string, format: email, description: 'IDfier emails the link to it, unless `sendEmail` is false' }
                message: { type: string, maxLength: 1000, description: A message to the customer }
                scopes:
                  type: array
                  items: { type: string }
                  description: |
                    What to verify; defaults to all your scopes. Must be among
                    your scopes. Any age check (`ageOverN`,
                    `ageBetween{min}And{max}`) is allowed if you have `age`,
                    `dateOfBirth` or an age scope.
                  example: [fullname, ageOver18]
                expiresInDays: { type: integer, minimum: 1, maximum: 30, default: 7 }
                sendEmail: { type: boolean, default: true }
                createdBy: { type: string, description: Your own note of who created it }
      responses:
        '201':
          description: The link
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data: { $ref: '#/components/schemas/IssuedLink' }
        '400': { $ref: '#/components/responses/Invalid' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/NoCredits' }
        '500': { $ref: '#/components/responses/ServerError' }
    get:
      tags: [Verification links]
      operationId: listLinks
      summary: List verification links
      description: Newest first. Pass `nextBefore` as `before` for the next page.
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { name: before, in: query, description: '`nextBefore` from the previous page', schema: { type: string, format: date-time } }
      responses:
        '200':
          description: A page of links
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data:
                    type: object
                    properties:
                      links:
                        type: array
                        items: { $ref: '#/components/schemas/Link' }
                      nextBefore: { type: [string, 'null'], format: date-time }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/links/{id}:
    parameters: [{ $ref: '#/components/parameters/LinkId' }]
    get:
      tags: [Verification links]
      operationId: getLink
      summary: Get a link and its result
      description: '`result` is set once the customer has approved: exactly the requested scopes, plus an `id` unique to your business.'
      responses:
        '200':
          description: The link
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Link'
                      - type: object
                        properties:
                          method: { $ref: '#/components/schemas/Method' }
                          result:
                            oneOf:
                              - $ref: '#/components/schemas/Customer'
                              - type: 'null'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    delete:
      tags: [Verification links]
      operationId: deleteLink
      summary: Delete a link
      description: Deletes the link and its record of the customer.
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/links/{id}/cancel:
    parameters: [{ $ref: '#/components/parameters/LinkId' }]
    post:
      tags: [Verification links]
      operationId: cancelLink
      summary: Cancel an open link
      responses:
        '200':
          description: The link, now `cancelled`
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data: { $ref: '#/components/schemas/Link' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '500': { $ref: '#/components/responses/ServerError' }

  /business/links/{id}/resend:
    parameters: [{ $ref: '#/components/parameters/LinkId' }]
    post:
      tags: [Verification links]
      operationId: resendLink
      summary: Issue a new URL and resend
      description: |
        Issues a new URL (the old one stops working), extends the expiry and
        emails it again if the link has a `recipientEmail`. Works on `sent`,
        `opened` and `expired` links.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                expiresInDays: { type: integer, minimum: 1, maximum: 30, default: 7 }
      responses:
        '200':
          description: The new URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { $ref: '#/components/schemas/Success' }
                  data: { $ref: '#/components/schemas/IssuedLink' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '500': { $ref: '#/components/responses/ServerError' }

components:
  securitySchemes:
    clientId:
      type: apiKey
      in: header
      name: clientid
    clientSecret:
      type: apiKey
      in: header
      name: clientsecret

  parameters:
    LinkId:
      name: id
      in: path
      required: true
      schema: { type: string }

  schemas:
    Success:
      type: integer
      const: 100
      description: 100 on success

    Error:
      type: object
      properties:
        status:
          type: integer
          enum: [101, 103, 105, 106]
          description: 101 not found · 103 not authorised · 105 failed · 106 no credits
        data:
          type: object
          properties:
            error: { type: string, description: 'A message for your logs; branch on the HTTP status instead' }

    Method:
      type: [string, 'null']
      enum: [idfier, eudi_wallet, null]
      description: How the customer verified, once approved

    Session:
      type: object
      properties:
        referenceId: { type: string, format: uuid }
        status:
          type: string
          enum: [pending, started, approved, denied, error]
          description: |
            `pending` not scanned yet · `started` scanned, waiting for approval ·
            `approved` · `denied` by the customer · `error`. A session still
            `pending` or `started` after `expiresAt` has expired.
        method: { $ref: '#/components/schemas/Method' }
        expiresAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }

    Customer:
      type: object
      description: |
        `id` is always there. Every other field is sent only if it is in your
        scopes and IDfier has it; treat a missing field as unknown, and a
        missing age check as not verified.
      required: [id]
      additionalProperties: true
      properties:
        id: { type: string, description: 'Stable for the customer within your business, and different for every other business', example: u_3f9a2c }
        fullname: { type: string }
        handle: { type: string }
        email: { type: string }
        personalNo: { type: string }
        age: { type: integer, description: Age in whole years today }
        dateOfBirth: { type: string }
        ageOver13: { type: boolean }
        ageOver16: { type: boolean }
        ageOver18: { type: boolean }
        ageOver21: { type: boolean }
        ageOver65: { type: boolean }
        country: { type: string }
        nationality: { type: string }
        gender: { type: string }
        documentType: { type: string }
        authLevel:
          type: string
          enum: [Gold, Silver, Bronze, EU Wallet, Unknown]
          description: How the customer's identity was verified
      patternProperties:
        '^ageBetween\d+And\d+$': { type: boolean }

    Link:
      type: object
      properties:
        id: { type: string }
        recipientName: { type: [string, 'null'] }
        recipientEmail: { type: [string, 'null'] }
        message: { type: [string, 'null'] }
        scopes: { type: array, items: { type: string } }
        status: { type: string, enum: [sent, opened, approved, denied, cancelled, expired] }
        createdBy: { type: [string, 'null'] }
        expiresAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        openedAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }

    IssuedLink:
      type: object
      properties:
        id: { type: string }
        url: { type: string, format: uri, description: 'The link to give the customer; only returned on create and resend', example: 'https://www.idfier.com/v/…' }
        expiresAt: { type: string, format: date-time }
        emailSent: { type: boolean }

  responses:
    Invalid:
      description: Missing or invalid input (status 105)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing, wrong or deactivated `clientid` / `clientsecret` (status 103)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NoCredits:
      description: No verification credits left (status 106)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Doesn't exist, or belongs to another business (status 101)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Doesn't fit the current state, e.g. cancelling an approved link (status 105)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ServerError:
      description: A problem at IDfier; retry later (status 105)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
