API REFERENCE
Grant Credits (Admin)
On this page
/api/admins/creditsSets 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.
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, 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.
Authentication
x-api-key in header
bearerAuth bearer
Request
curl --request POST \
--url 'https://api.recoupable.dev/api/admins/credits' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"account_id": "YOUR_ID",
"remaining_credits": 0,
"reason": "string"
}'Request body required
The account to set, the balance to leave it at, and why
application/json
account_idstringrequiredUUID of the account whose balance is being set. Must be an existing account — an unknown UUID is a 404, not a silent no-op.
format: uuid
remaining_creditsintegerrequiredThe 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.
minimum: 0
reasonstringrequiredWhy 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.
minLength: 1
Responses
200Balance set and the grant recorded+
application/json
statusstring · enumrequiredStatus of the request
Values: "success"
grant_idstringrequiredUUID of the recorded grant row. Matches the id of the corresponding entry in the grants array of GET /api/admins/credits/events.
format: uuid
account_idstringrequiredUUID of the account whose balance was set (echoes the request)
format: uuid
remaining_creditsintegerrequiredThe balance the account now holds — the value supplied in the request
previous_creditsintegerrequirednullableThe balance immediately before the grant. Null when the account had no credits row at all and one was created by this request.
reasonstringrequiredThe reason recorded with the grant (echoes the request)
granted_bystringrequiredUUID of the admin account that made the grant, resolved from the credentials on the request. Never taken from the body.
format: uuid
granted_atstringrequiredWhen the grant was recorded
format: date-time
400Malformed JSON body, or a missing/invalid account_id, remaining_credits, or reason+
application/json
statusstring · enumrequiredStatus of the request
Values: "error"
missing_fieldsarray<one of>JSON path segments of the first field that failed validation, e.g. ["reason"]. Absent when the body was not valid JSON at all.
Item properties for missing_fields
oneOf · string 1
string
oneOf · integer 2
integer
errorstringrequiredError message describing what went wrong
401Unauthorized - missing or invalid credentials+
application/json
statusstring · enumrequiredStatus of the request
Values: "error"
errorstringrequiredError message describing what went wrong
403Forbidden - authenticated account is not a Recoup admin+
application/json
statusstring · enumrequiredStatus of the request
Values: "error"
messagestringrequiredError message describing what went wrong
404No account exists with the supplied account_id+
application/json
statusstring · enumrequiredStatus of the request
Values: "error"
messagestringrequiredError message describing what went wrong
500Internal server error while setting the balance or recording the grant+
application/json
statusstring · enumrequiredStatus of the request
Values: "error"
messagestringrequiredError message describing what went wrong
Full specification
Download the OpenAPI file for complete schemas, constraints, and examples.
Download accounts.jsonView operation source
{
"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"
}
}
}
}
}
}