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

# Exchange an access key for a bearer token

> Exchange a service account's access key pair for a short-lived bearer
token, then send that token as `Authorization: Bearer <token>` on every
other call.

This is the ordinary way to authenticate. The access key pair stays the
one long-lived credential a service account has; what changes is that you
present a token derived from it rather than signing each request.

```
curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
  https://iam.basaltic.sh/v1/oauth/token
```

The same key pair is *also* the AWS SigV4 credential for the
S3-compatible object endpoint, which speaks nothing else. Use the token
for this API and the key pair for S3; there is no need to choose.

**Errors here use the OAuth 2.0 shape, not this API's usual envelope** —
`{"error": "...", "error_description": "..."}` — because every OAuth
client library parses that and nothing else. Two answers matter and their
remedies are opposite. `invalid_client` means the key was rejected: check
or rotate it. `invalid_grant` means the key is fine and the organization
is suspended or still onboarding, where rotating a working key would
waste your time.

An unknown access key and a wrong secret both answer `invalid_client`
with the same message, so the endpoint cannot be used to discover which
keys exist.




## OpenAPI

````yaml /api-reference/specs/iam.yaml post /v1/oauth/token
openapi: 3.0.3
info:
  title: Basaltic IAM API
  version: 1.0.0
  description: |
    Identity for the platform: organizations, accounts, users, groups,
    service accounts, roles and policies, together with the sign-in flows and
    the temporary STS credentials every other Basaltic API authenticates
    against.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://iam.basaltic.sh
    description: Global API endpoint
security:
  - BearerAuth: []
  - SignatureAuth: []
paths:
  /v1/oauth/token:
    post:
      tags:
        - IAM
      summary: Exchange an access key for a bearer token
      description: >
        Exchange a service account's access key pair for a short-lived bearer

        token, then send that token as `Authorization: Bearer <token>` on every

        other call.


        This is the ordinary way to authenticate. The access key pair stays the

        one long-lived credential a service account has; what changes is that
        you

        present a token derived from it rather than signing each request.


        ```

        curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
          https://iam.basaltic.sh/v1/oauth/token
        ```


        The same key pair is *also* the AWS SigV4 credential for the

        S3-compatible object endpoint, which speaks nothing else. Use the token

        for this API and the key pair for S3; there is no need to choose.


        **Errors here use the OAuth 2.0 shape, not this API's usual envelope** —

        `{"error": "...", "error_description": "..."}` — because every OAuth

        client library parses that and nothing else. Two answers matter and
        their

        remedies are opposite. `invalid_client` means the key was rejected:
        check

        or rotate it. `invalid_grant` means the key is fine and the organization

        is suspended or still onboarding, where rotating a working key would

        waste your time.


        An unknown access key and a wrong secret both answer `invalid_client`

        with the same message, so the endpoint cannot be used to discover which

        keys exist.
      operationId: getOAuthToken
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuthTokenRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthTokenRequest'
      responses:
        '200':
          description: A bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthTokenResponse'
        '400':
          description: |
            Malformed request, or a grant type this deployment does not serve.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              example:
                error: unsupported_grant_type
                error_description: unsupported grant type
        '401':
          description: >
            Client authentication failed — an unknown access key, a wrong
            secret,

            or a key that is disabled or expired. One answer covers all of them.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              example:
                error: invalid_client
                error_description: client authentication failed
        '403':
          description: |
            The credential is valid but the organization is not active. Nothing
            is wrong with the key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              example:
                error: invalid_grant
                error_description: Organization is suspended
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: >
            The exchange could not be completed. The credential was not refused
            —

            retry rather than rotating it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              example:
                error: temporarily_unavailable
                error_description: try again shortly
      security: []
