Developers
Onfire Platform Integration Guide
This guide is for Platforms — third-party software that hosts many independent practitioners and integrates with Onfire on each practitioner's behalf to list their rate cards, create invoices, and receive lifecycle events. It covers authentication, the rate-card and invoice APIs, and the webhook contract, with end-to-end examples.
A reference implementation that exercises this entire flow (multi-practitioner OAuth, invoice creation, signed webhook receiver) is the
platform-demoapp.
1. Concepts
| Term | Meaning |
|---|---|
| Platform | Your application. You hold one OAuth client (Connected App) and connect many practitioners through it. |
| Connection | One practitioner's authorization of your Platform, established once via OAuth consent. You hold N connections (one access/refresh token pair per practitioner). |
| Practitioner | The Onfire customer you act for. Each is identified to you by a stable partner_public_id (prefix prt_…) — use it to route webhook events to the right practitioner in your system. |
| Rate card | A practitioner's purchasable offering. Referenced by an opaque ref_id; you pass ref_id when creating an invoice. The price is set by the practitioner, never by you. |
| Invoice | A charge you create for a practitioner's client (the guest). Identified by your own external_invoice_ref (your idempotency key) and Onfire's invoice_public_id. |
| Guest | The practitioner's customer who pays. They pay on an Onfire-hosted page (pay_url); you never handle card/bank data. |
PHI-minimal contract: outbound webhooks carry only money/status + your correlation ids (partner_public_id, external_invoice_ref, external_client_ref) — never the guest's name, email, phone, or address.
2. Environments
| Environment | Base URL |
|---|---|
| Production | https://api.onfirehealth.com |
| Staging | https://api-stage.onfirehealth.com |
All endpoints below are relative to the base URL. All traffic is HTTPS only.
3. What Onfire provides at onboarding
Onfire registers your Connected App and webhook endpoint and gives you:
client_idandclient_secret— your OAuth client credentials (the secret is shown once; store it securely).- Authorize URL and token URL — the OAuth
authorization_codeendpoints. - Scopes —
payment-connection(list rate cards, create/read invoices) andoffline_access(receive a refresh token). - Webhook signing secret (
whsec_…) — used to verify webhook authenticity (shown once).
Contact us at support@onfirehealth.com. You provide Onfire with your redirect URI (OAuth callback) and your webhook receiver URL (a public HTTPS endpoint — see §7).
4. Authentication (OAuth 2.0)
Onfire uses the standard authorization_code grant. Each practitioner consents once; you store the resulting tokens and call the API with the practitioner's access token.
4.1 Connect a practitioner (one-time consent)
- Redirect the practitioner to the authorize URL with your
client_id,redirect_uri,scope=payment-connection offline_access,response_type=code, and astateyou generate. - The practitioner approves; Onfire redirects back to your
redirect_uriwith?code=…&state=…. - Exchange the code at the token URL (HTTP Basic auth with
client_id:client_secret):
curl -X POST "<TOKEN_URL>" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="<code>" \
-d redirect_uri="https://platform.example.com/api/oauth/callback"
# → { "access_token": "...", "refresh_token": "...", "expires_in": 3600, ... }Store the access_token + refresh_token per practitioner. The access token is an RS256 JWT scoped to exactly that practitioner — it can never act for another.
4.2 Refresh
curl -X POST "<TOKEN_URL>" -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=refresh_token -d refresh_token="<refresh_token>"4.3 Calling the API
Send the practitioner's access token as a bearer token:
Authorization: Bearer <access_token>Payout-readiness gate. A practitioner cannot consent or transact until their Onfire payout setup is verified. Until then, consent and invoice creation are refused (
403).
5. Resolve the practitioner — GET /api/v1/meta/partners/me
The access token does not contain partner_public_id. Call /me once per connection (e.g. right after consent) and store the mapping partner_public_id → your tenant.
curl "$BASE/api/v1/meta/partners/me" -H "Authorization: Bearer $ACCESS_TOKEN"{
"partner_id": 42,
"partner_public_id": "prt_9f1c3a7b8d2e4f60a1b2c3d4e5f60718",
"partner_name": "The Happy Health Co",
"bill_com_vendor_status": "VERIFIED"
}partner_public_id is the routing key: every webhook you receive carries it, so this mapping is how you attribute events to the right practitioner.
6. APIs
6.1 List rate cards — GET /api/v1/meta/partner-rate-cards/
Returns the calling practitioner's active rate cards (no cross-tenant data). Optional query params: type (bundle|addOn), payout_plan, search, sort_by, skip, limit.
curl "$BASE/api/v1/meta/partner-rate-cards/" -H "Authorization: Bearer $ACCESS_TOKEN"[
{
"rate_card_id": 1201,
"partner_id": 42,
"company": "The Happy Health Co",
"product_name": "Initial Consultation",
"ref_id": "rc_7c2a91e4",
"sub_title": "60-minute intake",
"type": "bundle",
"duration": "6", // installment months
"full_price": "2050.00",
"installments_price": "1999.50",
"full_price_only": false,
"created_at": "2026-06-01T12:00:00Z",
"updated_at": "2026-06-01T12:00:00Z"
}
]Use the ref_id when creating an invoice. You display the price; you never set it.
6.2 Create an invoice — POST /api/v1/core/partner/invoices/
Creates an invoice for a guest and emails them an Onfire-hosted payment link. The amount is derived server-side from the rate card — you cannot supply it.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
ref_id | string | yes | The rate card's ref_id. |
external_invoice_ref | string | yes | Your invoice id. Idempotency key (see below). |
client_email | string (email) | yes | Guest email (where the pay link is sent). |
client_name | string | yes | Guest name. |
client_phone | string | yes | Guest phone. |
client_billing_address | object | yes | line1, city, state, postal_code required; line2, country optional. |
external_client_ref | string | no | Your id for the guest; echoed back on webhooks to correlate multiple invoices for the same guest. |
curl -X POST "$BASE/api/v1/core/partner/invoices/" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{
"ref_id": "rc_7c2a91e4",
"external_invoice_ref": "BIO-2026-000123",
"client_email": "guest@example.com",
"client_name": "Jane Doe",
"client_phone": "+14155550101",
"client_billing_address": {"line1":"1 Market St","city":"San Francisco","state":"CA","postal_code":"94105"},
"external_client_ref": "patient_5567"
}'Response 201 (InvoiceResponse, abridged):
{
"invoice_public_id": "inv_3Kp9...",
"status": "open",
"amount": "2050.00",
"currency": "USD",
"external_invoice_ref": "BIO-2026-000123",
"external_client_ref": "patient_5567",
"pay_url": "https://payment.onfirehealth.com/pay/inv_3Kp9...",
"created_at": "2026-06-24T17:00:00Z"
}Onfire sends the email, you can store the pay_url to show in the UI for user to manually send/resend. You receive an invoice.open webhook immediately, then invoice.paid once they pay.
Idempotency. external_invoice_ref is unique per practitioner. Re-POSTing the same ref returns the existing invoice (same invoice_public_id) with no duplicate and no second event. Use a stable ref and retry safely.
7. Webhooks
Onfire delivers invoice.* events for all your connected practitioners to the single endpoint registered for your Connected App. Route each event by partner_public_id.
7.1 Event types
| Event | When |
|---|---|
invoice.open | Invoice created; awaiting payment. |
invoice.processing | Payment initiated (e.g. ACH in flight). |
invoice.paid | Payment settled. |
invoice.voided | Invoice voided. |
invoice.refunded | Payment refunded. |
webhook.ping | Connectivity test sent when the endpoint is registered. Carries no invoice. |
7.2 Payload
A versioned, Stripe-style envelope. PHI-minimal — correlation ids + money/status only.
{
"id": "evt_b6b96615cb3d4172a5855ee92cf42d36",
"type": "invoice.paid",
"api_version": "2026-06-01",
"created": 1782755598,
"data": {
"invoice": {
"partner_public_id": "prt_9f1c3a7b8d2e4f60a1b2c3d4e5f60718",
"invoice_public_id": "inv_3Kp9...",
"status": "paid",
"external_invoice_ref": "BIO-2026-000123",
"external_client_ref": "patient_5567",
"amount": "2050.00",
"currency": "USD",
"ref_id": "rc_7c2a91e4",
"payment_reference": "pay_…"
}
}
}7.3 Verifying the signature (required)
Every delivery includes:
X-Onfire-Webhook-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>The signed payload is "{t}." + <raw_request_body>, HMAC-SHA256 with your endpoint's signing secret. Verify over the raw body (before any JSON re-serialization), use a constant-time compare, and reject stale timestamps.
const crypto = require("crypto");
function verifyOnfireWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay guard
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody) // Buffer/string of the exact bytes received
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: capture the raw body for this route
app.post("/api/webhooks/onfire", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyOnfireWebhook(req.body, req.get("X-Onfire-Webhook-Signature"), process.env.ONFIRE_WEBHOOK_SIGNING_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event.type, route by event.data.invoice.partner_public_id
res.status(200).end(); // ack fast
});7.4 Delivery semantics
- Acknowledge with
2xxwithin ~10s. Any non-2xx (or a timeout) is a failed attempt. - No redirects. Onfire does not follow
3xx— a redirect counts as failure. Your receiver must serve the POST directly, with no auth wall, bot-gate, or interstitial in front of it. - Retries. ~7 attempts over ~24h with backoff (
1m, 5m, 30m, 2h, 6h, 24h, ±10% jitter), then the event is dead-lettered. - Idempotency & ordering. Dedupe on the envelope
id(events may be redelivered). Don't assume strict ordering; reconcile onstatus+external_invoice_ref.
8. End-to-end flow
1. (once per practitioner) OAuth consent → store access + refresh tokens
2. GET /meta/partners/me → store partner_public_id → your tenant
3. GET /meta/partner-rate-cards/ → show offerings (ref_id, price)
4. POST /core/partner/invoices/ → 201 + pay_url; guest pays on Onfire
5. Receive invoice.open, then invoice.paid at your webhook endpoint
6. Verify signature → route by partner_public_id → reconcile by external_invoice_ref9. Errors
| Status | Meaning |
|---|---|
401 | Missing/invalid/expired access token. Refresh and retry. |
403 | Practitioner not payout-verified, token can't be resolved to a practitioner, or out of scope. |
404 | Rate card ref_id not found (or not active) for this practitioner. |
409 | Conflict (e.g. duplicate resource). |
422 | Validation error (missing/invalid fields). |
429 | Rate limited (see below). |
Error bodies are JSON: { "detail": "<message>" }.
10. Rate limits
The partner OAuth endpoints are rate-limited per connection (per practitioner), so one practitioner's volume never throttles another. On 429, back off and retry. If you expect high burst volume, contact Onfire.
11. Versioning
The webhook envelope carries api_version (currently 2026-06-01). Additive fields may be introduced without a version bump — ignore unknown fields and don't assume field order. Breaking changes ship under a new api_version.
12. Support
Reach out to your Onfire contact for credentials, sandbox access, or to update your redirect URI or webhook URL.
