> ## 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.

# Add tokens to group

> Adds tokens to the group and returns the updated group. A token can be a member of any number of groups, so adding it here does not remove it from other groups. The members of a group cannot be listed yet.

Requires the `emsp:tokens:write` scope.



## OpenAPI

````yaml /openapi/emsp-token/openapi.yaml post /emsp/v1/token-groups/{groupId}/tokens
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/token-groups/{groupId}/tokens:
    post:
      tags:
        - Token Groups
      summary: Add tokens to group
      description: >-
        Adds tokens to the group and returns the updated group. A token can be a
        member of any number of groups, so adding it here does not remove it
        from other groups. The members of a group cannot be listed yet.


        Requires the `emsp:tokens:write` scope.
      operationId: token_groups_add_tokens
      parameters:
        - name: groupId
          required: true
          in: path
          description: The token group `group_id`
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenGroupAddTokensRequestDto'
      responses:
        '200':
          description: The updated group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenGroupDto'
        '400':
          description: >-
            A path id is not a UUID (`INVALID_PATH_ID`), or 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'
        '404':
          description: No token group with this id exists in your tenant
          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:
    TokenGroupAddTokensRequestDto:
      type: object
      properties:
        token_ids:
          minItems: 1
          maxItems: 1000
          description: The `id` of each token to add
          type: array
          items:
            type: string
            format: uuid
      required:
        - token_ids
    TokenGroupDto:
      type: object
      properties:
        group_id:
          type: string
          format: uuid
          description: Stable token group id
        tenant_id:
          type: string
          description: Owning tenant, set by the platform
        emsp_identity_id:
          type: string
          minLength: 1
          maxLength: 64
          description: The EMSP identity the group belongs to
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          nullable: true
          maxLength: 1000
        token_count:
          type: integer
          minimum: 0
          description: Number of member tokens
        network_ids:
          description: >-
            Networks assigned to the group. Managed through the network
            assignment operations
          type: array
          items:
            type: string
            format: uuid
        status_summary:
          $ref: '#/components/schemas/StatusSummaryDto'
        audit:
          $ref: '#/components/schemas/AuditMetaDto'
      required:
        - group_id
        - tenant_id
        - emsp_identity_id
        - name
        - token_count
        - status_summary
        - audit
    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
    StatusSummaryDto:
      type: object
      properties:
        provisioned:
          type: integer
          minimum: 0
          description: Member tokens in PROVISIONED
        active:
          type: integer
          minimum: 0
          description: Member tokens in ACTIVE
        suspended:
          type: integer
          minimum: 0
          description: Member tokens in SUSPENDED
        expired:
          type: integer
          minimum: 0
          description: Member tokens in EXPIRED
        revoked:
          type: integer
          minimum: 0
          description: Member tokens in REVOKED
      description: >-
        Number of member tokens in each status. The counts add up to
        `token_count`
      required:
        - provisioned
        - active
        - suspended
        - expired
        - revoked
    AuditMetaDto:
      type: object
      properties:
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        created_by:
          type: string
          description: Actor who created the resource
      required:
        - created_at
        - updated_at
        - created_by
    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

````