> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Step-Up | Didit Authentication API

> POST /step-up/ applies your step-up rules before a sensitive action. It returns required false, or a verification URL for the person.

> [!WARNING]
>
> Authentication is new. These endpoints are served by your application's issuer, such as `https://<name>.auth.didit.me`, and never take your Didit API key.

The `step_up` and `status` query parameters on your `return_to` only tell your application to look. Before you act, read the step-up with [Get a step-up](https://docs.didit.me/authentication/api-reference/retrieve-step-up.md) and continue only when `status` is `approved`.


## OpenAPI

````yaml openapi-authentication.json POST /step-up/
openapi: 3.0.3
info:
  title: Didit Authentication API
  version: 1.0.0
  description: >-
    The server-facing endpoints of Didit Authentication, the sign-in product for
    your application's end users. Authentication is **new** and reaches
    production in the 2026-11-01 release.


    ## Base URL


    Every application is its own OpenID Connect issuer, and every endpoint below
    is served at the root of that issuer:


    - `https://<name>.auth.didit.me` for a live application, or
    `https://<name>.sandbox.auth.didit.me` for a sandbox application.

    - Your own custom domain, once it is active. It then replaces the default
    host as the issuer: new tokens name it in `iss`, discovery points at it and
    the publishable key changes. Access tokens issued under the default host
    fail `/session/`, `/step-up/` and `/monitoring-token/` until they are
    refreshed.


    Live and sandbox are separate applications, so they are separate issuers
    with separate keys. The publishable key is `pk_live_` (live) or `pk_test_`
    (sandbox) followed by the unpadded base64url encoding of the issuer, without
    a trailing slash: decode it to find the issuer. It is a public identifier,
    not a credential.


    The same JSON endpoints are also served under
    `https://apx.didit.me/auth/v2/authentication/{application_id}/`. Tokens
    always name the issuer above, whichever address issued them.


    ## Credentials


    These endpoints never take your Didit API key or a console token.


    - Discovery, the JWKS and `POST /token/` take no credential. Clients are
    public: there is no client secret, and an authorization code is bound to its
    PKCE verifier (`S256`). `client_id` is your application ID.

    - `/session/`, `/step-up/` and `/monitoring-token/` take the person's access
    token: `Authorization: Bearer <access_token>`. Each successful call counts
    as activity on the session, so it restarts the session's idle timeout.


    ## Request bodies


    Every endpoint that takes a body accepts JSON, form-encoded and multipart
    bodies. This document lists JSON, plus form-encoded for `POST /token/` as
    OAuth clients send it. A field the endpoint does not accept gets `400
    {"<field>": "unknown_field"}`.


    ## Tokens


    - **Access token**: an ES256 JWT with header `typ: at+jwt`, valid for at
    most 5 minutes and never longer than its session. Claims:
    `AccessTokenClaims`.

    - **ID token**: an ES256 JWT with header `typ: JWT`. Claims:
    `IDTokenClaims`. It is never accepted as an access token.

    - **Refresh token**: a random string that rotates on every use. Sending a
    used refresh token again revokes the session.


    Verify a token with the keys from `GET /.well-known/jwks.json`: the
    algorithm must be `ES256`, `typ` must match the token type, `iss` must equal
    the issuer you configured and `aud` your application ID. Offline
    verification sees a revoked session only once its access token expires; `GET
    /session/` is the instant check.


    ## Errors


    | Body | When |

    | --- | --- |

    | `{"error": "<code>"}` | Errors from `POST /token/`, all with status `400`,
    in the body shape of [RFC 6749 section
    5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2): `invalid_grant`,
    `invalid_client` and Didit's `mfa_required`. Service errors:
    `temporarily_unavailable`, `monitoring_unavailable`, `monitoring_disabled`.
    |

    | `{"<field>": ["<message>"]}` or `{"<field>": "<code>"}` | A request field
    failed validation. `non_field_errors` names a rule across fields, and
    `unknown_field` a field the endpoint does not accept. |

    | `{"detail": "<message>"}` | `401` (`invalid_token`), `403`
    (`origin_not_allowed`), `404` (unknown application or step-up) and `429`
    (rate limit). Also `400` for a body that cannot be parsed (`JSON parse error
    - ...`), `405` for a method the endpoint does not serve and `415` for an
    unsupported `Content-Type`. |


    A `401` also carries `WWW-Authenticate: Bearer`, and a `429` carries
    `Retry-After` in seconds.


    Two refusals are not JSON. An address that is not an issuer is refused
    before it reaches these endpoints, for example with an empty `404` for an
    unused `*.auth.didit.me` name. A `step_up_id` that is not a UUID gets a
    `404` page.


    ## Rate limits


    A request has to pass two layers.


    **Endpoint limits**, counted per client IP address, per application and per
    endpoint:


    - Discovery and the JWKS: 1,200 requests a minute and 12,000 an hour.

    - Every other endpoint: 30 requests a minute and 300 an hour. `GET` and
    `DELETE /session/` share one count.


    Over a limit: `429 {"detail": "Too many requests from this IP address.
    Please wait before trying again. Expected available in 60 seconds."}`, or
    `3600 seconds` for the hourly limit.


    **Bot protection**, on every `POST` and `DELETE`. Its buckets are shared by
    all of the application's `POST` and `DELETE` requests, the hosted sign-in
    steps included, and refill continuously:


    - 60 requests a minute per client IP address.

    - 30 requests a minute per device, for requests that identify their device,
    as the browser and native SDKs do.

    - 1,000 requests a minute for the whole application, across every IP
    address.


    Over a bucket: `429 {"detail": "rate_limited Expected available in N
    seconds."}` (`1 second.` when N is 1). The per-IP bucket spans endpoints, so
    one address that mixes token, step-up and sign-out calls can empty it before
    it reaches any endpoint limit. The application bucket counts every client of
    the application, browsers included.


    Both layers answer before the endpoint runs, so a request that got `429` did
    not run: retry it after `Retry-After`, a refresh included.


    ## Browser requests


    A request that carries an `Origin` header must come from one of the
    application's own origins or its allowed origins, or it gets `403
    origin_not_allowed`. Server-to-server requests send no `Origin`.


    ## Not covered here


    These routes serve the hosted pages, the browser and native SDKs, and AI
    agents. They are not part of the server contract, and server SDKs do not
    call them:


    - Sign-in steps: `POST /authorize/`, `POST /password/`, `/email/*`,
    `/phone/*`, `/password-reset/*`, `/passkeys/*`, `/mfa/*`, `/social/*` and
    `/protection/complete/`.

    - Hosted pages and callbacks: `/sign-in`, `/magic`, `/invite`, `/reset`,
    `/verify/`, `/sso/*`, `/wallet/callback/`, `/logo/` and the native app
    association files under `/.well-known/`.

    - Agent access: `/agents/*` and `/grants/`, including `GET
    /agents/session/`, the live check for an AI agent's access token. Agent
    access is not part of this version of the contract.

    - The console management API for Authentication, which takes console
    sessions.


    Authentication events are not delivered as webhooks yet, so this document
    defines no webhook.
servers:
  - url: https://{issuer}
    description: 'Your application''s issuer: its sign-in origin.'
    variables:
      issuer:
        default: your-app.auth.didit.me
        description: >-
          The issuer's host: `<name>.auth.didit.me` (live),
          `<name>.sandbox.auth.didit.me` (sandbox) or your active custom domain.
security: []
tags:
  - name: Issuer
    description: Discovery and signing keys, to verify tokens.
  - name: Tokens
    description: The authorization code and refresh grants.
  - name: Sessions
    description: Check and end the session behind an access token.
  - name: Step-ups
    description: Verification before a sensitive action.
  - name: Monitoring
    description: Device credentials for transaction monitoring events.
paths:
  /step-up/:
    post:
      tags:
        - Step-ups
      summary: Create a step-up
      description: >-
        Asks whether the person has to verify before a sensitive action, using
        the step-up rules you set for the application.


        - `200 {"required": false}`: no rule applies, go ahead.

        - `201`: send the person to `verification_url`. They come back to
        `return_to` with `step_up=<id>&status=approved`, but that query proves
        nothing: read the step-up with `GET /step-up/{step_up_id}/` and act only
        on `approved`.


        A step-up never fails open: when verification is unavailable, the action
        stays blocked.
      operationId: authentication_create_step_up
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StepUpRequest'
            examples:
              Payout:
                summary: Before a payout
                value:
                  action: payout
                  amount: '900.00'
                  currency: USD
                  return_to: https://app.example.com/payouts/confirm
              Bank:
                summary: Before changing a bank account
                value:
                  action: bank
                  return_to: https://app.example.com/settings/bank
      responses:
        '200':
          description: No rule applies.
          headers:
            Cache-Control:
              description: 'Always `no-store`: never cache this response.'
              schema:
                type: string
                example: no-store
            Pragma:
              description: Always `no-cache`.
              schema:
                type: string
                example: no-cache
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepUpNotRequired'
              examples:
                NotRequired:
                  summary: Go ahead
                  value:
                    required: false
        '201':
          description: The person has to verify first.
          headers:
            Cache-Control:
              description: 'Always `no-store`: never cache this response.'
              schema:
                type: string
                example: no-store
            Pragma:
              description: Always `no-cache`.
              schema:
                type: string
                example: no-cache
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepUpRequired'
              examples:
                Required:
                  summary: A selfie before a payout
                  value:
                    id: 94efc2c3-ca84-42fe-900b-6fb61669a7a1
                    rule: payout
                    method: selfie
                    status: pending
                    level: null
                    amount: '900.00'
                    currency: USD
                    created_at: '2026-10-17T10:41:14.894632Z'
                    completed_at: null
                    expires_at: '2026-10-17T11:11:14.894581Z'
                    required: true
                    verification_url: >-
                      https://your-app.auth.didit.me/verify/?verification=1nSX7HJCXAP9XPA9AeJfQxIuPk2GkG0dGSOzCA2Zpww
                    expires_in: 1800
        '400':
          description: >-
            A field is missing or invalid. `return_to: invalid_redirect_uri`
            means a rule applies and `return_to` is not one of the application's
            redirect URIs.
          headers:
            Cache-Control:
              description: 'Always `no-store`: never cache this response.'
              schema:
                type: string
                example: no-store
            Pragma:
              description: Always `no-cache`.
              schema:
                type: string
                example: no-cache
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FieldErrors'
              examples:
                MissingAmount:
                  summary: A payout without amount
                  value:
                    amount:
                      - required
                InvalidReturnTo:
                  summary: Unregistered return_to
                  value:
                    return_to: invalid_redirect_uri
                UnknownAction:
                  summary: Unsupported action
                  value:
                    action:
                      - '"withdraw" is not a valid choice.'
        '401':
          $ref: '#/components/responses/InvalidToken'
        '403':
          $ref: '#/components/responses/OriginNotAllowed'
        '404':
          $ref: '#/components/responses/ApplicationNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/TemporarilyUnavailable'
      security:
        - AccessToken: []
components:
  schemas:
    StepUpRequest:
      type: object
      description: The sensitive action the person is about to take.
      required:
        - action
        - return_to
      additionalProperties: false
      properties:
        action:
          type: string
          enum:
            - payout
            - bank
            - contact
          description: >-
            `payout` (a withdrawal or transfer), `bank` (adding or changing a
            bank account) or `contact` (changing an email or phone number). A
            payout also applies the new-device rule.
        amount:
          type: string
          pattern: ^[0-9]{1,16}(\.[0-9]{1,2})?$
          description: >-
            Required for `payout`: the amount, up to two decimals. A JSON number
            is accepted too.
          example: '900.00'
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          description: >-
            ISO 4217 code of `amount`. Defaults to the payout rule's currency; a
            payout in another currency is always checked.
          example: USD
        return_to:
          type: string
          maxLength: 2048
          description: >-
            Where the person returns after verifying, with
            `step_up=<id>&status=approved` added. When a rule applies, it must
            be one of the application's redirect URIs; when none applies, it is
            not checked.
          example: https://app.example.com/payouts/confirm
    StepUpNotRequired:
      type: object
      description: 'No rule applies: go ahead with the action.'
      required:
        - required
      properties:
        required:
          type: boolean
          enum:
            - false
    StepUpRequired:
      description: A new step-up. Send the person to `verification_url`.
      allOf:
        - $ref: '#/components/schemas/StepUp'
        - type: object
          required:
            - required
            - verification_url
            - expires_in
          properties:
            required:
              type: boolean
              enum:
                - true
            verification_url:
              type: string
              format: uri
              description: >-
                The hosted page where the person verifies; open it in the
                person's browser. It works until the step-up is approved or
                expires, so a person who was declined can try again from it.
              example: >-
                https://your-app.auth.didit.me/verify/?verification=1nSX7HJCXAP9XPA9AeJfQxIuPk2GkG0dGSOzCA2Zpww
            expires_in:
              type: integer
              description: 'Seconds the step-up stays open: 1800.'
              example: 1800
    FieldErrors:
      title: Field errors
      type: object
      description: >-
        Validation errors by field. A value is a list of messages, or a single
        code (`unknown_field`, `invalid_redirect_uri`). `non_field_errors` holds
        rules across fields.
      additionalProperties:
        oneOf:
          - type: string
          - type: array
            items:
              type: string
    StepUp:
      type: object
      description: A verification the person has to pass before the action.
      required:
        - id
        - rule
        - method
        - status
        - level
        - amount
        - currency
        - created_at
        - completed_at
        - expires_at
      properties:
        id:
          type: string
          format: uuid
          example: 94efc2c3-ca84-42fe-900b-6fb61669a7a1
        rule:
          type: string
          enum:
            - payout
            - device
            - bank
            - contact
          description: >-
            The rule that asked for it. When several apply, the one with the
            strongest method.
          example: payout
        method:
          type: string
          enum:
            - liveness
            - selfie
            - full_id
          description: >-
            What the person does: `liveness`, `selfie` (liveness matched to the
            face Didit already holds) or `full_id` (document and selfie). When
            Didit holds no face for the person yet, `selfie` becomes `full_id`
            once they open `verification_url`.
          example: selfie
        status:
          type: string
          enum:
            - pending
            - review
            - approved
            - declined
            - expired
            - superseded
          description: >-
            Act only on `approved`. `review` waits for your team's review.
            `declined` is not final while `verification_url` works: the person
            can try again, and the step-up returns to `pending`. `superseded`
            means another check took over its verification, or the result was
            not for this step-up: create a new one.
          example: pending
        level:
          type: string
          nullable: true
          enum:
            - liveness
            - liveness_face_match
            - full_id
            - null
          description: The level the verification reached, once approved.
          example: null
        amount:
          type: string
          nullable: true
          description: The payout amount, two decimals.
          example: '900.00'
        currency:
          type: string
          nullable: true
          example: USD
        created_at:
          type: string
          format: date-time
          example: '2026-10-17T10:41:14.894632Z'
        completed_at:
          type: string
          format: date-time
          nullable: true
          example: null
        expires_at:
          type: string
          format: date-time
          description: >-
            30 minutes after creation. Reading a step-up first collects any
            result its verification reached, so it can still read `approved`,
            `review` or `declined` after this time; one still `pending` reads
            `expired`.
          example: '2026-10-17T11:11:14.894581Z'
    Detail:
      type: object
      required:
        - detail
      properties:
        detail:
          type: string
          example: invalid_token
    OAuthError:
      title: OAuth error
      type: object
      description: An OAuth error (RFC 6749 section 5.2) or a service error.
      required:
        - error
      properties:
        error:
          type: string
          example: invalid_grant
  responses:
    InvalidToken:
      description: >-
        The access token is missing, malformed, expired, signed by another
        issuer or key, an ID token, an agent token, or replaced by a refresh; or
        its session was revoked, idled out, passed its lifetime or no longer
        meets your MFA policy, or its person was blocked or their email is no
        longer verified. Send the person through sign-in again.
      headers:
        WWW-Authenticate:
          description: '`Bearer`.'
          schema:
            type: string
            example: Bearer
        Cache-Control:
          description: 'Always `no-store`: never cache this response.'
          schema:
            type: string
            example: no-store
        Pragma:
          description: Always `no-cache`.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            InvalidToken:
              summary: Rejected access token
              value:
                detail: invalid_token
    OriginNotAllowed:
      description: >-
        The request carried an `Origin` that is neither the application's own
        nor in its allowed origins. Server requests send no `Origin`.
      headers:
        Cache-Control:
          description: 'Always `no-store`: never cache this response.'
          schema:
            type: string
            example: no-store
        Pragma:
          description: Always `no-cache`.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            OriginNotAllowed:
              summary: Origin not allowed
              value:
                detail: origin_not_allowed
    ApplicationNotFound:
      description: >-
        The address belongs to an application, but Authentication is not active
        for it; on the `apx.didit.me` address, the application ID is unknown. An
        address that belongs to no application is refused before it reaches the
        endpoint, and that response is not JSON.
      headers:
        Cache-Control:
          description: 'Always `no-store`: never cache this response.'
          schema:
            type: string
            example: no-store
        Pragma:
          description: Always `no-cache`.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            ApplicationNotFound:
              summary: Unknown application
              value:
                detail: No Configuration matches the given query.
    RateLimited:
      description: >-
        Too many requests from this IP address for this endpoint, or, on a
        `POST` or `DELETE`, a bot protection bucket is empty. Nothing ran: wait
        `Retry-After` seconds, then retry.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 60
        Cache-Control:
          description: 'Always `no-store`: never cache this response.'
          schema:
            type: string
            example: no-store
        Pragma:
          description: Always `no-cache`.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            RateLimited:
              summary: Endpoint limit
              value:
                detail: >-
                  Too many requests from this IP address. Please wait before
                  trying again. Expected available in 60 seconds.
            BotProtection:
              summary: Bot protection limit (POST and DELETE)
              value:
                detail: rate_limited Expected available in 2 seconds.
    TemporarilyUnavailable:
      description: >-
        A service the request depends on is briefly unavailable. Retry with
        backoff.
      headers:
        Cache-Control:
          description: 'Always `no-store`: never cache this response.'
          schema:
            type: string
            example: no-store
        Pragma:
          description: Always `no-cache`.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/OAuthError'
          examples:
            TemporarilyUnavailable:
              summary: Retry later
              value:
                error: temporarily_unavailable
  securitySchemes:
    AccessToken:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        The person's access token from `POST /token/`: an ES256 JWT with header
        `typ: at+jwt`, valid for at most 5 minutes. Not your Didit API key.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.