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

# Bulk provision tokens

> Accepts up to 1000 tokens for provisioning in the background. The batch is validated as a whole before it is accepted: one invalid item rejects the batch with 422, and each entry in `errors` names the item by index (`body.tokens[<index>].<field>`). uid uniqueness and metadata validation run per item during processing rather than on this call. This operation does not use the Idempotency-Key header; instead each item carries its own `idempotency_key`.

Requires the `emsp:tokens:write` scope.



## OpenAPI

````yaml /openapi/emsp-token/openapi.yaml post /emsp/v1/tokens/bulk
openapi: 3.0.0
info:
  title: Spirii EMMS - Token Management API
  description: >-
    Register, provision, activate, and manage charging tokens and token groups.


    ## Authentication

    Every operation needs an OAuth2 client-credentials access token sent as
    `Authorization: Bearer <token>`. The API gateway rejects a missing,
    malformed, invalid or expired token with 401 before the request reaches the
    service. Each operation states the scope it requires: `emsp:tokens:read` for
    reads and `emsp:tokens:write` for changes, and `emsp:tokens:write` also
    allows every read. A token without the required scope gets 403.


    ## Errors

    Errors from the service are returned as RFC 9457 problem details with
    content type `application/problem+json`. `error_code` is a machine-readable
    code for the error, `retryable` says whether the same request may succeed
    later, and `correlation_id` matches the `X-Request-Id` response header.
    Validation failures return 422 with one entry per failed rule in `errors`,
    so a field can appear more than once; a 400 carries `errors` in the same
    shape. `errors[].field` is a JSON-path style location of the input:
    `body.type`, `body.tokens[3].uid`, `query.limit`, `path.tokenId`,
    `header.Idempotency-Key`, or `body` when the body is not valid JSON. Errors
    raised by the API gateway before a request reaches the service, such as a
    rejected access token, throttling or an unknown path, use a plain JSON body
    with a `message` field instead.


    `error_code` names the cause, and a status can carry more than one:


    | `error_code` | Status | Retryable | When |

    | --- | --- | --- | --- |

    | `INVALID_REQUEST_BODY` | 400 | no | The request body is not valid JSON |

    | `INVALID_PATH_ID` | 400 | no | A path id is not a UUID |

    | `INVALID_IDEMPOTENCY_KEY` | 400 | no | The Idempotency-Key header is
    missing or not a UUID |

    | `AUTH_CONTEXT_INVALID` | 401 | no | The `x-spirii-*` principal headers are
    missing or not UUIDs. Not raised behind the API gateway, which rejects a
    missing or invalid access token first with a plain JSON body |

    | `INSUFFICIENT_SCOPE` | 403 | no | The access token does not carry the
    scope this operation requires |

    | `RESOURCE_NOT_FOUND` | 404 | no | The requested resource was not found |

    | `UID_ALREADY_REGISTERED` | 409 | no | The uid is already registered in
    this tenant |

    | `IDEMPOTENCY_KEY_REUSED` | 409 | no | The Idempotency-Key was already used
    with a different request body |

    | `REQUEST_IN_PROGRESS` | 409 | yes | A request with this Idempotency-Key is
    still in progress, or failed less than a minute ago. Retry with the same key
    and body |

    | `RESOURCE_CONFLICT` | 409 | no | The resource is in a conflicting state.
    Not raised by the service today |

    | `PAYLOAD_TOO_LARGE` | 413 | no | The request body exceeds the 1 MB limit |

    | `UNSUPPORTED_MEDIA_TYPE` | 415 | no | The request body uses a charset or
    Content-Encoding the service does not accept |

    | `VALIDATION_FAILED` | 422 | no | The request body or a query parameter
    failed validation, and errors lists each failed rule |

    | `RATE_LIMIT_EXCEEDED` | 429 | yes | Too many requests. Not raised by the
    service today: the API gateway throttles first, with a plain JSON body |

    | `INTERNAL_SERVER_ERROR` | 500 | yes | An unexpected error occurred. Retry
    after retry_after_seconds |


    ## Idempotency

    Operations that take an `Idempotency-Key` header require a client-generated
    UUID. Repeating a request with the same key and body within 24 hours replays
    the original response and sets `Idempotent-Replay: true`; the same key with
    a different body returns 409. Keys are scoped to the method and path, so the
    same key on a different token or group is a separate request. If a request
    fails, its key stays reserved for about a minute: retrying with the same key
    and body in that window returns 409, and after it the same key runs the
    request again. Send a corrected request with a new key.


    ## Pagination

    List operations are cursor-paginated. Pass `pagination.end_cursor` from one
    response as `after` on the next, and stop when `pagination.has_next` is
    false.


    ## Asynchronous operations

    Bulk provisioning and token group lifecycle actions are accepted with 202
    and processed in the background. An endpoint for reading an operation by
    `operation_id` is not available yet.


    ## Resources

    On tokens and token groups, optional fields without a value are sent as
    `null` rather than left out. A token history event's `reason` and a group
    operation's `failures` are left out when they do not apply.


    ## Current limitations

    The service currently answers from fixtures: responses are generated per
    request and nothing is persisted, so reading a resource back does not return
    what was written. Every request resolves to the same single tenant, whatever
    credentials it carries. The 404 and 409 responses documented on each
    operation are the intended contract, but the service does not produce them
    yet: an unknown id returns a resource, and a lifecycle action succeeds from
    any status. The 409 for a `uid` that is already provisioned and for a reused
    `Idempotency-Key` are the exceptions and are returned today. Bulk import
    accepts the documented 1000 tokens in one request, and a request body over 1
    MB returns 413.
  version: v1
  contact: {}
