Keeping your data in sync

Many integrations keep their own copy of some Provet data, such as clients, patients, appointments or invoices. This page describes how to keep that copy up to date with as few requests as possible. Integrations that follow it get faster responses, stay well within the rate limits, and put less load on the clinics they serve.

In short:

  • Read everything once, then on each run ask only for what changed since your last successful run.

  • Follow the next link from page to page, with pages of 50 to 200 records.

  • Use webhooks to hear about changes as they happen, and keep a “changed since” run as a backstop.

The examples on this page leave out the start of the address. Real requests include your organization’s ID: https://provetcloud.com/<provet_id>/api/0.1/client/.

Sync only what changed

The list endpoints for clients, patients, appointments, reminders, consultations, invoices, invoice rows and invoice payments accept a modified__gte filter: “records changed on or after this time”.

  1. First run. Note the time, then read the whole list once. On a large list, read it in id order rather than by page number (see Read a large list by id).

  2. Every run. Note the time when the run starts. Ask only for records changed since the start of your last successful run:

    GET /api/0.1/client/?modified__gte=2026-10-21T02:55:00Z

  3. When the run has finished, save its start time for the next run. If a run fails, don’t save it, so the next run covers the same period again.

A few things keep this from missing records:

  • Overlap. Start each window a few minutes earlier than the last run’s start time. Records saved while your previous run was reading would otherwise be missed, and list reads can lag behind the latest writes by no fixed amount. A few minutes is usually enough, but no overlap is guaranteed to catch everything.

  • Duplicates are fine. Because of the overlap, some records come back twice. Save each record under its id, updating it if you already have it, so that seeing one twice does no harm. Reminders and unallocated payments have no id field; use the number at the end of their url instead, here and below.

  • Keep the newer copy. A record can occasionally come back older than a copy you already have, because list reads can lag. Compare modified and keep the copy with the later time. Custom field values don’t include modified; replace your copy with the one you read most recently.

  • Act once per change. If your system does something for each record, such as sending a message, tie it to the record’s id and modified time, not to the run that returned it. Otherwise the overlap makes it happen twice.

  • Clocks. The start time comes from your clock and modified from ours, so keep your server’s clock synchronized.

  • A full read now and then. If your copy must be exact, read the full list again from time to time, for example once a month, outside clinic hours. It is the only way to catch the changes listed under What a “changed since” request doesn’t show.

Warning

Don’t send the same fixed date on every run, such as modified__gte=1901-01-01, and don’t re-read a fixed window, such as “the last 30 days” or “the next two weeks”, on every run. Both download the same records again each time. Use the time of your last successful run instead.

Send times with a time zone

Include a time zone in every date filter, either Z for UTC or an offset such as +03:00. A time without one is read in the time zone set for the organization in Provet, not in UTC. In a URL, write the + of an offset as %2B. An unencoded + is read as a space, so the time can be read wrongly and the request returns the wrong records, without an error:

GET /api/0.1/appointment/?modified__gte=2026-10-21T05:55:00%2B03:00

Check the spelling of filters

A field or method the endpoint doesn’t support for filtering is ignored rather than refused, so a misspelled filter, such as modifed__gte, returns the list as if it weren’t there: every record you have access to. A value the endpoint can’t read, such as a malformed date or text on a number field, is refused with an error, and so is a method used on a related object instead of one of its fields, such as phonenumbers__icontains. The phone number list (/api/0.1/phonenumber/) has no modified filter at all. If a “changed since” request returns as many records as your first full run, check the filter. See Query filtering for the filters each endpoint supports.

What a “changed since” request doesn’t show

A record’s modified time changes when that record is saved. Some data is stored separately, and changing it on its own doesn’t save the record it belongs to. A “changed since” request may not return:

  • a client whose phone numbers were added or edited. Subscribe to the “Phone number” webhook to hear about these.

  • a client or patient whose custom field values changed. Read changed values from their own list instead: GET /api/0.1/custom_field_values/?modified__gte=2026-10-21T02:55:00Z. Each value names its field and the object_id of the record it belongs to. The field’s content_type in /api/0.1/custom_fields/ says whether that is a client, a patient or a consultation.

  • a patient whose weight was added or edited.

  • a few changes made in bulk by Provet itself, such as an invoice’s due_sum being recalculated after a card terminal payment, or patients, notes, consultations, appointments and invoices moved to the other client when duplicate clients are merged.

