curl --request POST \
--url https://api.recoupable.dev/api/admins/credits \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 1,
"reason": "<string>"
}
'import requests
url = "https://api.recoupable.dev/api/admins/credits"
payload = {
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 1,
"reason": "<string>"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
account_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
remaining_credits: 1,
reason: '<string>'
})
};
fetch('https://api.recoupable.dev/api/admins/credits', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.recoupable.dev/api/admins/credits",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'account_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'remaining_credits' => 1,
'reason' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.recoupable.dev/api/admins/credits"
payload := strings.NewReader("{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.recoupable.dev/api/admins/credits")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.recoupable.dev/api/admins/credits")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"grant_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 123,
"previous_credits": 123,
"reason": "<string>",
"granted_by": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"granted_at": "2023-11-07T05:31:56Z"
}{
"status": "error",
"error": "<string>",
"missing_fields": [
"<string>"
]
}{
"status": "error",
"error": "<string>"
}{
"status": "error",
"message": "<string>"
}{
"status": "error",
"message": "<string>"
}{
"status": "error",
"message": "<string>"
}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.
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.
curl --request POST \
--url https://api.recoupable.dev/api/admins/credits \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 1,
"reason": "<string>"
}
'import requests
url = "https://api.recoupable.dev/api/admins/credits"
payload = {
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 1,
"reason": "<string>"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
account_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
remaining_credits: 1,
reason: '<string>'
})
};
fetch('https://api.recoupable.dev/api/admins/credits', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.recoupable.dev/api/admins/credits",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'account_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'remaining_credits' => 1,
'reason' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.recoupable.dev/api/admins/credits"
payload := strings.NewReader("{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.recoupable.dev/api/admins/credits")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.recoupable.dev/api/admins/credits")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"account_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"remaining_credits\": 1,\n \"reason\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"grant_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"remaining_credits": 123,
"previous_credits": 123,
"reason": "<string>",
"granted_by": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"granted_at": "2023-11-07T05:31:56Z"
}{
"status": "error",
"error": "<string>",
"missing_fields": [
"<string>"
]
}{
"status": "error",
"error": "<string>"
}{
"status": "error",
"message": "<string>"
}{
"status": "error",
"message": "<string>"
}{
"status": "error",
"message": "<string>"
}Authorizations
Your Recoup API key. Learn more.
Body
The account to set, the balance to leave it at, and why
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.
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.
x >= 0Why 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.
1Response
Balance set and the grant recorded
Status of the request
success UUID of the recorded grant row. Matches the id of the corresponding entry in the grants array of GET /api/admins/credits/events.
UUID of the account whose balance was set (echoes the request)
The balance the account now holds — the value supplied in the request
The balance immediately before the grant. Null when the account had no credits row at all and one was created by this request.
The reason recorded with the grant (echoes the request)
UUID of the admin account that made the grant, resolved from the credentials on the request. Never taken from the body.
When the grant was recorded
Was this page helpful?