components:
  schemas:
    OAuthTokenRequest:
      type: object
      description: >
        An OAuth 2.0 token request. Form-encoded is what RFC 6749 specifies and

        what client libraries send; JSON is accepted too.


        Client credentials may be sent as HTTP Basic (`Authorization: Basic

        base64(key_id:secret)`, which is what most libraries do by default) or
        as

        `client_id` and `client_secret` fields. Basic wins if both are present.
      required:
        - grant_type
      properties:
        grant_type:
          type: string
          enum:
            - client_credentials
            - authorization_code
            - refresh_token
          description: >
            `client_credentials` is the one to use for a service account: it

            exchanges an access key pair for a token, and needs nothing else.


            `authorization_code` and `refresh_token` belong to the interactive

            login a person runs (`basaltic login`), where the token names a USER

            rather than a service account. They are driven by the CLI, not
            written

            by hand. Check the authorization-server metadata document before

            branching on them — they are advertised only where an authorization

            endpoint is configured.
          example: client_credentials
        client_id:
          type: string
          description: The access key id. Omit when using HTTP Basic.
          example: BYCLD1a2b3c4d5e6f7
        client_secret:
          type: string
          format: password
          description: The secret access key. Omit when using HTTP Basic.
        duration_seconds:
          type: integer
          minimum: 900
          maximum: 43200
          description: >
            Requested token lifetime. A Basaltic extension, not an OAuth

            parameter — omit it and you get the default. Values outside the
            range

            are clamped into it rather than refused, so asking for a day yields

            the longest token allowed.
          example: 3600
        code:
          type: string
          description: >
            The authorization code from the consent redirect. Single use, and
            valid

            for five minutes. `authorization_code` grant only.
        code_verifier:
          type: string
          description: >
            The PKCE verifier whose SHA-256 was sent as `code_challenge` when
            the

            flow started (RFC 7636). Required with `authorization_code`: it is
            what

            proves this is the client that began the flow, since a CLI holds no

            client secret.
        redirect_uri:
          type: string
          description: >
            The same `redirect_uri` the code was issued for. Re-checked here, so
            a

            code cannot be redeemed against a different destination.
          example: http://127.0.0.1:53682/callback
        refresh_token:
          type: string
          description: >
            `refresh_token` grant only. Renews a user session without another
            trip

            through the browser. Rotated on every use — store the new one.
    OAuthTokenResponse:
      type: object
      description: RFC 6749 token response.
      required:
        - access_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
          description: |
            Send as `Authorization: Bearer <token>`. Opaque to clients: do not
            parse it, and do not key anything on the token string.
          example: eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...
        token_type:
          type: string
          enum:
            - Bearer
          example: Bearer
        expires_in:
          type: integer
          description: Seconds until the token expires.
          example: 3600
        refresh_token:
          type: string
          description: >
            Returned only by the user grants (`authorization_code` and

            `refresh_token`). Present it to the `refresh_token` grant to renew

            without another browser round trip; it is ROTATED on each use, so

            replace the stored copy every time.


            A service account gets none. It already holds a long-lived access
            key

            and can simply run `client_credentials` again, so a refresh token
            would

            be a second credential to store for no gain.
    OAuthError:
      type: object
      description: >
        RFC 6749 error response. Deliberately NOT this API's usual error
        envelope:

        OAuth client libraries parse this shape and nothing else, and the whole

        point of the token endpoint is that a stock client can reach it.
      required:
        - error
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - unsupported_grant_type
            - temporarily_unavailable
            - server_error
          description: |
            `invalid_client` — the key was rejected; check or rotate it.
            `invalid_grant` — the key is fine, the organization is not active.
            Those two have opposite remedies and are worth distinguishing before
            anyone rotates a working credential.
          example: invalid_client
        error_description:
          type: string
          description: Human-readable detail. Do not match on it.
          example: client authentication failed
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
  responses:
    TooManyRequests:
      description: >
        Rate limit exceeded. The budget is a fixed window counted per endpoint
        and

        per caller — the authenticated principal when the request carries

        credentials, the client IP otherwise — so one throttled endpoint never

        spends another's budget, and one tenant never spends another's.


        Wait `Retry-After` seconds, then retry. The `X-RateLimit-*` headers ride
        on

        the successful responses of a rate-limited endpoint too, so a client can

        pace itself instead of discovering the ceiling by hitting it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests, please try again later
              request_id: 550e8400-e29b-41d4-a716-446655440000
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Never zero.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 42
        X-RateLimit-Limit:
          description: Requests allowed per window on this endpoint.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 5
        X-RateLimit-Remaining:
          description: Requests left in the current window. Always 0 on a 429.
          required: true
          schema:
            type: integer
            minimum: 0
          example: 0
        X-RateLimit-Reset:
          description: >-
            Seconds until the window resets — a duration, not a timestamp, so it
            needs no clock agreement between client and server.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 42
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        An OAuth 2.0 bearer token, sent as `Authorization: Bearer <token>`.
        This is the recommended way to authenticate.

        Get one by exchanging a service account's access key pair at
        `POST /v1/oauth/token` with `grant_type=client_credentials`. It is the
        standard client-credentials grant, so any OAuth-aware library will
        obtain and refresh it for you.

        ```
        curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
          https://iam.basaltic.sh/v1/oauth/token
        ```

        Tokens last an hour by default. The same access key pair is separately
        your AWS SigV4 credential for the S3-compatible object endpoint, which
        speaks nothing else.
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````