Products and scopes
Why an endpoint documented on this site answers 403 for your credential: every key carries an explicit list of what it may call, and there is no wildcard.
The reference documents every operation this API has. Your credential reaches
the subset your agreement covers. When those two differ you get a 403, and
this page is how to read it.
Your credential is issued for ONE product
OnLink's partner API serves two products, and every partner is set up for exactly one:
- USDT rails — quoting, buying and selling USDT against KES, registering withdrawal addresses, sending USDT to a registered destination.
- Collections — collecting and converting funds without the USDT leg.
Which product you were onboarded for decides which operations you can ever be
issued a scope for. A usdt:write scope is meaningless to a Collections
partner and will never appear on one of your keys, and the reverse holds for a
scope that belongs only to Collections. Ask your OnLink contact which product
your account is set up for if you are unsure.
A credential carries an explicit list
Every API key is issued with a list of scopes in resource:action form. A call
to an operation whose scope is not on your list is refused — by the credential,
before the operation runs.
Four properties of that list are worth knowing before you debug one:
- There is no wildcard. No
*, noadmin, no super-scope. The list is exhaustive. - An empty list authorises nothing. It is not a shorthand for "unrestricted".
- A scope outside your product can never be issued to you. Not by mistake, not by request — the platform refuses it at issuance.
- Widening a credential means issuing a new one. A scope cannot be added to a key in place, which is also why the grant is worth getting right at issuance rather than discovering in integration.
The scopes
Some scopes are available to every partner regardless of product; others belong to one product only.
Available to every partner:
| Scope | What it unlocks |
|---|---|
health:read | GET /v1/health — the smallest signed call, for testing signing. |
balances:read | The account you hold with us, and what is available in it. |
funding:read | Your deposit instructions and collection account. |
funding:write | Requesting an M-PESA prompt on a number you name. |
payout_accounts:read | The Kenyan bank accounts a payout can settle to. |
transfers:write | Sending money via a bank transfer. |
transfers:read | Reading a bank transfer back. |
confirmations:write | Confirming or resending the code for a bank transfer. |
admins:write | Registering and removing an approver on your account. |
admins:read | Listing them. |
USDT rails only:
| Scope | What it unlocks |
|---|---|
quotes:write | Taking a quote. |
orders:write | Creating an order, and attaching a transaction hash to one. |
orders:read | Reading an order back. |
wallets:write | Registering, confirming and removing a withdrawal address. |
wallets:read | Listing your registered addresses. |
usdt:write | Sending USDT to a registered wallet. |
usdt:read | Reading a USDT send back. |
Collections only:
The Collections product's own operations are documented as they come online. Your OnLink contact will confirm which scopes apply to your integration.
A read scope never implies its write counterpart, and a write scope never implies the read. Ask for both where you need both.
What a refusal looks like
Response — 403 Forbidden:
{
"statusCode": 403,
"message": "This credential is not authorised for this endpoint. Required scope: orders:write.",
"error": "insufficient_scope",
"requestId": "req-1756512000000-a1b2c3d",
"errorCode": "orders:write"
}error is the constant insufficient_scope. The scope the route needed is
in errorCode, and message repeats it in prose — so a refusal tells you what
to ask for rather than sending you to us to find out.
What the response does not contain is the list your credential holds. That is a description of your own privileges, and it is more than a single refusal needs to disclose.
A 403 is not a signing problem
It is a 403 and not a 401 deliberately: your signature, your key and your
clock were all correct, and the call was authenticated. Rotating a working
secret will not fix it. A 401 is the signing path — see
Authentication.
Why your grant is shaped the way it is
A grant is scoped to what your integration does. A platform that only converts
in one direction does not need the other direction's write scope, and a
credential that never registers a destination does not need wallets:write.
That is not friction for its own sake: the narrower the grant, the less a
leaked secret can do, and the destinations it could reach are registered in
advance anyway. See API credentials.
If you need a scope you were not issued, tell us what you are building and we will issue a credential that carries it. Email info@onlink.africa.