servers:
  - url: https://api.spirii.com
    description: Production
security:
  - OAuth2ClientCredentials: []
tags:
  - name: Tokens
    description: Provision tokens, change their lifecycle and read their history
  - name: Token Groups
    description: >-
      Create token groups, manage their member tokens and network assignments,
      and change the lifecycle of every member at once
paths:
  /emsp/v1/tokens/bulk:
    post:
      tags:
        - Tokens
      summary: Bulk provision tokens
      description: >-
        Accepts up to 1000 tokens for provisioning in the background. The batch
        is validated as a whole before it is accepted: one invalid item rejects
        the batch with 422, and each entry in `errors` names the item by index
        (`body.tokens[<index>].<field>`). uid uniqueness and metadata validation
        run per item during processing rather than on this call. This operation
        does not use the Idempotency-Key header; instead each item carries its
        own `idempotency_key`.


        Requires the `emsp:tokens:write` scope.
      operationId: tokens_bulk_import
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkTokenImportRequestDto'
      responses:
        '202':
          description: The batch was accepted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkImportAcceptedDto'
        '400':
          description: The body is not valid JSON (`INVALID_REQUEST_BODY`)
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetailDto'
        '401':
          description: >-
            The access token is missing, invalid or expired, rejected by the API
            gateway with a plain JSON body
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '403':
          description: >-
            The access token does not carry the scope this operation requires
            (`INSUFFICIENT_SCOPE`)
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetailDto'
        '422':
          description: >-
            The request body failed validation (`VALIDATION_FAILED`). `errors`
            lists each failed rule
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetailDto'
        '429':
          description: >-
            Too many requests, rejected by the API gateway with a plain JSON
            body
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '500':
          description: >-
            Unexpected server error. When `retryable` is true, retry after
            `retry_after_seconds`. Where the operation takes an Idempotency-Key,
            reuse the same key; a retry within about a minute may return 409
            while it is still reserved
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetailDto'
components:
  schemas:
    BulkTokenImportRequestDto:
      type: object
      properties:
        tokens:
          minItems: 1
          maxItems: 1000
          type: array
          items:
            $ref: '#/components/schemas/BulkTokenImportItemDto'
      required:
        - tokens
    BulkImportAcceptedDto:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
          description: Identifies the background operation processing this batch
        submitted_count:
          type: integer
          minimum: 1
          maximum: 1000
          description: Number of tokens accepted, equal to the length of `tokens`
      required:
        - operation_id
        - submitted_count
    ProblemDetailDto:
      type: object
      properties:
        type:
          type: string
          example: /errors/validation-failed
          description: Error type URI
        title:
          type: string
          example: Validation Failed
        status:
          type: integer
          example: 422
        detail:
          type: string
        error_code:
          example: RESOURCE_NOT_FOUND
          description: >-
            Machine-readable error cause. The Errors section of the API
            description says when each is returned
          allOf:
            - $ref: '#/components/schemas/ErrorCode'
        correlation_id:
          type: string
          format: uuid
          description: Matches X-Request-Id header
        retryable:
          type: boolean
        retry_after_seconds:
          type: integer
          minimum: 1
        errors:
          type: array
          items:
            $ref: '#/components/schemas/FieldErrorDto'
      required:
        - type
        - title
        - status
        - detail
        - error_code
        - correlation_id
        - retryable
    BulkTokenImportItemDto:
      type: object
      properties:
        type:
          type: string
          enum:
            - RFID
            - APP_USER
            - AD_HOC_USER
            - EMAID
            - OTHER
        uid:
          type: string
          minLength: 1
          maxLength: 50
          description: Caller-supplied, unique within the tenant
        contract_id:
          type: string
          minLength: 1
          maxLength: 50
          description: Contract or credential identity, e.g. an eMAID
        issuer:
          type: string
          minLength: 1
          maxLength: 100
          description: Issuer name associated with the credential
        whitelist:
          type: string
          enum:
            - ALWAYS
            - ALLOWED
            - ALLOWED_OFFLINE
            - NEVER
          description: OCPI whitelist policy. Required, no default
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2, e.g. DK
        party_id:
          type: string
          minLength: 3
          maxLength: 3
          description: Party ID of the token owner, e.g. SPI
        visual_number:
          type: string
          minLength: 1
          maxLength: 64
        language:
          type: string
          minLength: 2
          maxLength: 10
          description: Preferred language, e.g. en or da
        valid_from:
          type: string
          format: date-time
          description: Defaults to the creation time
        valid_until:
          type: string
          format: date-time
          description: Omit for no expiry
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Name/value pairs validated against the tenant's active TOKEN
            metadata schema
        emsp_group_ids:
          maxItems: 100
          description: >-
            Spirii EMSP token groups to join at provision time. Canonical group
            ids are opaque strings up to 50 characters, so they are not
            validated as UUIDs here
          type: array
          items:
            type: string
        group_id:
          type: string
          minLength: 1
          maxLength: 50
          description: >-
            OCPI token group id used in roaming payloads. Unrelated to Spirii
            EMSP token groups
        auto_activate:
          type: boolean
          default: false
          description: >-
            Provision straight into ACTIVE. Whitelist distribution to CPO
            networks starts in the background, and `whitelist_distributed` turns
            true once it completes
        idempotency_key:
          type: string
          format: uuid
          description: >-
            Client-generated UUID for this token. Resubmitting an item with the
            same key in a later batch does not provision it a second time, so a
            failed batch can be retried as a whole
      required:
        - type
        - uid
        - contract_id
        - issuer
        - whitelist
        - idempotency_key
    ErrorCode:
      type: string
      enum:
        - INVALID_REQUEST_BODY
        - INVALID_PATH_ID
        - INVALID_IDEMPOTENCY_KEY
        - AUTH_CONTEXT_INVALID
        - INSUFFICIENT_SCOPE
        - RESOURCE_NOT_FOUND
        - UID_ALREADY_REGISTERED
        - IDEMPOTENCY_KEY_REUSED
        - REQUEST_IN_PROGRESS
        - RESOURCE_CONFLICT
        - PAYLOAD_TOO_LARGE
        - UNSUPPORTED_MEDIA_TYPE
        - VALIDATION_FAILED
        - RATE_LIMIT_EXCEEDED
        - INTERNAL_SERVER_ERROR
      description: >-
        Machine-readable error cause. The Errors section of the API description
        says when each is returned
    FieldErrorDto:
      type: object
      properties:
        field:
          type: string
          description: >-
            The input that failed, as a JSON-path style location: `body.type`,
            `body.tokens[3].uid`, `query.limit`, `path.tokenId`,
            `header.Idempotency-Key`, or `body` when the body is not valid JSON
          example: body.tokens[3].uid
        code:
          type: string
          enum:
            - INVALID_ENUM
            - INVALID_FORMAT
            - MISSING_REQUIRED
            - CONSTRAINT_VIOLATION
          description: Machine-readable validation error code
        message:
          type: string
          description: Human-readable error description
      required:
        - field
        - code
        - message
  securitySchemes:
    OAuth2ClientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: >-
            https://auth.spirii.dk/realms/public-api/protocol/openid-connect/token
          scopes:
            emsp:tokens:read: >-
              Read tokens, token history, token groups and their network
              assignments
            emsp:tokens:write: >-
              Provision, update and change the lifecycle of tokens; create and
              manage token groups. Also grants everything `emsp:tokens:read`
              does

````