Learn
Guides & concepts
The conventions that hold across every resource. Learn these once and the whole API becomes predictable.
These conventions — auth, paging, filtering, IDs, dates and errors — are shared
by both APIs. Examples lean on PNE resources like customers; the same
patterns apply to whichever resources your API exposes (VISH's catalog is a
focused subset). Where something is PNE-only,
it's flagged inline.
Request basics
- Protocol: REST over HTTPS; resources under
/api/v1. - Format: JSON request and response bodies. Send
Content-Type: application/jsononPOSTandPUT. - Verbs:
GETreads,POSTcreates,PUTupdates,DELETEremoves. - Auth: a gateway
apikey(query string) plus HTTP Basic credentials on every request — see Authentication. - Field naming: JSON properties are
PascalCase(e.g.FirstName,CompanyId).
Pagination
List endpoints (those returning collections) page their results using two query parameters:
| Parameter | Type | Default | Meaning |
|---|---|---|---|
Page | integer | 1 | 1-based page number. |
Rows | integer | 20–30 | Rows per page. The default varies by resource (most 20, some 30) and is capped at 100 — higher values are clamped down. |
Walk a collection by incrementing Page until a page returns fewer
rows than you asked for (or none):
def iter_customers(session, base, rows=50, **filters):
page = 1
while True:
res = session.get(f"{base}/customers",
params={"Page": page, "Rows": rows, **filters})
res.raise_for_status()
batch = res.json()
if not batch:
return
yield from batch
if len(batch) < rows: # last page
return
page += 1async function* iterCustomers(base, headers, rows = 50, filters = {}) {
let page = 1;
for (;;) {
const qs = new URLSearchParams({ Page: page, Rows: rows, ...filters });
const batch = await (await fetch(`${base}/customers?${qs}`, { headers })).json();
if (!batch.length) return;
yield* batch;
if (batch.length < rows) return; // last page
page++;
}
}You don't have to page blindly: most list responses include a set
of Data* headers describing where you are in the
result set. Read them to size your loop or build a pager.
| Response header | Meaning |
|---|---|
DataTotalRows | Total rows across the whole result set (matching your filters). |
DataTotalPages | Total pages at the current Rows size. |
DataCurrentPage | The page you just received. |
DataNextPage | Next page number (clamped to the last page). |
DataPriorPage | Previous page number (clamped to 1). |
Larger pages mean fewer round-trips but heavier responses. Start around 50 and
tune. You can loop until a short/empty page (as above) or use
DataTotalPages / DataTotalRows to know the total up
front — note these are HTTP headers, not fields in the JSON
body. A few endpoints (notably giftcertificates) return a bare JSON
array without these headers, so don't rely on them being present — page
until a short/empty page as a fallback.
Filtering & search
Many list endpoints accept filter parameters in the query string. They vary by resource, but common ones include:
| Parameter | Applies to | Example |
|---|---|---|
FirstName, LastName, Email, MobilePhone | Customers | ?LastName=Smith |
CompanyId | Most resources | ?CompanyId=ENCRYPTED_ID |
IsActive | Customers, products, services, employees… | ?IsActive=true |
StartDate, EndDate | Appointments, schedules, time cards | ?StartDate=2026-06-01&EndDate=2026-06-30 |
ModifiedOnStart, ModifiedOnEnd | Customers, appointments, products, services… | Incremental sync — see below |
ModifiedOn*
To keep a local copy in sync without re-reading everything, store the timestamp
of your last sync and pass it as ModifiedOnStart to pull only
records changed since then. Combine with pagination to page through the deltas.
Resource IDs
IDs returned by the API (such as CustomerId, CompanyId,
LeadSourceId) are opaque, encrypted, URL-safe strings
— not raw database integers.
- Treat them as opaque tokens. Don't parse, increment, or construct them. Pass them back exactly as received.
- They're stable for a given record, so you can store them as foreign keys in your own system.
- Related IDs nest naturally: a customer's
CompanyIdcan be used directly anywhere a company ID is expected.
// A customer ID looks like an opaque string, not a number:
{ "CustomerId": "9d4Kx2...", "CompanyId": "Aa01...", "FirstName": "Ada" }
// Use it directly in the next call (these customer sub-resources use POST):
POST /api/v1/customers/9d4Kx2.../AppointmentsDates & times
- Date and date-time fields are returned as strings. Many filters accept a
plain
YYYY-MM-DDdate. - Nullable dates (e.g.
Birthday,LastVisit) may benullwhen unset — guard for that in your client. - When sending a date range, pass both
StartDateandEndDateunless the endpoint says one is optional.
Companies & locations
A PatientNow Essentials account can span multiple companies/locations. Most
resources accept a CompanyId filter so you can scope reads and writes
to a specific location. Fetch the list of companies from
GET /api/v1/companies and pass
the relevant CompanyId through to other calls.
Memberships vs. membership programs PNE
These are two different resources, and mixing them up is a common source of "the name comes back blank" confusion:
| Resource | What it is | Key fields |
|---|---|---|
membershipprograms | The catalog — the plans you offer. | MembershipProgramId, ProgramName, Description |
memberships | A customer's enrollment in a plan. | MembershipId, CustomerId, MembershipProgram (name string), BillAmount, NextBillDate, LastBillDate, SoldOn |
It lives on membershipprograms as
ProgramName. The memberships (enrollment) resource has
no ProgramName field — only a MembershipProgram name
string for display — so reading enrollments alone makes plan names look empty.
Fetch GET /api/v1/membershipprograms for the catalog names. Note
there is no sign-up/enrollment fee field in the API;
memberships exposes only the recurring BillAmount.
Errors & status codes
The API uses standard HTTP status codes. The status line is the primary signal; a JSON body may accompany errors with more detail.
| Code | Meaning | What to do |
|---|---|---|
200 OK | Success — including reads, updates, and creates. | Parse the body. On a create, read the new record's ID from the body (e.g. {"id": "…"}), then fetch the full record if needed. |
400 Bad Request | Validation failed — missing or invalid fields. | Fix the payload; check required fields in the reference. |
401 Unauthorized | Missing or invalid credentials. | See 401 pitfalls. |
404 Not Found | No such record, or not visible to this user. | Re-check the ID and the authenticated user's access. |
429 Too Many Requests | You've exceeded your plan's request quota for the current window. | Back off and retry later — see Rate limits. |
500 Server Error | Unexpected server-side failure. | Retry with backoff; if it persists, contact support with the request details. |
Always branch on the status code before parsing. A non-2xx response may have an
empty or non-JSON body. For a missing record you'll typically get
404 rather than a 200 with null.
Rate limits
Requests are throttled at the gateway. Your gateway apikey identifies
your integration's app, and each app is tied to a plan
that sets how many requests you may make per window. Exceed it and the gateway
returns 429 Too Many Requests until the window resets — your credentials
are still valid; you're simply over quota.
| Plan | Quota |
|---|---|
| Standard (default) | 1,000 requests per day |
| Higher tiers | Larger daily quotas (e.g. 2,500/day) for higher-volume integrations |
The standard plan allows 1,000 requests per day. If your integration needs more, higher-volume plans are available — contact your PatientNow representative to have your app moved to a larger quota.
The same habits that make a well-behaved client also keep you comfortably within your plan:
- Use
ModifiedOn*filters for incremental sync instead of full re-reads — far fewer calls. - Request reasonable page sizes; don't burn quota on tiny pages.
- Cache rarely-changing reference data (e.g. service types, lead sources).
- On
429, back off and retry after a delay rather than looping immediately. Retry5xxresponses with exponential backoff too — never in a tight loop.
Webhooks (events) PNE
Rather than polling for changes, you can register webhooks under
api/v1/events/webhooks to be
notified when data changes. Manage your registered webhooks through that resource.
Webhooks are part of the full PatientNow Essentials API. The VISH subset doesn't
expose an events resource — VISH clients poll with the ModifiedOn*
filters above for sync.