This reference needs JavaScript. The guides follow; the full description of every endpoint is in openapi.json.
PF Bridge gives your system an HTTPS and JSON API to one clinic's Practice Fusion (PF) account. You never handle PF's login: PF Bridge signs in for you, keeps the session alive, and your calls to /v1 use that session.
/v1
This page takes you from nothing to a first successful call.
login_failed
POST /v1/login/security-code
When the connection is signed in, GET /v1/status answers "loginState": "logged_in".
GET /v1/status
"loginState": "logged_in"
Replace the base URL with the address you were given and KEY with your key. Both calls are safe: they only read.
KEY
BASE=https://api.example.com KEY=pfb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 1. Is the connection signed in? (No Practice Fusion call, not limited by the connection limiter.) curl -s -H "Authorization: Bearer $KEY" $BASE/v1/status # 2. The practice's providers curl -s -H "Authorization: Bearer $KEY" $BASE/v1/providers
If the second call answers 503 with "error": "pf_not_ready", the connection is not signed in yet. Read loginState in the answer and see Connection status and states.
503
"error": "pf_not_ready"
loginState
A new connection has bookingEnabled and messagesEnabled switched off. POST /v1/appointments answers 403 booking_disabled and POST /v1/messages answers 403 messages_disabled until a clinic administrator turns the switch on in the console (turning it on asks for a confirmation). GET /v1/status shows both switches.
bookingEnabled
messagesEnabled
POST /v1/appointments
403 booking_disabled
POST /v1/messages
403 messages_disabled
apiReady
true
/v1/providers
/v1/facilities
/v1/appointment-types
/v1/coverage-types
GET /v1/patients/by-mrn/{mrn}
POST /v1/patients/search
patientId
GET /v1/patients/{patientId}
sms
insurance
null
GET /v1/schedule/appointments?date=YYYY-MM-DD
GET /v1/schedule/conflicts
Idempotency-Key
error
Every operation has a Test Request button. Paste your API key (only the key, starting pfb_live_) into the Bearer Token box under Authentication, check the address at the top of the request, and press Send. Do not add an Authorization header by hand: the page already sends one, and two of them make the call fail with 401. The request goes straight from your browser to that address, and the key is sent nowhere else. Leave X-Connection-Id empty unless you use an organization key. If the browser reports a network error, the API at that address may not allow requests from this page; use the curl examples instead.
pfb_live_
Authorization
401
Every /v1 call needs an API key in the Authorization header:
Authorization: Bearer pfb_live_<43 characters>
The form is strict: Bearer with a capital B, exactly one space, then the key. Keys are pfb_live_ followed by 43 characters of A-Za-z0-9_-. Anything else (a missing header, a malformed key, an unknown, revoked or expired key) answers the same way:
Bearer
A-Za-z0-9_-
{ "error": "unauthorized", "message": "Missing or wrong API key" }
with status 401. The answer never says which of these it was.
X-Connection-Id
400 connection_mismatch
X-Connection-Id: conn_…
400 connection_required
404 connection_not_found
curl -s -H "Authorization: Bearer $ORG_KEY" -H "X-Connection-Id: conn_01J8EXAMPLE0000000000000" $BASE/v1/status
A key carries scopes. Each endpoint needs one; a key without it answers 403 forbidden_scope with requiredScope in the body.
403 forbidden_scope
requiredScope
status:read
catalog:read
GET /v1/appointment-types
/v1/practice-contacts
schedule:read
GET /v1/schedule
/v1/schedule/appointments
/v1/schedule/conflicts
patients:read
GET /v1/patients/{patientId}/insurance
GET /v1/appointments/{appointmentId}
appointments:write
messages:write
login:write
A key created for integrations normally has all of these. Keys can also carry manage:* scopes for the management API, which is not part of this reference.
manage:*
Rotate in the console. Rotation creates a new secret. The old secret keeps working for a grace period (24 hours by default, at most 168 hours), then stops. Deploy the new secret during the grace period. To cut an old key off at once, revoke it instead: a revoked key answers 401 immediately.
When a clinic administrator or PF Bridge support pauses a connection, or the organization is suspended, calls with an organization key answer 403 connection_disabled or 403 org_suspended. Calls with a connection key answer 503 pf_not_ready with "loginState": "logged_out" instead, because that is the answer such clients already handle.
403 connection_disabled
403 org_suspended
503 pf_not_ready
"loginState": "logged_out"
GET /v1/status describes the connection of your API key. It makes no Practice Fusion call and is not limited by the connection's rate limiter (only the per-key limit applies), so you can poll it. Poll every few minutes at most, and use Retry-After from other calls to decide when to try again sooner.
Retry-After
curl -s -H "Authorization: Bearer $KEY" $BASE/v1/status
loginStateSince
sessionActive
logged_in
apiReady: false
startedAt
lastApiOkAt
lastLoginAt
false
403
autoLogin
enabled
credentialsSaved
twoFactor
awaitingCodeSince
lastResult
rejected
timeout
limiter
connectionId
conn_…
session
obtainedAt
refreshDueAt
refresh.state
idle
queued
logging_in
awaiting_code
failed
lastUnlockAt, unlockAttempts, reloads, loginAttempts, crashes and lastActivityAt are bookkeeping fields that carry no information you need. Ignore them. lastUnlockAt and lastActivityAt are always null, and unlockAttempts and reloads are always 0.
lastUnlockAt
unlockAttempts
reloads
loginAttempts
crashes
lastActivityAt
0
unknown
logged_out
locked, unlocking and unlock_failed are never reported.
locked
unlocking
unlock_failed
A PF-backed call made while no usable session exists answers:
HTTP/1.1 503 Retry-After: 30 { "error": "pf_not_ready", "message": "Practice Fusion is not logged in yet", "loginState": "awaiting_code", "retryAfterSeconds": 30 }
Nothing was sent to Practice Fusion. loginState tells you why. Wait for Retry-After seconds and try again. If loginState is awaiting_code, a security code is needed. If it is login_failed, a person has to act. Reads can be retried freely. For a booking, retry with the same Idempotency-Key.
A session can also end by itself (Practice Fusion signs it out). Calls then answer 503 pf_session_rejected for a moment, a new login starts, and the connection returns to logged_in within a minute or two.
503 pf_session_rejected
Every failed call answers with a JSON body of the same shape:
{ "error": "slot_taken", "message": "Practice Fusion reports a conflict at that time", "appointmentConflicts": 1, "blockedTimeConflicts": 0 }
message
Every answer carries an X-Request-Id header. Send your own X-Request-Id (up to 100 characters of A-Za-z0-9._:-) to correlate calls, and quote it when you ask for support. Answers carry Cache-Control: no-store, and X-Bridge-Version.
X-Request-Id
A-Za-z0-9._:-
Cache-Control: no-store
X-Bridge-Version
429 rate_limited
502
504
4xx
500 internal_error
bad_request
bad_json
Content-Type: application/json
connection_required
connection_mismatch
unauthorized
forbidden_scope
booking_disabled
messages_disabled
connection_disabled
org_suspended
not_found
connection_not_found
method_not_allowed
body_too_large
provider_not_found
facility_not_found
appointment_type_not_found
patient_not_found
patient_mrn_ambiguous
appointment_not_found
start_in_past
appointment_type_inactive
any_time_disabled
allowAnyTime
recipient_unknown
unknownRecipients
GET /v1/practice-contacts
slot_taken
appointmentConflicts
blockedTimeConflicts
outside_hours
patient_inactive
idempotency_key_reused
pf_rejected_booking
pfStatus
pfDetail
booking_failed
pf_rejected_message
pf_user_unknown
booking_outcome_unknown
not_awaiting_code
code_already_used
code_rejected
rate_limited
reason
retryAfterSeconds
busy
budget
pf_throttled
key_rate
For organization keys, a plan with a hard usage cap can also answer 402 quota_exceeded (meter, periodEnd) when the cap is reached. Connection keys get 429 rate_limited with reason budget instead.
402 quota_exceeded
meter
periodEnd
pf_error
pf_not_ready
pf_session_rejected
pf_timeout
internal_error
Practice Fusion is a shared system that we reach through one signed-in session per connection, so every connection has its own limiter. It protects the clinic's PF account, not our servers. A limited call answers 429 with a body like:
429
HTTP/1.1 429 Retry-After: 2 { "error": "rate_limited", "message": "Too many Practice Fusion requests in flight for this connection", "reason": "busy", "retryAfterSeconds": 2 }
The Retry-After header and retryAfterSeconds carry the same number of seconds. Wait that long plus 0 to 1 second of random jitter, then retry. A limited call did nothing: it is always safe to repeat.
ip_rate
Besides the per-connection limiter, every API key may send 50 requests per second (a burst of 100). It exists to stop a runaway loop, and the defaults are far above what a normal integration sends. Over it you get:
HTTP/1.1 429 Retry-After: 1 { "error": "rate_limited", "message": "Too many requests for this API key", "reason": "key_rate", "retryAfterSeconds": 1 }
A call refused this way is not billed and did nothing, so it is safe to repeat after Retry-After. Another key of the same connection has its own allowance. Connection keys get exactly this body too (the reason value is new, the shape is the one you already handle).
The numbers are defaults. The actual values for your connection are in the limiter block of GET /v1/status (maxInFlight, pfCallsPerMinute, burst, tokens left now, pfBackoffUntil, and counters of rejected calls by reason).
maxInFlight
pfCallsPerMinute
burst
tokens
pfBackoffUntil
Each endpoint reserves a number of Practice Fusion calls up front and gives back what it did not use (a call that needs more is charged for what it really used). The costs are estimates. They are shown on every operation of the reference:
GET /v1/providers
5 + (1 if facilityId is given, else 2) × ceil(days / 7)
GET /v1/schedule/appointments
3 + (1 if facilityId is given, else 2)
Every /v1 call except GET /v1/status and POST /v1/login/security-code counts towards your organization's usage. Ask your administrator about the limits of your plan.
Networks fail between "sent" and "answered". Two calls change data in Practice Fusion: booking an appointment and sending a message. Both are built so that you can retry without doing it twice.
POST /v1/appointments requires an Idempotency-Key header: 8 to 100 characters of A-Za-z0-9._:-. Without it the call answers 400 bad_request. Generate a new key for each booking you intend to make (a UUID is fine) and store it with your own record of that booking, so a retry can send the same key.
400 bad_request
curl -s -X POST $BASE/v1/appointments \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: booking-2026-11-16-jordan-0001" \ -H "Content-Type: application/json" \ -d '{"patientId":"3f2b7a10-5c1e-4d8a-9b61-0a7d2e4c8f11","providerId":"9c4e1d52-7a30-4b6f-8e21-5d0b3a7c9e44","facilityId":"b1d6e3a8-2f47-4c90-a5d3-6e8f0c1b7a22","appointmentTypeId":"5a8c2f90-1e6d-4b37-9d42-c7e0a3b5f166","start":"2026-11-16T10:00","durationMinutes":30}'
Records are kept for 24 hours. The rules:
201
"replayed": true
422 idempotency_key_reused
504 booking_outcome_unknown
"Same body" means the same booking after defaults are applied (for example, leaving out durationMinutes and sending the default value are the same).
durationMinutes
"recovered": true
502 booking_failed
502 pf_rejected_booking
409 slot_taken
504 pf_timeout
POST /v1/messages has no header. It is de-duplicated on the messageId you put in the body, for 24 hours. A repeat of the same messageId returns the first answer with "replayed": true and sends nothing.
messageId
threadId
GET calls and the searches change nothing and can be retried at will within the rate limits. POST /v1/login/security-code is safe to retry: a code is used once (409 code_already_used for the same code within an hour).
GET
409 code_already_used
Practice Fusion protects logins with a two-step code that it texts to the practice's phone. PF Bridge asks for one whenever Practice Fusion does: on the first login of a connection, and later whenever Practice Fusion does not recognize the signed-in browser. This is rare for a connection that has been running for a while, but your integration should handle it.
loginState: "awaiting_code"
twoFactor.awaitingCodeSince
curl -s -X POST $BASE/v1/login/security-code \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"code":"482913"}'
The code is 4 to 6 digits, sent as a string. The key needs the login:write scope. This call is not limited by the connection's rate limiter (only the per-key limit applies) and does not need a signed-in session.
200 { "result": "logged_in" }
422 code_rejected
409 not_awaiting_code
A call can take up to about 19 seconds, because the login may still be reaching the code page and Practice Fusion then verifies the code. Set your client timeout to 20 seconds or more.
The code goes to the PF user's phone. Your system can read it and post it to this endpoint (for example, from an SMS provider webhook). An administrator can also type the code into the console. Whichever way a code arrives, it is used once.
Do not write security codes to logs. PF Bridge never logs them either.
The API is versioned in the path: everything here is /v1. Every answer carries X-Bridge-Version with the service version (for example 1.0.0).
1.0.0
Within /v1:
/v2
The OpenAPI 3.1 file this page is built from is published next to it as openapi.json. It is generated from the code, so it always matches the API.
openapi.json
status