API reference
Version 1.0.0-experimental. Read-only access to your organisation's own records with scoped API keys, and signed webhooks for events.
Authentication
Every request carries an API key in the Authorization header. A browser session is never accepted, so the API cannot be called with someone's signed-in cookie.
Authorization: Bearer ark_live_<keyId>_<secret>
- Keys are created by an organisation administrator, are shown once, and are stored only as a SHA-256 hash.
- A key expires within your organisation's maximum lifetime (default 365 days, never more than 365). Rotation issues a new key and keeps the old one working for a grace period you choose (up to 7 days).
- A key never does more than the administrator who created it can do now. Deactivate them, change their role, or end the organisation's subscription to a product, and the key loses that access on its next request.
ark_live_keys work only on a production deployment andark_test_keys only on a test deployment.
Scopes
| Scope | Application | Grants |
|---|---|---|
| rcm.claims:read | ArkCloud RCM | Claim status, amounts and dates. No patient demographics, no denial narrative. |
| rehab.appointments:read | ArkCloud Rehab | Appointment times, status and the ids of the patient, episode and clinician. No notes, no reasons. |
Endpoints
The machine-readable description is at /api/v1/openapi.json (OpenAPI 3.0). Administrators can try each endpoint from the API explorer in the API & Integrations Center.
ArkCloud Health platform
GET/api/v1/me
Identifies the calling key: its organisation, environment, scopes and expiry. Useful as a connectivity check.
Scope: any valid key · Stability: experimental
Example
GET /api/v1/me
{
"data": {
"credentialId": "3c1d…",
"tenantId": "9b2e…",
"environment": "live",
"scopes": [
"rcm.claims:read"
],
"expiresAt": "2027-01-01T00:00:00.000Z"
}
}GET/api/v1/events
The events an endpoint can subscribe to, with the exact fields each one carries.
Scope: any valid key · Stability: experimental
Example
GET /api/v1/events
{
"data": [
{
"type": "claim.submitted",
"application": "rcm",
"description": "A claim's 837P was uploaded to the clearinghouse mailbox.",
"fields": {
"claimId": "string"
}
}
]
}ArkCloud RCM
GET/api/v1/rcm/claims
Claims in the organisation, oldest change first, so a client can poll with `updatedSince` and page with `cursor`.
Scope: rcm.claims:read · Stability: experimental
- status (query) — Filter by claim status.
- updatedSince (query) — Only claims changed at or after this time.
- limit (query) — Page size, 1-100. Default 25.
- cursor (query) — `nextCursor` from the previous page.
Example
GET /api/v1/rcm/claims?status=SUBMITTED&limit=1
{
"data": [
{
"id": "6f1c2b9e-0d4f-4b8e-9a51-3b7d2c1e0a11",
"accountId": "0b7f3e21-5c9a-4d0e-8f6b-1a2c3d4e5f60",
"claimNumber": "CLM-000123",
"claimType": "PROFESSIONAL",
"status": "SUBMITTED",
"payerSequence": "PRIMARY",
"payerName": "Example Health Plan",
"payerId": "EXMPL",
"payerClaimNumber": null,
"serviceFromDate": "2026-09-29T00:00:00.000Z",
"serviceToDate": null,
"billedAmount": "180.00",
"allowedAmount": null,
"paidAmount": null,
"patientResponsibility": null,
"denialCode": null,
"submittedAt": "2026-09-30T14:02:11.000Z",
"adjudicatedAt": null,
"paidAt": null,
"createdAt": "2026-09-30T13:58:40.000Z",
"updatedAt": "2026-09-30T14:02:11.000Z"
}
],
"pagination": {
"nextCursor": "MjAyNi0wOS0zMFQxNDowMjoxMS4wMDBafDZmMWMy…",
"limit": 1
}
}GET/api/v1/rcm/claims/{id}
One claim by id.
Scope: rcm.claims:read · Stability: experimental
- id (path, required) — Claim id.
Example
GET /api/v1/rcm/claims/6f1c2b9e-0d4f-4b8e-9a51-3b7d2c1e0a11
{
"data": {
"id": "6f1c2b9e-0d4f-4b8e-9a51-3b7d2c1e0a11",
"accountId": "0b7f3e21-5c9a-4d0e-8f6b-1a2c3d4e5f60",
"claimNumber": "CLM-000123",
"claimType": "PROFESSIONAL",
"status": "SUBMITTED",
"payerSequence": "PRIMARY",
"payerName": "Example Health Plan",
"payerId": "EXMPL",
"payerClaimNumber": null,
"serviceFromDate": "2026-09-29T00:00:00.000Z",
"serviceToDate": null,
"billedAmount": "180.00",
"allowedAmount": null,
"paidAmount": null,
"patientResponsibility": null,
"denialCode": null,
"submittedAt": "2026-09-30T14:02:11.000Z",
"adjudicatedAt": null,
"paidAt": null,
"createdAt": "2026-09-30T13:58:40.000Z",
"updatedAt": "2026-09-30T14:02:11.000Z"
}
}ArkCloud Rehab
GET/api/v1/rehab/appointments
Visits whose start time falls in [from, to). The window may be at most 31 days; it defaults to the next 7 days.
Scope: rehab.appointments:read · Stability: experimental
- from (query) — Window start (inclusive). Default: now.
- to (query) — Window end (exclusive). Default: from + 7 days.
- status (query) — Filter by appointment status.
- limit (query) — Page size, 1-100. Default 25.
- cursor (query) — `nextCursor` from the previous page.
Example
GET /api/v1/rehab/appointments?from=2026-10-06T00:00:00Z&to=2026-10-07T00:00:00Z
{
"data": [
{
"id": "a1d0c3f4-77b2-4c39-9e0a-5b6c7d8e9f01",
"patientId": "c2e4f6a8-1b3d-4f5a-8c7e-9d0b1a2c3e4f",
"episodeId": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f7a8b9",
"providerId": "u_8f3c",
"encounterId": null,
"startsAt": "2026-10-06T15:00:00.000Z",
"endsAt": "2026-10-06T15:45:00.000Z",
"status": "SCHEDULED",
"telehealth": false,
"checkedInAt": null,
"completedAt": null,
"cancelledAt": null,
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-01T09:12:00.000Z"
}
],
"pagination": {
"nextCursor": null,
"limit": 25
}
}GET/api/v1/rehab/appointments/{id}
One appointment by id.
Scope: rehab.appointments:read · Stability: experimental
- id (path, required) — Appointment id.
Example
GET /api/v1/rehab/appointments/a1d0c3f4-77b2-4c39-9e0a-5b6c7d8e9f01
{
"data": {
"id": "a1d0c3f4-77b2-4c39-9e0a-5b6c7d8e9f01",
"patientId": "c2e4f6a8-1b3d-4f5a-8c7e-9d0b1a2c3e4f",
"episodeId": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f7a8b9",
"providerId": "u_8f3c",
"encounterId": null,
"startsAt": "2026-10-06T15:00:00.000Z",
"endsAt": "2026-10-06T15:45:00.000Z",
"status": "SCHEDULED",
"telehealth": false,
"checkedInAt": null,
"completedAt": null,
"cancelledAt": null,
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-01T09:12:00.000Z"
}
}Examples
cURL
curl -H "Authorization: Bearer $ARKCLOUD_API_KEY" \ "https://<your ArkCloud address>/api/v1/rcm/claims?status=SUBMITTED&limit=25"
JavaScript / TypeScript
const res = await fetch(`${base}/api/v1/rehab/appointments?from=${from}&to=${to}`, {
headers: { Authorization: `Bearer ${process.env.ARKCLOUD_API_KEY}` },
});
if (res.status === 429) await sleep(Number(res.headers.get("Retry-After")) * 1000);
const { data, pagination } = await res.json();
// pass pagination.nextCursor back as ?cursor= until it is nullPython
import os, requests
r = requests.get(
f"{base}/api/v1/rcm/claims",
params={"updatedSince": "2026-10-01T00:00:00Z", "limit": 100},
headers={"Authorization": f"Bearer {os.environ['ARKCLOUD_API_KEY']}"},
timeout=30,
)
r.raise_for_status()
body = r.json() # {"data": [...], "pagination": {"nextCursor": ..., "limit": 100}}Pagination and filtering
List endpoints return { data, pagination: { nextCursor, limit } }. Pass nextCursor back as ?cursor= until it is null. limit is 1 to 100 (default 25). Claims are ordered by last change, oldest first, so updatedSince plus the cursor is a complete incremental sync. Appointments are filtered by a start-time window of at most 31 days.
Errors
Errors are { error: { code, message, requestId } }. Every response carries X-Request-Id; quote it when reporting a problem. A record in another organisation, or one that does not exist, is the same 404 NOT_FOUND.
| 503 | API_UNAVAILABLE | The API is not enabled on this deployment. |
| 401 | UNAUTHENTICATED | No key, or not in the form Authorization: Bearer ark_<env>_... |
| 401 | INVALID_CREDENTIAL | The key is not recognised by this deployment. |
| 401 | CREDENTIAL_REVOKED | The key was revoked. |
| 401 | CREDENTIAL_EXPIRED | The key passed its expiry date. |
| 401 | CREDENTIAL_OWNER_INACTIVE | The administrator who created the key has left the organisation or been deactivated. |
| 403 | API_DISABLED_FOR_ORGANIZATION | The organisation's administrator has switched API access off. |
| 403 | INSUFFICIENT_SCOPE | The key was not granted the scope this endpoint requires. |
| 403 | PRODUCT_NOT_AVAILABLE | The organisation, or the key's creator, cannot use this product. |
| 403 | PERMISSION_DENIED | The key's creator's role no longer allows this. |
| 404 | NOT_FOUND | No such record in this organisation. |
| 400 | INVALID_PARAMETER | A query or path parameter is malformed; the message names it. |
| 429 | RATE_LIMITED | Too many requests for this key this minute. Retry after the Retry-After header. |
| 500 | INTERNAL_ERROR | Unexpected failure. Quote the X-Request-Id when reporting it. |
Rate limits
Per key, per minute: your organisation's setting (default 120, at most 600). Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 carries Retry-After in seconds. Repeated failed authentication from one address is also limited.
Webhooks
An administrator registers an HTTPS endpoint, chooses events, and verifies it: ArkCloud sends a signed webhook.ping and the endpoint must answer 2xx. Private, loopback and cloud-metadata addresses are refused when the endpoint is saved and again before every delivery, and redirects are not followed.
Each delivery is a POST of { id, type, apiVersion, createdAt, tenantId, data }. Events carry identifiers, statuses, codes, amounts and times - never names, dates of birth, contact details or note text. Fetch more through the API with a scoped key if your system needs it.
| Event | When | data fields |
|---|---|---|
| appointment.scheduled | A visit was booked. | appointmentId, patientId, episodeId, providerId, startsAt, endsAt, telehealth |
| appointment.completed | A visit was completed; its encounter, if one was started, is completed with it. | appointmentId, patientId, encounterId, completedAt |
| appointment.cancelled | A visit was cancelled or recorded as a no-show. | appointmentId, patientId, status |
| documentation.signed | A clinical note was signed. | documentId, patientId, episodeId, encounterId, documentType, version, signedAt |
| charge.posted | A delivered service was posted to the patient account as a charge. | chargeId, serviceEventId, accountId |
| claim.created | A claim was assembled from posted charges. | claimId, accountId, claimNumber, billedAmount |
| claim.submitted | A claim's 837P was uploaded to the clearinghouse mailbox. | claimId, claimNumber, interchangeControlNumber |
| remittance.posted | An 835 remittance was posted to a claim. | claimId, remittanceId, paymentAmount, adjustmentAmount, patientResponsibility, partiallyPosted |
| eligibility.checked | An eligibility (270/271) check completed. | eligibilityCheckId, patientId, coverageActive, coverageChanged |
| eligibility.coverage_changed | An eligibility check found coverage had become active or inactive. | eligibilityCheckId, patientId, previousCoverageActive, currentCoverageActive |
| payment.succeeded | A card payment was captured and posted. | paymentId, accountId, amount |
| payment.failed | A card payment failed. | paymentId, accountId, amount, errorCode |
| payment.refunded | A card payment was refunded, in part or in full. | paymentId, accountId, refundAmount, isFullRefund |
| denial.created | A claim denial was recorded. | denialId, claimId, carcCode, category, amount |
Verifying a delivery. X-ArkCloud-Signature is v1=<hex HMAC-SHA256(secret, "<X-ArkCloud-Timestamp>.<raw body>")>. While a secret is being rotated it carries two v1= values, one per secret; accept either. Refuse a timestamp more than five minutes old, and deduplicate on X-ArkCloud-Event-Id.
Order and repeats. Deliveries can arrive out of order (a retried event arrives after later ones) and, rarely, more than once. Order by the envelope's createdAt, and treat an event id you have already processed as done.
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (do not re-serialise it).
export function verifyArkCloudWebhook(rawBody: string, headers: Headers, secret: string): boolean {
const ts = headers.get("X-ArkCloud-Timestamp") ?? "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest();
return (headers.get("X-ArkCloud-Signature") ?? "")
.split(",")
.map((part) => part.trim().match(/^v1=([0-9a-f]{64})$/)?.[1])
.some((hex) => hex !== undefined && timingSafeEqual(Buffer.from(hex, "hex"), expected));
}
// Then deduplicate on X-ArkCloud-Event-Id: a delivery can arrive more than once.Python
import hashlib, hmac, time
def verify_arkcloud_webhook(raw_body: bytes, headers, secret: str) -> bool:
ts = headers.get("X-ArkCloud-Timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300: # replay window
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
sigs = [p.strip()[3:] for p in headers.get("X-ArkCloud-Signature", "").split(",") if p.strip().startswith("v1=")]
return any(hmac.compare_digest(s, expected) for s in sigs)A failed delivery is retried with growing delays (30 seconds, doubling, at most 6 hours) up to your organisation's attempt limit (default 8). An endpoint that fails 20 deliveries in a row, or answers 410 Gone, is switched off until an administrator resumes it. The delivery history and a manual retry are in the API & Integrations Center.
Versioning and deprecation
The version is in the path (/api/v1). Within v1, fields may be added and new endpoints and events may appear; nothing is removed or renamed. While v1 is experimental, a breaking change is announced in this reference at least 30 days ahead. Once it is generally available, a breaking change means v2, and v1 keeps working for at least 12 months after v2 is published.
What is not here
The web application's own /api endpoints (described for reference at /api/docs) serve a signed-in browser session and are not a supported contract. There are no write endpoints, SDKs, OAuth client credentials or bulk-import endpoint. Claims, remittances and acknowledgements move as X12 files through a clearinghouse mailbox, and other connections are described with their real status under integrations. To ask for something the API does not cover: [email protected].
