Every request to the API is made with an access token, and every token comes from an API credential. The API uses the OAuth 2.0 client-credentials grant: your system sends the credential’s Client ID and Client Secret to the token endpoint and gets back a Bearer token.
Get a credential
An Administrator of your organisation creates the credential under Settings > Profile and terms, on the API Access tab, and chooses its scopes. The Client Secret is shown once. See API access in the Help Centre.
Ask for a token
Send the pair to POST /oauth/token on your organisation’s own address, form-encoded, with grant_type=client_credentials. You may send the Client ID and Client Secret in the form body, or with HTTP Basic authentication instead.
curl -X POST "https://your-org.backed.giving/oauth/token" \
-d grant_type=client_credentials \
-d client_id=your-client-id \
-d client_secret=your-client-secret
# or, with HTTP Basic authentication:
curl -X POST "https://your-org.backed.giving/oauth/token" \
-u your-client-id:your-client-secret \
-d grant_type=client_credentials{
"access_token": "q3Vx9LmT2sKp4Rz7Wn1YeB8cD5fG6hJ0kN_aQ-uIoPr",
"token_type": "Bearer",
"expires_in": 432000,
"created_at": 1762650000
}expires_inis 432000 seconds: a token lasts 5 days.- There is no refresh token. Each successful request issues a new token; when a request answers
401 invalid_token, ask for a new one. - The answer carries
Cache-Control: no-store. Keep the token as you keep the secret. - The token endpoint takes 30 requests a minute per
client_id, counted before the secret is checked. Ask for a token when you need one, not before every call.
Send the token
Send the token on every request in the Authorization header:
curl "https://your-org.backed.giving/api/v2/funding_rounds" \
-H "Authorization: Bearer $BACKED_TOKEN"const host = 'https://your-org.backed.giving';
async function token() {
const response = await fetch(`${host}/oauth/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.BACKED_CLIENT_ID,
client_secret: process.env.BACKED_CLIENT_SECRET,
}),
});
if (!response.ok) throw new Error(`token: ${response.status}`);
return (await response.json()).access_token;
}
const rounds = await fetch(`${host}/api/v2/funding_rounds`, {
headers: { Authorization: `Bearer ${await token()}` },
}).then((response) => response.json());A missing, invalid, expired or revoked token answers 401 with invalid_token and WWW-Authenticate: Bearer error="invalid_token". Every request must use HTTPS: one sent over plain HTTP answers 403 https_required and is not processed. See Errors.
Scopes
A credential holds the scopes an Administrator ticked when they created or edited it. Every credential holds read. A write needs its own write scope, and a credential without it is answered 403 insufficient_scope.
| Scope | What it allows |
|---|---|
read | Read every /api/v2 resource. Every credential holds it. |
read:reporting_feed | Read the OData v4 reporting feed at /odata/v4. |
write:registrations | Create and edit registration entities and invite their users. |
write:applications | Move funding applications, add notes and replace tags. |
write:assessments | Assign Assessors to applications in an assessment round. |
write:post_award | Create and approve milestones, mark payment batches paid, record contracts. |
write:files | Upload files to deliverables, application notes and registration entities. |
Who a write acts as
A credential with a write scope acts as one Administrator, chosen under Acts as. Every write runs with that person’s permissions, Data Access Rules and approval limits, so it can do nothing they could not, and the audit log records the credential and the Administrator together. A credential with no enabled Administrator to act as is answered 403 run_as_disabled on every write. The write routes are described in Write API: registrations, applications, notes, tags and assignments and Write API: milestones, payments, contracts and files.
Rate limits
A credential may make 120 requests a minute, the API and the OData feed together. Past that, the API answers 429 rate_limited with a Retry-After header: wait that many seconds, then carry on.
