Handling Orders
Note
Part of the Wholesaler Integrations guide. This step assumes you have already completed Registering an Available Wholesaler — orders only arrive for clinics that have connected your wholesaler.
See Create an Order for how a
clinic places the purchase order that triggers order.placed below.
order.placed event reference
Note
This event is also published in the API reference, whose machine-readable payload schema you can generate a client from.
Provet POSTs this event to
available_wholesaler.webhook_callback_url when clinic staff send a
purchase order to a wholesaler of type Integration created from one of
your AvailableWholesaler registrations. It does not fire for
wholesalers of any other type (e.g. Email).
Which clinic locations can order depends on how the clinic set up its
credentials (see Registering an Available Wholesaler). With clinic-wide
credentials, every location can order. With per-location credentials, only
the locations that entered their own can: Provet doesn’t let the
other locations choose your wholesaler for an order or send one to you, so
no order.placed arrives from a location you never received a
wholesaler.created for.
When the order is sent, Provet optimistically marks it as ordered and delivers this event; the payload is richer than a typical order notification so you can act on the order without follow-up calls. Whether the order reached you is tracked on the order, and once you know whether you can fulfil it, you answer with an accept or a reject — see Accepting or rejecting an order.
Warning
order.placed may be delivered more than once for the same
order.id — either as a redundant retry of the same event
(identified by a repeated event_id, safe to drop), or as a genuinely
new event for the same order under a different event_id. The
latter doesn’t necessarily mean a brand new order was placed again —
the clinic may simply have updated the existing order. Use event_id
only to discard redundant deliveries; when a new event_id arrives
for an order.id you have already processed, compare the payload
against the version you are holding and act on whatever changed. Each
delivery carries the complete order, so the payload alone tells you
what is different.
{
"event": "order.placed",
"event_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"available_wholesaler": {"id": 7, "name": "Acme Veterinary Supplies"},
"wholesaler": {"id": 123, "customer_number": "CLINIC-42"},
"order": {
"id": 5567,
"notes": "Please deliver before noon",
"order_date": "2026-10-27T09:15:00+00:00",
"total_excl_vat": "42.00",
"customer": {
"name": "Downtown Vet Group",
"department": "Main Clinic",
"postal_address": "12 Market Street",
"postal_code": "00100",
"city": "Helsinki",
"country_code": "FI"
},
"purchaser": {
"name": "Jane Doe",
"phone": "+358401234567",
"email": "jane.doe@downtownvet.example"
},
"items": [
{
"product_number": "ACM-001",
"product_name": "Amoxicillin 250mg",
"quantity": "12.00",
"unit": "tablet",
"barcode": "6412345678901",
"notes": null,
"unit_price": "3.50",
"line_total": "42.00"
}
]
}
}
Field |
Type |
Optional |
Meaning |
|---|---|---|---|
|
string |
No |
Always |
|
string |
No |
UUID identifying this event; stable across delivery attempts. See deduplication rules. |
|
number |
No |
ID of your registered |
|
string |
No |
Name of that |
|
number |
No |
The same |
|
number |
No |
Per-clinic |
|
object |
No |
The ordering clinic’s billing/delivery identity. |
|
object |
No |
The user who sent the order ( |
|
array |
No |
Order lines: |
|
string |
No |
Wholesale price per unit, excluding VAT. Decimal string. |
|
string |
No |
|
|
string |
No |
When the order was placed, ISO 8601; falls back to the order’s creation time. |
|
string |
No |
Sum of every item’s |
|
string / null |
Yes |
The clinic’s customer number with you; department override if set, else wholesaler-global; |
|
string / null |
Yes |
Free-text note the clinic added to the order. |
Accepting or rejecting an order
Every order sent to you carries a transmission_status that tells clinic
staff where it stands. Provet sets the first values from the
order.placed delivery; accepted and rejected are yours to set:
Value |
Set by |
Meaning |
|---|---|---|
|
— |
Not tracked: the order was not sent to you through the API, or was sent before tracking was enabled for the clinic. |
|
Provet |
|
|
Provet |
Your endpoint answered |
|
Provet |
The order could not be submitted: the retries ran out, your callback URL was invalid or redirected, or no outcome was recorded within 30 minutes. |
|
You |
You will fulfil the order. |
|
You |
You will not fulfil the order; |
Answer every order you receive, as soon as you know whether you can fulfil it. Until you do, clinic staff can only see that the order reached you, not whether it will arrive:
curl -X POST \
"https://[env.]provetcloud.com/<provet_id>/api/0.1/order/<order_id>/transmission_status/" \
-H "Authorization: Bearer <oauth_access_token>" \
-H "Content-Type: application/json" \
-d '{"status": "rejected", "reason": "Your account is on credit hold.", "instructions": "Call our accounts team on +358 9 123 4567."}'
{
"transmission_status": "rejected",
"transmission_status_reason": "Your account is on credit hold.",
"transmission_status_instructions": "Call our accounts team on +358 9 123 4567."
}
Field |
Type |
Optional |
Meaning |
|---|---|---|---|
|
string |
No |
|
|
string |
Yes for |
Why you reject the order, up to 1000 characters. Clinic staff read it, so write something they can act on ( |
|
string |
Yes |
What the clinic should do next, up to 2000 characters: when to expect the delivery, whom to call, what to change before ordering again. |
The wholesaler scope is sufficient — no additional Provet
permission is needed.
Your own orders only. An order placed with a wholesaler belonging to another integration, or with no integration wholesaler behind it, is rejected with 403 (
Token does not match this wholesaler.).You can answer an order that is
pending,submittedorerror. Answering while it is stillpendingis fine: the submission outcome recorded afterwards never overwrites your answer. Answering anerrororder tells the clinic it reached you after all.An
unsetorder is not tracked — it was not sent to you through the API, or was sent before tracking was enabled for the clinic — so it cannot be answered: 409.Transmission status is being enabled clinic by clinic. Until it is enabled for a clinic, the call answers 404 there, and that clinic’s orders stay
unset.
Your answer is final. Sending the answer the order already carries
changes nothing and returns the same 200 with the values stored the
first time, so a timed-out call is safe to retry; a different reason
or instructions in the retry is ignored. Sending the other answer
returns 409. The answer also survives the clinic re-sending the order:
a repeated order.placed for an order you already answered (see the
warning above) cannot be answered differently.
Do not mark delivered an order you rejected: Provet does not stop you, and the clinic would then see an order both rejected and delivered.
GET /order/<order_id>/ returns the same three fields, so you can read
back what an order currently shows. Every change of transmission_status,
yours or Provet’s, also updates the order’s modified
timestamp, so integrations that sync orders with modified__gte pick it
up.
Recording your own order number
When you accept an order you normally assign it a supplier order number of your own. Write it back so clinic staff can quote it to you and see it on the order:
curl -X POST \
"https://[env.]provetcloud.com/<provet_id>/api/0.1/order/<order_id>/set_wholesaler_reference_number/" \
-H "Authorization: Bearer <oauth_access_token>" \
-H "Content-Type: application/json" \
-d '{"wholesaler_reference_number": "SO123456"}'
{
"status": "ok",
"wholesaler_reference_number": "SO123456"
}
The wholesaler scope is sufficient — no additional Provet
permission is needed. The value is limited to 63 characters and appears on
the order in Provet as its Wholesaler invoice number.
Your own orders only. An order placed with a wholesaler belonging to another integration, or with no integration wholesaler behind it, is rejected with 403 (
Token does not match this wholesaler.).The order must already be placed; one still in Ordering in process is 400.
Last write wins. If the order already carries a reference number — clinic staff can enter one themselves before placing the order — yours replaces it. The response always echoes the value now stored.
The call is safe to retry: sending the same value again changes nothing and answers the same 200.
Marking an order delivered
Once you have shipped an order received via order.placed, move it out
of Ordered yourself:
curl -X POST \
"https://[env.]provetcloud.com/<provet_id>/api/0.1/order/<order_id>/mark_delivered/" \
-H "Authorization: Bearer <oauth_access_token>"
The wholesaler scope is sufficient — no additional Provet
permission is needed. The call runs exactly what the clinic’s own “Mark
products delivered” button runs:
{
"status": "ok",
"order_status": 2,
"all_delivered": true,
"delivery_date": "2026-08-12T09:15:00Z"
}
Your own orders only. An order placed with a wholesaler belonging to another integration, or with no integration wholesaler behind it, is rejected with 403 (
Token does not match this wholesaler.).The order must be in Ordered status; anything else is 400.
transmission_status, set_wholesaler_reference_number and
mark_delivered are the only order actions the wholesaler scope
opens up. Every other order
operation stays behind the clinic’s own permissions.
Retrying a call that timed out
The call is not idempotent: it only moves an order out of Ordered, so calling it a second time on an order it already moved answers 400, not a repeat of the original success.
That makes a timed-out request safe to retry, as long as you read the failure correctly:
400 with
Order must be in 'Ordered' status to mark products delivered.— the order is no longer in Ordered, so your earlier call landed. Treat your retry as complete; there is nothing further to do.Any other 400 — a genuine failure, reporting a problem with the order’s contents. The order is still in Ordered and still awaiting delivery, so this one does need your attention. Do not treat it as success.
Concurrent duplicate calls are safe: the order row is locked for the duration, so two requests arriving together are applied one after the other, and the second gets the 400 above rather than repeating the stock changes.
Listing orders with undelivered webhooks
Warning
This is a fallback for when webhooks have failed, not a replacement
for normal order processing. Act on order.placed as it arrives; do
not poll this endpoint routinely to discover orders.
If order.placed could not reach your callback URL after every retry —
your endpoint was down, or timed out without answering within 30 minutes —
you never learn an order exists, and you have no order.id to look it up
with. Use this endpoint to reconcile after an incident, once your endpoint
is back up:
curl -X GET \
"https://[env.]provetcloud.com/<provet_id>/api/0.1/order/undelivered/" \
-H "Authorization: Bearer <oauth_access_token>"
The wholesaler scope is sufficient — no additional Provet
permission is needed. The response is a paginated list of your own orders
whose transmission_status is error, with a transmission_status_reason
of either submission_failed (retries ran out, or your callback URL was
invalid or redirected) or submission_timed_out (no outcome was recorded
within 30 minutes — likely your own downtime recovering). Each order in the
list carries the same fields as GET /order/<order_id>/, including
transmission_status_reason, so you know which of the two happened
without a follow-up call.
Your own orders only. Orders placed with another integration’s wholesaler, or with no integration wholesaler at all, never appear here.
An order that reached you after all (
transmission_statusmoved tosubmitted) drops out of this list; so does one you already answered withtransmission_status.Transmission status is being enabled clinic by clinic. While it is not enabled for a clinic, that clinic’s orders stay
unsetand never appear here either.
Errors specific to orders
Situation |
Response |
|---|---|
|
403 |
|
400 |
|
409 |
|
409 |
|
404 |
|
400 |
|
400 |
|
400 |
See the error reference for the integration-specific errors that can occur across every phase.