Credentials
- One key per integration, named for what it does — “Booking widget”, “Nightly export” — not one shared key across several systems. Requests and audit trails are per key (the rate limit is shared across the account), and a name that says what a key is for means revoking it later is surgical, not a guess about what else might break. See Best practices → Credentials.
- Scopes are minimal. Read-only unless the integration writes; a read-only key can’t write anything by construction (
403 key_read_only), which is the cheapest access control available. See Authentication → API keys. - Stored server-side only. Never in a browser, a mobile app bundle or a public repository —
phk_keys are recognised by secret scanners, but the fix for a leak is still rotation, not detection. See Authentication → Rotation, expiry and audit.
Error handling
-
422is handled by field, readingerrorsrather than guessing frommessage.messageis written for a human and can be reworded without notice; the attribute keys inerrorsare the stable part. See Errors → Handling errors well. - Only
429and5xxare retried, with exponential backoff and jitter, and the sameIdempotency-Keyreused on every retry of a givenPOSTso a retried write can’t create the record twice.409is retried only after the state that caused it changes;422,401and403are never retried. See Errors → Retry taxonomy. - The
codeis logged, not just the status.rate_limited,appointment_rule,billing_rule,key_read_onlyand the rest are stable identifiers your alerting and dashboards should key off — see the full error code catalogue.
Pacing
- 120 requests per minute, shared across the account’s API keys (AI connectors have their own separate MCP bucket), spent steadily rather than in bursts — firing the whole budget in the first second of every run and idling for the rest of the minute is the surest way to see
429s (for you and for every other integration on the account) that a paced integration never would. See Rate limits → Pacing an integration. -
Retry-Afteris honoured on every429, andX-RateLimit-Remainingis watched in normal operation — a value trending toward zero outside of known bulk runs usually means something should be a filter or a cache instead of a poll. See Best practices → Rate limits and retries.
Data hygiene
- Records you create carry your external id in
metadata(prefixed with your app name, e.g.crm_id) — so you can find, de-duplicate and cross-reference them without keeping a separate mapping table. See Metadata. - The clinic-local vs UTC rule is applied consistently.
created_at/updated_at(and webhookoccurred_at) are UTC, for windowing; everything the practice sees —starts_at/ends_at,invoice_date,payment_date, availability slots — is clinic-local with no offset, and is sent the same way when you write it. Get this backwards once and every booked time is off by the clinic’s UTC offset. See Best practices → Time.
Monitoring
- You know where to look when something’s wrong. The account’s Developers → API Logs report shows every call your integration makes — status, error code, latency, the calling key — filterable and exportable. Check it during rollout, not just when a user complains. See Security → Logging and what is stored about your calls and Errors → Diagnosing.
Sync correctness
- Incremental runs use an
updated_atwatermark with overlap — filterupdated_at=gte:<watermark>, sortupdated_at:asc, walk by cursor, and set the next watermark to when the run started (with a few minutes of overlap), not when it finished. See Best practices → Pagination. - Deletions are handled via
deleted_entities, not inferred from a record disappearing from a list — walk it since your last sync and remove or archive the matching ids on your side. See Querying → Deleted records.
Go-live day
- Start with one clinic. If your integration serves many accounts, turn it on for one first, watch Developers → API Logs and your own error rates while it runs for real, and only then roll out to the rest. A problem caught on one clinic is a problem the others never see.