Authentication
The Reeds API uses OAuth 2.0 client credentials. You exchange a client id and secret for an access token, then send that token as a Bearer token on every request.
Get your credentials
Each account has a client id and a client secret for each mode. Find them in Dashboard → Developers.
| Credential | Test mode | Live mode |
|---|---|---|
| Client id | rci_test_... | rci_live_... |
| Client secret | rcs_test_... | rcs_live_... |
- A secret is shown once. Copy it when you create it. Reeds stores only a hash.
- Two secrets can be active at the same time, so you can rotate a secret without downtime: create the new one, deploy it, then revoke the old one.
- Optional, and recommended for live credentials: limit them to your servers' IP addresses with the IP allowlist. It is off by default, and test credentials are never restricted.
Never put a client secret in a browser, a mobile app or a public repository. Call Reeds from your backend. The prefixes make leaked secrets easy to spot, so revoke any secret that is exposed.
The API Reference's Test Request button and the dashboard's API console accept test credentials only. Live credentials sent from a browser get 400 unauthorized_client from /oauth/token and 403 TEST_MODE_ONLY from the API.
Get an access token
Send POST https://api.getreeds.co/oauth/token with grant_type=client_credentials, your client id and your client secret. You can send them form-encoded, as JSON, or as HTTP Basic credentials. The endpoint follows the OAuth 2.0 standard, so off-the-shelf OAuth libraries work.
- cURL
- Node.js
- PHP
- Python
curl https://api.getreeds.co/oauth/token \
-d grant_type=client_credentials \
-d client_id="$REEDS_CLIENT_ID" \
-d client_secret="$REEDS_CLIENT_SECRET"
const res = await fetch('https://api.getreeds.co/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.REEDS_CLIENT_ID,
client_secret: process.env.REEDS_CLIENT_SECRET,
}),
});
const token = await res.json();
console.log(token.access_token);
<?php
$ch = curl_init('https://api.getreeds.co/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => getenv('REEDS_CLIENT_ID'),
'client_secret' => getenv('REEDS_CLIENT_SECRET'),
]),
]);
$token = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $token['access_token'];
import os
import requests
res = requests.post(
"https://api.getreeds.co/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["REEDS_CLIENT_ID"],
"client_secret": os.environ["REEDS_CLIENT_SECRET"],
},
)
token = res.json()
print(token["access_token"])
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "shipping",
"livemode": false
}
| Field | Description |
|---|---|
access_token | The token to send on every API call |
token_type | Always Bearer |
expires_in | Seconds until the token expires. Tokens last one hour (3600). |
scope | The scopes the token carries. v1 has one scope, shipping. |
livemode | true for live credentials, false for test credentials |
Use the token
Send it in the Authorization header on every call:
Authorization: Bearer <access_token>
Cache and refresh the token
Don't request a token for every API call. Cache it, and fetch a new one shortly before it expires. The token endpoint allows 60 requests a minute per client, so a token-per-request design will hit the limit.
- Node.js
- Python
let cached = { token: null, expiresAt: 0 };
export async function getAccessToken() {
// Refresh 60 seconds early so a token never expires mid-request.
if (cached.token && Date.now() < cached.expiresAt - 60_000) {
return cached.token;
}
const res = await fetch('https://api.getreeds.co/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.REEDS_CLIENT_ID,
client_secret: process.env.REEDS_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
const body = await res.json();
cached = {
token: body.access_token,
expiresAt: Date.now() + body.expires_in * 1000,
};
return cached.token;
}
import os
import time
import requests
_cache = {"token": None, "expires_at": 0.0}
def get_access_token():
# Refresh 60 seconds early so a token never expires mid-request.
if _cache["token"] and time.time() < _cache["expires_at"] - 60:
return _cache["token"]
res = requests.post(
"https://api.getreeds.co/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["REEDS_CLIENT_ID"],
"client_secret": os.environ["REEDS_CLIENT_SECRET"],
},
timeout=10,
)
res.raise_for_status()
body = res.json()
_cache["token"] = body["access_token"]
_cache["expires_at"] = time.time() + body["expires_in"]
return _cache["token"]
Authentication errors
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 401 | TOKEN_EXPIRED | The token is older than an hour | Get a new token and retry the request |
| 401 | UNAUTHENTICATED | The token is missing, invalid or revoked | Check the header. If the token was revoked, fix your credentials. Don't loop on this. |
| 400 | invalid_request or unsupported_grant_type | A field is missing, the credentials were sent both in the body and as HTTP Basic, or grant_type isn't client_credentials | Fix the request |
| 400 | unauthorized_client | A live client id came from a browser, or from an IP address that isn't on your live allowlist | Call from your server; check the allowlist |
| 401 | invalid_client | The client id or secret sent to /oauth/token is wrong or revoked, or live credentials were used before your account is approved | Check you are using the matching id and secret for the same mode |
| 403 | TEST_MODE_ONLY | A live token was sent from a browser, or to a sandbox endpoint | Use test credentials there; call with live credentials only from your server |
| 403 | INSUFFICIENT_SCOPE | The token doesn't carry the scope the endpoint needs | Use credentials for an account that has the scope |
| 503 | temporarily_unavailable | Reeds is busy and didn't look at the request | Wait the Retry-After seconds, then ask again |
The lowercase codes are standard OAuth errors from /oauth/token, such as { "error": "invalid_client" }, not the usual Reeds error format. See Errors for the rest.
Recommended: restrict live credentials by IP
You can limit your live credentials to the IP addresses of your own servers. It is optional and off by default, so nothing changes until you set it up. We recommend it: if a secret leaks, it can't be used from anywhere else, because Reeds checks the IP address on the token request and on every API call.
To set it up:
- In the dashboard, switch to Live mode and open Developers. The IP allowlist card is under your credentials.
- Look at Recent IP addresses. These are the addresses that used your live credentials in the last 30 days. Press Add next to each one that is yours, or type an address or a range yourself.
- Adding the first address turns the allowlist on. The dashboard asks you to confirm first, and lists any recent address that would be refused.
What to know:
- An entry is one IPv4 or IPv6 address, or a range such as
41.58.0.0/16, with an optional label. You can add up to 50. - Use the public IP address your servers call Reeds from. Private and reserved addresses (such as
192.168.x.x,10.x.x.xor100.64.x.x) are refused, and so are ranges wider than/16for IPv4 or/48for IPv6. - Once it is on, a live request from any other address is refused. A token request gets
400 unauthorized_client, and an API call with a live token gets403IP_NOT_ALLOWED. Neither does anything or costs anything. - Every change, adding or removing an address, asks for a 6-digit code from your authenticator app (recovery codes aren't accepted), so a stolen session alone can't open the list. Turn on two-factor authentication in Settings first. The account's Owners are emailed about every change.
- Changes take effect within a minute. Removing the last address turns the allowlist off again.
- Test credentials are never restricted, so you can build and test from anywhere.
- If your servers' addresses change (for example when you move host), add the new ones before you switch over.
- The allowlist needs fixed outgoing addresses. Serverless functions and some hosting platforms send requests from addresses that change, so their calls would be refused at random. Give them a fixed outgoing IP (most clouds call it a NAT gateway or static egress IP) before you turn the allowlist on, or leave it off.
Revoking a secret
Revoking a secret in the dashboard also ends every token issued with it, within a minute. Rotate a secret by creating its replacement first, so there is no gap.