# Grant Credits (Admin)

Source: https://recoupable.dev/docs/api-reference/admins/credits-grant

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

## POST /api/admins/credits

Full OpenAPI specification: https://recoupable.dev/docs/spec/accounts.json

## Authentication

This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification.

[Authentication guide](https://recoupable.dev/docs/authentication)

## Operation and referenced schemas

```json
{
  "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"
    }
  ],
  "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.\n\n**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.\n\n**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).\n\n**A grant is never reduced by the monthly refill.** Credit balances are topped up to the plan total by a monthly refill 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. That refill is a floor, not an assignment: it raises a balance up to the plan total and never lowers one. A grant above the plan total therefore survives every refill and holds until it is spent, so headroom granted once stays granted and does not need re-granting on a schedule.",
        "security": [
          {
            "apiKeyAuth": []
          },
          {
            "bearerAuth": []
          }
        ],
        "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"
                }
              }
            }
          }
        }
      }
    }
  },
  "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"
        ],
        "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"
          }
        }
      },
      "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"
      }
    }
  }
}
```
