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-demo app.

1. Concepts

TermMeaning
PlatformYour application. You hold one OAuth client (Connected App) and connect many practitioners through it.
ConnectionOne practitioner's authorization of your Platform, established once via OAuth consent. You hold N connections (one access/refresh token pair per practitioner).
PractitionerThe 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 cardA 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.
InvoiceA 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.
GuestThe 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

EnvironmentBase URL
Productionhttps://api.onfirehealth.com
Staginghttps://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_id and client_secret — your OAuth client credentials (the secret is shown once; store it securely).
  • Authorize URL and token URL — the OAuth authorization_code endpoints.
  • Scopespayment-connection (list rate cards, create/read invoices) and offline_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)

  1. Redirect the practitioner to the authorize URL with your client_id, redirect_uri, scope=payment-connection offline_access, response_type=code, and a state you generate.
  2. The practitioner approves; Onfire redirects back to your redirect_uri with ?code=…&state=….
  3. 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

FieldTypeRequiredNotes
ref_idstringyesThe rate card's ref_id.
external_invoice_refstringyesYour invoice id. Idempotency key (see below).
client_emailstring (email)yesGuest email (where the pay link is sent).
client_namestringyesGuest name.
client_phonestringyesGuest phone.
client_billing_addressobjectyesline1, city, state, postal_code required; line2, country optional.
external_client_refstringnoYour 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

EventWhen
invoice.openInvoice created; awaiting payment.
invoice.processingPayment initiated (e.g. ACH in flight).
invoice.paidPayment settled.
invoice.voidedInvoice voided.
invoice.refundedPayment refunded.
webhook.pingConnectivity 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 2xx within ~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 on status + 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_ref

9. Errors

StatusMeaning
401Missing/invalid/expired access token. Refresh and retry.
403Practitioner not payout-verified, token can't be resolved to a practitioner, or out of scope.
404Rate card ref_id not found (or not active) for this practitioner.
409Conflict (e.g. duplicate resource).
422Validation error (missing/invalid fields).
429Rate 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.