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

# Grant Credits (Admin)

> Sets an account's credit balance, and records who set it and why. Requires the authenticated account to be a Recoup admin.

**Absolute, not a delta.** `remaining_credits` is the balance the account is left holding, whatever it held before — the outcome never depends on the balance at the moment the request lands. The prior balance comes back as `previous_credits` for the record. There is deliberately no add/subtract variant.

**Every grant is audited.** `reason` is required and must be non-empty. The grant is stored against the acting admin's account ID and is readable afterwards in the `grants` array of [`GET /api/admins/credits/events`](/api-reference/admins/credits-events).

**A grant expires after one month — plan for it.** Credit balances are refilled to the plan total by a monthly reset that runs lazily on the next read of [`GET /api/accounts/{id}/credits`](/api-reference/accounts/credits-get), once the underlying row is more than a month old. Setting a balance here restarts that clock, so the balance holds until `expires_at` (one month after `granted_at`) and is then silently overwritten on the first balance read after it — with 9,999 for an account carrying an active Stripe subscription, and 333 for every other account, including one with no Stripe customer at all. Nothing warns the account holder, and nothing warns you. If the headroom needs to outlive a month, grant it again; if the account should stay funded indefinitely, put it on a pro subscription rather than granting.



## OpenAPI

````yaml api-reference/openapi/accounts.json POST /api/admins/credits
openapi: 3.1.0
info:
  title: Recoup API - Accounts
  description: >-
    API documentation for the Recoup platform - an AI agent platform for the
    music industry
  license:
    name: MIT
  version: 1.0.0
servers:
  - url: https://api.recoupable.dev
security: []
paths:
  /api/admins/credits:
    post:
      description: >-
        Sets an account's credit balance, and records who set it and why.
        Requires the authenticated account to be a Recoup admin.


        **Absolute, not a delta.** `remaining_credits` is the balance the
        account is left holding, whatever it held before — the outcome never
        depends on the balance at the moment the request lands. The prior
        balance comes back as `previous_credits` for the record. There is
        deliberately no add/subtract variant.


        **Every grant is audited.** `reason` is required and must be non-empty.
        The grant is stored against the acting admin's account ID and is
        readable afterwards in the `grants` array of [`GET
        /api/admins/credits/events`](/api-reference/admins/credits-events).


        **A grant expires after one month — plan for it.** Credit balances are
        refilled to the plan total by a monthly reset that runs lazily on the
        next read of [`GET
        /api/accounts/{id}/credits`](/api-reference/accounts/credits-get), once
        the underlying row is more than a month old. Setting a balance here
        restarts that clock, so the balance holds until `expires_at` (one month
        after `granted_at`) and is then silently overwritten on the first
        balance read after it — with 9,999 for an account carrying an active
        Stripe subscription, and 333 for every other account, including one with
        no Stripe customer at all. Nothing warns the account holder, and nothing
        warns you. If the headroom needs to outlive a month, grant it again; if
        the account should stay funded indefinitely, put it on a pro
        subscription rather than granting.
      requestBody:
        description: The account to set, the balance to leave it at, and why
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminGrantCreditsRequest'
      responses:
        '200':
          description: Balance set and the grant recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminGrantCreditsResponse'
        '400':
          description: >-
            Malformed JSON body, or a missing/invalid `account_id`,
            `remaining_credits`, or `reason`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminGrantCreditsValidationError'
        '401':
          description: Unauthorized - missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Forbidden - authenticated account is not a Recoup admin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountErrorResponse'
        '404':
          description: No account exists with the supplied `account_id`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountErrorResponse'
        '500':
          description: >-
            Internal server error while setting the balance or recording the
            grant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountErrorResponse'
      security:
        - apiKeyAuth: []
        - bearerAuth: []
components:
  schemas:
    AdminGrantCreditsRequest:
      type: object
      required:
        - account_id
        - remaining_credits
        - reason
      properties:
        account_id:
          type: string
          format: uuid
          description: >-
            UUID of the account whose balance is being set. Must be an existing
            account — an unknown UUID is a 404, not a silent no-op.
        remaining_credits:
          type: integer
          minimum: 0
          description: >-
            The balance to leave the account holding, in credits (1 credit = 1
            US cent). Absolute, not a delta. Zero is allowed — it is how an
            account is deliberately zeroed out. Negative values are rejected
            with a 400, even though ordinary usage can overdraw a balance below
            zero on its own.
        reason:
          type: string
          minLength: 1
          description: >-
            Why the grant was made, in plain language — e.g. `Trial headroom for
            the Aug 12 label demo`. Required and non-empty (a whitespace-only
            string is rejected): this is the field that makes a grant
            distinguishable from a Stripe top-up or a monthly reset when someone
            asks months later. Stored verbatim and returned by the events
            endpoint.
    AdminGrantCreditsResponse:
      type: object
      required:
        - status
        - grant_id
        - account_id
        - remaining_credits
        - previous_credits
        - reason
        - granted_by
        - granted_at
        - expires_at
      properties:
        status:
          type: string
          enum:
            - success
          description: Status of the request
        grant_id:
          type: string
          format: uuid
          description: >-
            UUID of the recorded grant row. Matches the `id` of the
            corresponding entry in the `grants` array of `GET
            /api/admins/credits/events`.
        account_id:
          type: string
          format: uuid
          description: UUID of the account whose balance was set (echoes the request)
        remaining_credits:
          type: integer
          description: >-
            The balance the account now holds — the value supplied in the
            request
        previous_credits:
          type: integer
          nullable: true
          description: >-
            The balance immediately before the grant. Null when the account had
            no credits row at all and one was created by this request.
        reason:
          type: string
          description: The reason recorded with the grant (echoes the request)
        granted_by:
          type: string
          format: uuid
          description: >-
            UUID of the admin account that made the grant, resolved from the
            credentials on the request. Never taken from the body.
        granted_at:
          type: string
          format: date-time
          description: When the grant was recorded
        expires_at:
          type: string
          format: date-time
          description: >-
            When the monthly reset becomes eligible to overwrite this balance —
            one month after `granted_at`, clamped to the last day of the target
            month when that month is shorter (a grant on Jan 31 expires Feb 28,
            not Mar 3). The overwrite is lazy, not scheduled: it happens on the
            first read of the account's balance at or after this time, so the
            granted balance can outlive `expires_at` indefinitely if nothing
            reads it. Treat it as the point after which the balance is no longer
            guaranteed — it is deliberately never later than the moment the
            reset actually becomes eligible, so it can under-promise by up to a
            day but never over-promise.
    AdminGrantCreditsValidationError:
      type: object
      required:
        - status
        - error
      properties:
        status:
          type: string
          enum:
            - error
          description: Status of the request
        missing_fields:
          type: array
          description: >-
            JSON path segments of the first field that failed validation, e.g.
            `["reason"]`. Absent when the body was not valid JSON at all.
          items:
            oneOf:
              - type: string
              - type: integer
        error:
          type: string
          description: Error message describing what went wrong
    ErrorEnvelope:
      type: object
      required:
        - status
        - error
      properties:
        status:
          type: string
          enum:
            - error
          description: Status of the request
        error:
          type: string
          description: Error message describing what went wrong
    AccountErrorResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          type: string
          enum:
            - error
          description: Status of the request
        message:
          type: string
          description: Error message describing what went wrong
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Your Recoup API key. [Learn more](/quickstart#api-keys).
    bearerAuth:
      type: http
      scheme: bearer

````