# PACE Partner API Connect your hospital, pharma or partner system to PACE. The Partner API is a versioned REST API under `/api/v1/`, secured with OAuth 2.0 client credentials and described by a published OpenAPI 3 specification. ## What you can do today - Exchange your client credentials for an access token. - Confirm which organisation, environment and permissions your credentials belong to. - Build and test against a sandbox organisation that holds realistic demo data and no real patients. Programme data, events and referrals arrive in the next release. See [Roadmap](/developers/roadmap). ## Quickstart ### 1. Get your credentials PACE creates an API client for your organisation and sends you a `client_id` and a `client_secret` over a secure channel. Start with a sandbox client. ### 2. Request an access token ```bash curl -s -X POST https://mypacehub.com/api/v1/oauth/token/ \ -u "$PACE_CLIENT_ID:$PACE_CLIENT_SECRET" \ -d grant_type=client_credentials ``` ```json { "access_token": "pace_at_...", "token_type": "Bearer", "expires_in": 3600, "scope": "" } ``` ### 3. Make your first call ```bash curl -s https://mypacehub.com/api/v1/me/ \ -H "Authorization: Bearer $PACE_ACCESS_TOKEN" ``` ```json { "api_version": "1", "client_id": "pc_3f9a...", "name": "Sandbox CRM", "audience": "pharma", "organization": { "name": "Demo Pharma", "slug": "demo-pharma" }, "environment": "sandbox", "scopes": [], "token_expires_at": "2026-10-16T19:00:00Z", "rate_limit_per_minute": 120 } ``` If you see your organisation and `"environment": "sandbox"`, your integration is connected. ## Build with Claude Code or Codex Every page of these docs is available as plain Markdown, so a coding assistant can read the exact contract instead of guessing it. - **One file with everything:** [`/llms-full.txt`](/llms-full.txt) holds every guide and the full API reference. - **Index:** [`/llms.txt`](/llms.txt) lists every page with a link to its Markdown. - **OpenAPI spec:** [`/api/v1/schema/?format=json`](/api/v1/schema/?format=json) for client generators. - **Any page:** use **Copy page** at the top of the page. A prompt you can paste into Claude Code or Codex in your own repository: ```text Read https://mypacehub.com/llms-full.txt. It is the complete documentation of the PACE Partner API. Write a small client for it in this repository: 1. Read PACE_CLIENT_ID and PACE_CLIENT_SECRET from the environment, never hardcode them. 2. Get a token with the client credentials grant and cache it until 60 seconds before it expires. 3. Call GET /api/v1/me/ and print the organisation and environment. 4. Retry once on 401 with a fresh token. On 429 wait for Retry-After seconds. Follow the documentation exactly and do not invent endpoints that it does not list. ``` ## Reference - [Authentication](/developers/authentication) - [Environments and data access](/developers/environments) - [Rate limits and errors](/developers/errors-and-limits) - [Versioning](/developers/versioning) - [API reference](/developers/reference) - [Interactive console (Swagger)](/api/v1/swagger/) --- # Authentication 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): ```bash curl -s -X POST https://mypacehub.com/api/v1/oauth/token/ \ -u "$PACE_CLIENT_ID:$PACE_CLIENT_SECRET" \ -d grant_type=client_credentials ``` or as form fields: ```bash 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 ```json { "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: ```bash curl -s https://mypacehub.com/api/v1/me/ \ -H "Authorization: Bearer $PACE_ACCESS_TOKEN" ``` ## Example clients ```python 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()) ``` ```javascript 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. --- # Environments and data access ## Sandbox and live There is one base URL, `https://mypacehub.com`. What a client sees depends on the organisation it belongs to. | Environment | Who gets it | Data | |---|---|---| | `sandbox` | Clients of a simulation organisation | Realistic demo data, rebuilt nightly. No real patients, ever. | | `live` | Clients of a live organisation | Your organisation's real programme data, within your scopes. | `GET /api/v1/me/` tells you which one your credentials belong to. Build and test against a sandbox client first, then ask PACE for a live client. Sandbox and live clients have different credentials, so a test system cannot reach live data by mistake. ## One organisation per client A client belongs to exactly one organisation and only ever sees that organisation's data. There is no client that can read across organisations. ## Privacy rules These follow the same rules the PACE application applies on screen. - **Pharma clients receive de-identified programme data only.** No names, phone numbers, hospital record numbers or patient level records. - **Patient level data goes only to the treating hospital,** and only for patients who have consented. - The patient's consent is recorded by PACE. A referral from a hospital system still asks the patient for consent over WhatsApp before any message is sent. ## Audit trail Every request to `/api/v1/` is recorded against your client: method, path, status, time, duration and source address. Request bodies and query strings are never stored. Do not put patient information in a URL. --- # Rate limits and errors ## Rate limits | Limit | Default | When exceeded | |---|---|---| | API requests per client | 120 per minute | `429 Too Many Requests` with `Retry-After: 60` | | Token requests per client | 30 per minute | `429` with `"error": "slow_down"` | Your limit is shown in `rate_limit_per_minute` on `GET /api/v1/me/`. If you need a higher limit, ask PACE. When you receive a 429, wait for the number of seconds in `Retry-After` before trying again. ## Status codes | Status | Meaning | What to do | |---|---|---| | 200 | Success | | | 400 | The request is malformed | Check the body against the reference. | | 401 | Missing, invalid, expired or revoked token | Request a new token. If that also fails, your client may have been rotated or revoked. | | 403 | Your token does not hold the scope this endpoint needs | Request a token with that scope, or ask PACE to grant it. | | 404 | Not found, or outside your organisation | PACE answers the same way for both, so it never confirms that a record exists elsewhere. | | 429 | Rate limit exceeded | Wait for `Retry-After` seconds. | | 5xx | PACE error | Retry with exponential backoff. | ## Error bodies API errors return a JSON object with `detail`: ```json { "detail": "Invalid or expired access token." } ``` Token endpoint errors use the OAuth format with `error` and `error_description`. See [Authentication](/developers/authentication). --- # Versioning The version is part of the path: `/api/v1/`. ## What we promise within v1 - We **add** endpoints, optional request fields and response fields. - We **never remove or rename** a field, change its type or change the meaning of a value. - New enumeration values may appear. Treat unknown values as "other" rather than failing. A change that breaks any of these ships as `/api/v2/`, announced in advance, with v1 kept running alongside it for a transition period. Write your client to ignore response fields it does not recognise. ## The internal application API Other paths under `/api/` serve the PACE application itself. They change with the product, they are not a partner contract, and partner tokens are refused there. ## Specification The OpenAPI 3 specification is the contract: [`/api/v1/schema/?format=json`](/api/v1/schema/?format=json) (YAML without `?format=json`). It covers `/api/v1/` only. --- # Roadmap What is live, and what comes next. Dates are confirmed with each partner. ## Live now - OAuth 2.0 client credentials, `GET /api/v1/me/` - Sandbox organisations with demo data - Per client rate limits and audit trail - OpenAPI 3 specification, this portal and the interactive console ## Next release - **Programme data** (`programme.metrics.read`): de-identified enrolment, adherence, persistence and dropout reasons for pharma CRMs and data warehouses. - **Signed webhooks** (`events.subscribe`): `patient.enrolled`, `consent.changed`, `dose.confirmed`, `dose.missed`, `escalation.raised`, `safety_event.reported`, `therapy.discontinued`. - **Hospital access** (`patients.read`, `referrals.write`): enrolment status, adherence and escalations for the hospital's own consented patients, and referral intake from the EHR. ## Later - HL7 FHIR R4 read API with a published CapabilityStatement. - Two way EHR integration: FHIR Subscriptions and SMART on FHIR launch. - Ready made feeds for major pharma CRMs. To plan an integration, [book a technology integration discussion](/demo?interest=technology-integration-partnership). --- # API reference Base URL: `https://mypacehub.com`. Specification: [https://mypacehub.com/api/v1/schema/?format=json](https://mypacehub.com/api/v1/schema/?format=json). Interactive console: [https://mypacehub.com/api/v1/swagger/](https://mypacehub.com/api/v1/swagger/). ## Get an access token (client credentials) `POST /api/v1/oauth/token/` No bearer token. Authenticate with your client credentials. Exchange your client_id and client_secret for a bearer token valid for one hour. Send the credentials with HTTP Basic auth (preferred) or as form fields. `scope` is optional and space-separated; it may only narrow the scopes granted to your client. ### Request (`application/x-www-form-urlencoded`) | Field | Type | Required | Description | |---|---|---|---| | `grant_type` | "client_credentials" | Yes | Always client_credentials. | | `scope` | string | No | Space separated. Narrows the token to fewer scopes than your client holds. Omit to receive all of them. | | `client_id` | string | No | Only when not using HTTP Basic. | | `client_secret` | string | No | Only when not using HTTP Basic. | ### 200 | Field | Type | Always present | Description | |---|---|---|---| | `access_token` | string | Yes | Send as Authorization: Bearer . | | `token_type` | string | No | Always Bearer. | | `expires_in` | integer | Yes | Lifetime in seconds (3600). | | `scope` | string | Yes | Space separated scopes this token carries. Empty if none. | ```json { "access_token": "pace_at_2Vh0yQm7c1...", "token_type": "Bearer", "expires_in": 3600, "scope": "programme.metrics.read" } ``` ### 400 invalid_request / unsupported_grant_type / invalid_scope ### 401 invalid_client ```json { "error": "invalid_client", "error_description": "Client authentication failed." } ``` ### 429 too many attempts ### Example ```bash curl -s -X POST https://mypacehub.com/api/v1/oauth/token/ \ -u "$PACE_CLIENT_ID:$PACE_CLIENT_SECRET" \ -d grant_type=client_credentials ``` ## Describe the calling client `GET /api/v1/me/` Requires `Authorization: Bearer `. Returns the client this token belongs to, its organisation, the environment (`sandbox` for a simulation organisation: demo data only, no real patients) and the token's scopes. Use it to check your credentials work. ### 200 | Field | Type | Always present | Description | |---|---|---|---| | `api_version` | string | Yes | Major API version, "1". | | `client_id` | string | Yes | Your client identifier. | | `name` | string | Yes | The name PACE gave this client. | | `audience` | "pharma" | "hospital" | "psp_partner" | Yes | Which kind of partner this client is. | | `organization` | object | Yes | The one organisation this client belongs to. | | `organization.name` | string | Yes | Organisation name. | | `organization.slug` | string | Yes | Stable organisation identifier. | | `environment` | "sandbox" | "live" | Yes | sandbox: demo data only, never real patients. | | `scopes` | string[] | Yes | This token's scopes. | | `token_expires_at` | string (date-time) | Yes | When this access token stops working. | | `rate_limit_per_minute` | integer | Yes | Requests per minute allowed for this client. | ```json { "api_version": "1", "client_id": "pc_3f9a1c27b84e0d5a6c19f2e7", "name": "Sandbox CRM", "audience": "pharma", "organization": { "name": "Demo Pharma", "slug": "demo-pharma" }, "environment": "sandbox", "scopes": [], "token_expires_at": "2026-10-16T19:00:00Z", "rate_limit_per_minute": 120 } ``` ### Example ```bash curl -s -X GET https://mypacehub.com/api/v1/me/ \ -H "Authorization: Bearer $PACE_ACCESS_TOKEN" ```