Developers

Learn

Guides & concepts

The conventions that hold across every resource. Learn these once and the whole API becomes predictable.

ℹ️ Applies to both PNE and VISH

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

Pagination

List endpoints (those returning collections) page their results using two query parameters:

ParameterTypeDefaultMeaning
Pageinteger11-based page number.
Rowsinteger20–30Rows 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 += 1
async 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 headerMeaning
DataTotalRowsTotal rows across the whole result set (matching your filters).
DataTotalPagesTotal pages at the current Rows size.
DataCurrentPageThe page you just received.
DataNextPageNext page number (clamped to the last page).
DataPriorPagePrevious page number (clamped to 1).
📎 Choose a sensible page size

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:

ParameterApplies toExample
FirstName, LastName, Email, MobilePhoneCustomers?LastName=Smith
CompanyIdMost resources?CompanyId=ENCRYPTED_ID
IsActiveCustomers, products, services, employees…?IsActive=true
StartDate, EndDateAppointments, schedules, time cards?StartDate=2026-06-01&EndDate=2026-06-30
ModifiedOnStart, ModifiedOnEndCustomers, appointments, products, services…Incremental sync — see below
ℹ️ Incremental sync with 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.

// 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.../Appointments

Dates & times

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:

ResourceWhat it isKey fields
membershipprogramsThe catalog — the plans you offer.MembershipProgramId, ProgramName, Description
membershipsA customer's enrollment in a plan.MembershipId, CustomerId, MembershipProgram (name string), BillAmount, NextBillDate, LastBillDate, SoldOn
ℹ️ Looking for the plan's name?

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.

CodeMeaningWhat to do
200 OKSuccess — 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 RequestValidation failed — missing or invalid fields.Fix the payload; check required fields in the reference.
401 UnauthorizedMissing or invalid credentials.See 401 pitfalls.
404 Not FoundNo such record, or not visible to this user.Re-check the ID and the authenticated user's access.
429 Too Many RequestsYou've exceeded your plan's request quota for the current window.Back off and retry later — see Rate limits.
500 Server ErrorUnexpected server-side failure.Retry with backoff; if it persists, contact support with the request details.
📎 Build defensively

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.

PlanQuota
Standard (default)1,000 requests per day
Higher tiersLarger 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.

📎 Stay under your 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. Retry 5xx responses 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.

📎 PNE only

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.