A daily “changed since” run on the client or patient list doesn’t pick these up either.

Records that are removed show up in different ways:

  • Clients and patients are archived, not deleted: archived becomes true, and the record is returned as changed.

  • Appointments that are cancelled stay in the list with active set to 0, and the “Appointment delete” webhook is sent.

  • Reminders, phone numbers, invoice rows, consultations and draft invoices are deleted, and so are appointment bookings that were never confirmed. Voided draft invoices also disappear from the API. They no longer appear in the list, so a “changed since” request can’t show them. The “Reminder delete”, “Invoice row delete” and “Consultation delete” webhooks report those deletions. Deleting a draft invoice sends the regular “Invoice” webhook, after which the invoice can no longer be read. Phone numbers and never-confirmed bookings have no delete webhook.

  • A client whose data is erased on request disappears from the API, so it can’t appear in a “changed since” request. The “Client delete” webhook reports it.

See List of Webhook Triggers for the full list of webhooks.

Paging

Most list endpoints return 50 records per page by default. Ask for up to 1000 with page_size (see Paginated results).

Follow next

Read page after page by following the next link in each response until it is null. Don’t work out page numbers yourself from count or num_pages: on some lists these totals can be a few minutes old. If a request is refused with HTTP 429, wait the number of seconds in its Retry-After header and request the same link again; there’s no need to start the run over.

Deep pages get slower

Pages are numbered, not linked by a cursor, and deep pages can be much slower to return than the first ones. When a page starts more than 10,000 records into a list, the response carries an X-Pagination-Warning header. Read a smaller set, with modified__gte or a date filter, or read the list by id as below, rather than paging through the whole list.

Read a large list by id

Lists are sorted by id. To read a large list, such as on the first run, ask for the records after the last id you have, and always read the first page:

GET /api/0.1/client/?id__gt=0&page_size=200

GET /api/0.1/client/?id__gt=<largest id on the previous page>&page_size=200

Stop when a page comes back with no results. Every request is as fast as the first, and records that change or are deleted while you read don’t make you skip others, as page numbers can: when a record on a page you’ve read drops out of the list, the rest move up. Records added while you read may be missed, so note the start time before the first page; the next “changed since” run then picks them up.

The client, patient, consultation, invoice, invoice row and invoice payment lists accept id__gt. The appointment and reminder lists don’t; read them in parts by created time instead, such as one month at a time, since a record’s created time never changes. A list that doesn’t support id__gt ignores it and returns the same first page every time, so stop if a page holds no id larger than the last one you had.

Page size and the rate limit

A list request counts against the rate limit in steps of the default page size: a page of up to 50 counts once, a page of 51 to 100 counts twice, and a page of 1000 counts as 20 requests, even when fewer records come back. Where an endpoint caps the page size, the capped size is what counts. Large pages don’t let you read more records per minute, but each one takes much longer to build and to receive. Pages smaller than 50 use up the limit faster for the same number of records.

Pages of 50 to 200 records are a reasonable starting point for most integrations.

Use webhooks, with a backstop

Webhooks tell your system that a record changed, usually within moments, so you don’t have to poll every few minutes. Delivery time isn’t guaranteed, though. A webhook delivery counts as received only when your endpoint answers with HTTP 200; other answers, including 201 and 204, are retried. After 10 attempts in total the delivery is dropped (after 3 for some fiscal registration webhooks). Keep a “changed since” run, for example once a day, to pick up records whose change a webhook missed. It doesn’t replace webhooks for the changes a “changed since” request doesn’t show, such as phone numbers and deleted records. Archived clients and patients, and cancelled appointments, do show up in it.

Do and don’t

Don’t

Do

Send modified__gte=1901-01-01&page_size=1000 every night

Send modified__gte=<start of your last successful run, minus a few minutes>

Re-read a fixed window, such as the last 30 days, on every run

Ask for what changed since your last successful run

Download the whole list, unfiltered, on every run

Read it once, then use modified__gte

Work out page numbers from count or num_pages

Follow next until it is null

Ask for page_size=1 to read the total before every run

Start reading; follow next

Use page_size=1000

Use pages of 50 to 200 records

Loop over every client with patient/?client__is=<id>

Read patient/?modified__gte=<time> once

Send times without a time zone

Send Z or an offset, written %2B in a URL

Overwrite your copy with whatever arrives last

Keep the copy with the later modified time

Rely on webhooks alone

Use webhooks and a daily modified__gte run