Skip to content

Purchases and customers

What a clinic's storefront sold, to whom, what was refunded or disputed, which affiliate it is credited to, and how to keep your own copy in step.

A purchase is one paid charge to a customer: a storefront checkout, one charge of a membership, or a visit bought on its own. A customer is one storefront account and what it has bought. Both are read-only here: the storefront and its payment processor own every transition, and you hear about them as events.

Every operation on this page needs purchases:read. A clinic grants it separately from the clinical scopes, so a back office that settles money can be given it without patients:read.

Listing purchases

curl "https://api.cuvo.co/v1/purchases?affiliate=aff_3c9d00e5&updated_after=2026-11-01T00:00:00Z" \
  -H "Authorization: Bearer cuvo_sk_live_…"
{
  "data": [
    {
      "id": "pur_7c1f9a2e",
      "kind": "treatment",
      "customer": "cus_4b8e0d17",
      "patient": "pat_9f2c1a7b",
      "affiliate": "aff_3c9d00e5",
      "referral": "afr_51d2c8aa",
      "case": "case_7d1e4b2a",
      "order": "ord_0e6b93c4",
      "membership": null,
      "cycle": null,
      "product": { "id": "prod_2a7f", "variation_id": null, "purchase_option": "monthly", "rx": true },
      "amount_cents": 29900,
      "refunded_cents": 0,
      "currency": "USD",
      "discount": { "code": "SPRING", "amount_cents": 3000 },
      "payment_status": "paid",
      "dispute": null,
      "processor": "payengine",
      "transaction_id": "txn_8841",
      "fulfillment_status": "shipped",
      "follow_up_due_at": "2027-02-02T15:04:00Z",
      "created_at": "2026-11-02T15:04:00Z",
      "paid_at": "2026-11-02T15:04:00Z",
      "refunded_at": null,
      "updated_at": "2026-11-03T09:12:00Z"
    }
  ],
  "has_more": false
}
  • customer, patient, affiliate, kind, payment_status, created_after and updated_after filter the list.
  • limit and starting_after page it, newest first. See Pagination.
  • GET /v1/purchases/{id} reads one.

What a purchase says

kind is visit for a consult bought on its own, membership_cycle for one charge of a membership (membership and cycle say which), and treatment for every other checkout. A membership charge that wrote no storefront order has an id that starts pur_mc_; treat it like any other id.

amount_cents is what the customer was charged, in integer cents of currency. discount is the code applied at checkout, and its amount_cents is null when the ledger did not record the amount taken off.

payment_status is paid, then partially_refunded or refunded as money goes back. refunded_cents is the total given back and refunded_at the latest refund. A chargeback sets dispute, with its amount and when it was opened, and never changes payment_status: a disputed charge stays paid until a refund says otherwise. Its status is open, because the ledger records that a chargeback was opened, not how it ended; treat any other value as one added later. A read never calls the processor; these fields are the platform's own ledger.

case is the case the checkout opened, order the latest pharmacy order raised from it, and fulfillment_status where that stands in the storefront's words. All three are null for a purchase with no case. product is null for a charge that names no product.

Which affiliate a sale is credited to

affiliate is the in-house affiliate the sale is credited to, by the partner program's own rule. When an acquisition has been decided for the customer, it decides: its affiliate, or no affiliate when an outside tracking network won the customer. Until one is decided, the customer's referral decides. referral is the customer's referral either way, so a purchase with a referral and a null affiliate is a customer an affiliate referred whose sale a network was credited with.

A customer carries the same referral and affiliate, so GET /v1/customers?affiliate= is an affiliate's book of business and GET /v1/purchases?affiliate= is what that book has bought.

Customers

curl "https://api.cuvo.co/v1/customers?active=true" \
  -H "Authorization: Bearer cuvo_sk_live_…"

A customer is ids and totals: patient once the account is linked to a chart, referral and affiliate, first_order_at and last_order_at, order_count, lifetime_amount_cents (every charge, before refunds) and refunded_amount_cents. active is true while the customer has a paid purchase in the last 90 days or a membership that is active.

No name, email or address is on a customer. Who the person is belongs to the patient, read through patient under patients:read.

affiliate, patient, active, created_after and updated_after filter the list. GET /v1/customers/{id} reads one.

Keeping your copy in step

Events tell you something moved; the list tells you everything that did. Use both.

  1. Subscribe to purchase.paid.v1, purchase.refunded.v1 and purchase.disputed.v1. Each names the purchase. Read it with GET /v1/purchases/{id} and store it whole, because updated_at moves on every change and a later read always supersedes an earlier one.
  2. On a schedule, list GET /v1/purchases?updated_after=<the newest updated_at you have stored> and page until has_more is false. Store every row, then move your cursor to the newest updated_at among them. The list is ordered by when a purchase was made, not by when it changed, so read the whole window before you move the cursor.
  3. Do the same with GET /v1/customers?updated_after= for the totals.

A delivery that arrives late or twice is harmless under this loop: you store what the API says now, never what the event implied.

In test mode

The sandbox sells the four products of Products and has no storefront, so you record the checkouts yourself. All three controls need test:manage.

curl https://api.cuvo.co/v1/test/purchases \
  -H "Authorization: Bearer cuvo_sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{ "product": "prod_test_semaglutide", "variation_id": "starter", "patient": "pat_9f2c1a7b", "affiliate": "aff_test_3c9d", "coupon_code": "SPRING" }'
  • product is required. variation_id and purchase_option choose among the product's options, the first option by default, and amount_cents defaults to that option's price.
  • With no customer, the patient's existing customer is used, or a new one is made. A customer you name must belong to the patient you name, if you name both.
  • affiliate must be a sandbox affiliate. A customer with no referral gets one, crediting that affiliate, attributed_via coupon when you send a coupon_code and cookie otherwise. A customer already credited to another affiliate is refused.
  • The response is the purchase, paid. The sandbox writes affiliate_referral.created.v1 when it wrote a referral, then purchase.paid.v1.

POST /v1/test/purchases/{id}/refund gives back amount_cents, or everything not yet refunded, and writes purchase.refunded.v1. POST /v1/test/purchases/{id}/dispute opens a chargeback for amount_cents, or everything not yet refunded, and writes purchase.disputed.v1. A purchase can be disputed once, and a purchase refunded in full has nothing left to refund or dispute.

A sandbox purchase has no case, order or membership, processor is sandbox, and test mode writes no charges for any of it.