An API key is two values: a client ID (ci_test_... or ci_live_...) and a client secret (sk_test_... or sk_live_...). Make one in the Merchant Dashboard, under Developers → API keys. The secret is shown once, when the key is made; the client ID stays on the list.
Authenticating is two steps. Exchange the client ID and secret for an Access Token, which lasts 15 minutes, then send only that token on every /v1 call. This is the OAuth 2.0 client credentials grant, so any standard OAuth library can do both for you.
1. Get an Access Token
Section titled “1. Get an Access Token”curl https://api.example.com/oauth/token \ -u "ci_test_...:sk_test_..." \ -d grant_type=client_credentialscurl https://api.example.com/oauth/token \ -d grant_type=client_credentials \ -d client_id=ci_test_... \ -d client_secret=sk_test_...{"access_token": "at_test_...", "token_type": "Bearer", "expires_in": 900}-u sends the client ID and secret in a Basic Authorization header. You may send them as client_id and client_secret form fields instead, but not both ways at once.
2. Call the API with it
Section titled “2. Call the API with it”curl https://api.example.com/v1/payment_methods \ -H "Authorization: Bearer at_test_..."Reuse the token on every call until it expires: expires_in says after how many seconds. Get a new one a little before then, rather than one per request; the token endpoint is rate limited.
When a token expires
Section titled “When a token expires”A call with an expired token answers 401, with a WWW-Authenticate: Bearer error="invalid_token" header and this body:
{ "error": { "type": "authentication_failure", "code": "invalid_access_token", "message": "Invalid or expired Access Token. Get a new one at POST /oauth/token.", "param": null, "request_id": "..." }}Get a new token (step 1) and send the call again. A token also stops working at once when its key is revoked. After a rotation, a token from the old key stops when the old key's overlap ends, if that comes before its own 15 minutes are up.
When the token request is refused
Section titled “When the token request is refused”The token endpoint answers in OAuth 2.0's own error format, not the /v1 envelope:
error | Status | What to do |
|---|---|---|
invalid_client | 401 | The client ID or secret is wrong, the key was revoked, or it was rotated and its overlap has ended. Check both values and use the key's current secret; if the secret is lost, rotate the key. The response is the same for every one of these, on purpose. |
invalid_request | 400 | grant_type is missing, a parameter was sent twice, or the credentials were sent both as Basic and in the body. |
unsupported_grant_type | 400 | Send grant_type=client_credentials. |
rate_limited | 429 | Too many token requests. Reuse your token until it expires, then try again shortly. |
With an OAuth client library
Section titled “With an OAuth client library”A client library fetches a token, sends it on every call, and fetches a new one when it expires. In Python, with Authlib (tested with 1.8):
import os
from authlib.integrations.requests_client import OAuth2Session
API = "https://api.example.com"client = OAuth2Session( client_id=os.environ["CLIENT_ID"], # ci_test_... client_secret=os.environ["CLIENT_SECRET"], # sk_test_... token_endpoint=f"{API}/oauth/token", grant_type="client_credentials",)client.fetch_token()
# Every call carries the token; an expired one is replaced before the call is sent.methods = client.get(f"{API}/v1/payment_methods").json()Other libraries that support the client credentials grant take the same settings: the token endpoint /oauth/token, your client ID and your secret. Not every one fetches a new token by itself when the old one expires; if yours does not, fetch one before expires_in runs out, or when a call answers invalid_access_token.
Whose data a key reaches
Section titled “Whose data a key reaches”A key, and every token it is exchanged for, acts for its own merchant only. A request whose Merchant-Id header, query parameters or body fields name another merchant or tenant is refused with authorization_failure.