> ## Documentation Index
> Fetch the complete documentation index at: https://build.practicehub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing your integration

> Get a developer account as your sandbox, then test with habits that keep every write accounted for.

There is no *test flag* on a production account — a key is a key on a real clinic, and a `POST` creates a real patient, a real appointment, a real invoice. The sandbox is a separate account: PracticeHub provides **developer accounts** — full PracticeHub accounts of your own — for exactly this. This page is how to use one well, plus the habits that keep writes accounted for wherever you point your integration.

## Get a developer account — your sandbox

A **developer account** is a fully-fledged PracticeHub account — its own subdomain, its own isolated database, every feature enabled, exactly like any clinic's (see [How it is put together](/introduction#how-it-is-put-together)). Nothing about the API knows or cares that it's yours rather than a practice's. That's what makes it a real sandbox: every rule, every validation, every status code behaves precisely as it will in production, and there is nobody's real patient data to worry about breaking. Email [help@practicehub.io](mailto:help@practicehub.io) to request one; set up your reference data (locations, practitioners, appointment types, payment methods) in Settings the way a practice would, and it's your permanent staging clinic.

Sign up for one and treat it as yours to break:

* Build and iterate against it with a read & write key, without asking anyone's permission first.
* Run your full write path — creates, updates, deletes, the awkward edge cases — as many times as you need.
* Throw it away and start again if you make a mess. Nobody downstream is affected.

If you're mid-engagement and waiting on a developer account, the fallback is a spare "test patient" tagged clearly in the client's live account, with a read-only key wherever you don't strictly need to write, and any test writes agreed with the practice first. That works, but every mistake happens on a real clinic's data, watched by whoever owns the account. Reach for the developer account first.

## Prove reads before you write

Create your first key **read-only** (see [Authentication → API keys](/authentication#api-keys)). Build out querying, filtering, pagination and your data model against it — nothing you do with a read-only key can change anything, so there's no risk in getting the shape of the integration right first. Only once reads are solid do you create (or rotate to) a read & write key for the parts of your integration that actually need to write. This isn't just caution: it also means the first time you touch `POST` or `PATCH`, you already trust your understanding of the resource, because you've been reading it correctly for a while.

## The start-with-ten habit

The same discipline that makes a [data migration](/guides/data-migration) safe applies to testing any write path: don't point a new integration at a full dataset on the first run. Write ten records. Stop. Open PracticeHub and look at them properly — right fields in the right places, dates and times as expected, nothing silently defaulted. Only once ten look right do you let it run unattended on the rest. Every bug caught at ten is a bug the other ten thousand never have. See [Importing your data → The three habits of a good migration](/guides/importing-your-data#the-three-habits-of-a-good-migration) for the same pattern applied to a full migration.

## Idempotency-Key: your retry safety net

While you're iterating — scripts that get killed mid-run, flaky local networking, a debugger breakpoint that leaves a request hanging — you will re-run `POST`s you're not sure completed. Send an `Idempotency-Key` on every create from day one, even in test code, and a repeat with the same key and body returns the original result instead of creating a duplicate. This is the exact mechanism you'll rely on in production, so exercising it during testing — deliberately re-sending a request with the same key and watching `Idempotent-Replayed: true` come back — is worth doing once on purpose. See [Writing → Retrying safely](/guides/writing#retrying-safely-idempotency-keys).

## Tag test records so cleanup is one query

Every record you write while testing should carry a `metadata` flag, for example `{ "test": true }` — [metadata](/guides/metadata) accepts booleans and is filterable on every resource, so this costs nothing to add and nothing to query later:

```bash theme={null}
BASE=https://your-dev-account.your-region.practicehub.io/v3/api

# Tag everything you create while testing
curl -X POST $BASE/patients \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "first_name": "Test", "last_name": "Patient", "metadata": { "test": true } }'

# Find it all later, on any resource
curl -g "$BASE/patients?metadata[test]=eq:true" -H "Authorization: Bearer $KEY"
```

Cleanup is then a filtered read plus a `DELETE` sweep, instead of hunting through the account for records you remember creating at some point:

```bash theme={null}
for id in $(curl -gs "$BASE/patients?metadata[test]=eq:true" \
  -H "Authorization: Bearer $KEY" | jq '.data[].id'); do
  curl -X DELETE "$BASE/patients/$id" -H "Authorization: Bearer $KEY"
done
```

Delete in the same order any cleanup has to respect in production — the rules don't relax because it's test data. Void invoices (and their allocations) before deleting the patient; only a `pending` or `arrived` appointment with no active invoice can be deleted outright. A `409` mid-sweep means something's still attached — see [Writing](/guides/writing) for the exact preconditions per resource, and [Errors](/guides/errors) for what each refusal means.

## Verify your assumptions in CI: `GET /schema/{resource}`

`GET /schema/{resource}` is the contract for what a resource returns and accepts (see [Querying → Discovering the model](/guides/querying#discovering-the-model)). Add a step to your CI pipeline, run against your developer account, that asserts the fields your integration actually depends on are still there:

```bash theme={null}
# ci/check-schema.sh — fail the build if a field this integration relies on has gone
SCHEMA=$(curl -s -H "Authorization: Bearer $KEY" "$BASE/schema/patients")

for field in first_name last_name email numbers address metadata; do
  echo "$SCHEMA" | jq -e --arg f "$field" '.attributes[$f] // .input.create[$f] // empty' > /dev/null \
    || { echo "missing: $field"; exit 1; }
done
```

This is not a compatibility test in the usual sense, and it isn't meant to catch the API changing under you — per [Versioning and stability](/concepts/versioning-and-stability), the contract is additive within a version: existing attributes are never renamed or removed, and new ones can appear at any time without warning (deserialize leniently; don't assert against the *whole* schema, only the fields you use). The check above only ever fails because of something on **your** side — a refactor that stopped sending a field you meant to keep, a typo in an attribute name, a resource you thought was writable that isn't. That's still worth catching in CI rather than in production.

## Replay production-shaped traffic before go-live

Correctness on ten records doesn't tell you what your integration does at real volume and real pacing. Before pointing at a live clinic, capture the request pattern your integration will actually send — the mix of reads and writes, how bursty it is, how large your typical pages are — and run it against your developer account at that shape and rate. Watch `X-RateLimit-Remaining` and the account's **Developers → API Logs** while it runs. This catches a different class of bug than functional testing does: a sync that bursts its whole minute's budget in the first second of every hour instead of pacing steadily, a retry loop that hammers a `429` instead of backing off, a page walk that re-fetches instead of using a cursor. See [Best practices → Rate limits and retries](/guides/best-practices#rate-limits-and-retries) and [Rate limits](/concepts/rate-limits) for the numbers to design against.

## What you cannot test

Be honest with yourself about the edges of what the API — and therefore any amount of testing against it — can cover:

* **Card payments are app-side.** The API deliberately refuses to record a payment against a card-charging method (`422 card_payment_not_allowed`) — a real card is only ever charged through PracticeHub's own terminal flow, in the app, not through the API. There's nothing to rehearse here through a developer account; if your integration touches card payments at all, that part of the flow is tested in PracticeHub itself, not against the API.
* **Webhooks cover patients and appointments.** Open **Developers → Webhooks** on your test account, add your endpoint and use the portal's test event and delivery log to exercise your receiver — see [Webhooks](/guides/webhooks). Every other resource is still polled with `updated_at` filters and `deleted_entities`.

Once your integration passes all of the above, work through the [Launch checklist](/guides/launch-checklist) before it touches a real clinic.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.