PACE Partner API
Authentication
OAuth 2.0 client credentials, tokens and scopes.
The Partner API uses the OAuth 2.0 client credentials grant (RFC 6749, section 4.4). Your system authenticates as itself, not as a person.
Credentials
| Value | Format | Notes |
|---|---|---|
client_id | pc_ followed by 24 characters | Identifies your client. Not secret. |
client_secret | pace_cs_ followed by 43 characters | Shown once when the client is created. PACE stores only a hash. |
Each client belongs to one organisation and holds a fixed list of permissions (scopes). Keep the secret in a secrets manager or environment variable. If it is exposed, ask PACE to rotate it: rotation issues a new secret and immediately invalidates every token issued with the old one.
Request a token
POST /api/v1/oauth/token/ with Content-Type: application/x-www-form-urlencoded.
Send the credentials with HTTP Basic authentication (preferred):
curl -s -X POST https://mypacehub.com/api/v1/oauth/token/ \
-u "$PACE_CLIENT_ID:$PACE_CLIENT_SECRET" \
-d grant_type=client_credentialsor as form fields:
curl -s -X POST https://mypacehub.com/api/v1/oauth/token/ \
-d grant_type=client_credentials \
-d client_id="$PACE_CLIENT_ID" \
-d client_secret="$PACE_CLIENT_SECRET"| Field | Required | Description |
|---|---|---|
grant_type | Yes | Always client_credentials. |
scope | No | Space separated. Narrows the token to fewer scopes than your client holds. Omit it to receive all of them. |
Response
{
"access_token": "pace_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "programme.metrics.read"
}Tokens are valid for one hour. Cache the token and request a new one shortly before it expires. There are no refresh tokens in the client credentials grant.
Call the API
Send the token in the Authorization header on every request:
curl -s https://mypacehub.com/api/v1/me/ \
-H "Authorization: Bearer $PACE_ACCESS_TOKEN"Example clients
import os, time, requests
BASE = "https://mypacehub.com"
_token = {"value": None, "expires": 0}
def token():
if _token["value"] and time.time() < _token["expires"] - 60:
return _token["value"]
r = requests.post(
f"{BASE}/api/v1/oauth/token/",
auth=(os.environ["PACE_CLIENT_ID"], os.environ["PACE_CLIENT_SECRET"]),
data={"grant_type": "client_credentials"},
timeout=10,
)
r.raise_for_status()
body = r.json()
_token.update(value=body["access_token"], expires=time.time() + body["expires_in"])
return _token["value"]
me = requests.get(f"{BASE}/api/v1/me/", headers={"Authorization": f"Bearer {token()}"}, timeout=10)
print(me.json())const BASE = "https://mypacehub.com";
let cached = { value: null, expires: 0 };
async function token() {
if (cached.value && Date.now() < cached.expires - 60_000) return cached.value;
const basic = Buffer.from(`${process.env.PACE_CLIENT_ID}:${process.env.PACE_CLIENT_SECRET}`).toString("base64");
const res = await fetch(`${BASE}/api/v1/oauth/token/`, {
method: "POST",
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "client_credentials" }),
});
if (!res.ok) throw new Error(`token request failed: ${res.status}`);
const body = await res.json();
cached = { value: body.access_token, expires: Date.now() + body.expires_in * 1000 };
return cached.value;
}
const me = await fetch(`${BASE}/api/v1/me/`, { headers: { Authorization: `Bearer ${await token()}` } });
console.log(await me.json());Token errors
Errors follow RFC 6749 and always carry error and error_description.
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not form fields or a JSON object. |
| 400 | unsupported_grant_type | grant_type is not client_credentials. |
| 400 | invalid_scope | You asked for a scope your client does not hold. |
| 401 | invalid_client | Unknown client, wrong secret or revoked client. The answer is the same for all three. |
| 429 | slow_down | More than 30 token requests in a minute for this client. |
Scopes
| Scope | Gives access to | Available |
|---|---|---|
programme.metrics.read | De-identified programme KPIs: enrolment, adherence, persistence, dropout reasons | Next release |
events.subscribe | Register webhook endpoints and receive signed event notifications | Next release |
patients.read | Care team read of your own patients | Next release, hospitals only |
referrals.write | Refer a patient for enrolment | Next release, hospitals only |
Patient level scopes are never granted to a pharma client.