A REST API over the winery's catalog, customers, club, online orders, register sales, inventory and pack queue. v1 is frozen: a field that ships is never removed or retyped, only added to. Reads cover every resource; writes cover products, customers, orders, stock adjustments, bookings and shipment tracking, and nothing in v1 deletes anything. Authentication is one header, ids are opaque strings, money is always cents plus a currency, and every list is cursor-paginated.
GET /v1/market — one call tells you whether this winery collects a province or a state, what to call the postal field, which currency its money is in, what its legal drinking age is and which subdivisions it may ship to. Write one integration; it works for a winery in Kelowna and one in Walla Walla with no country conditional in your code.
POST /v1/shipping/quote — may this winery ship this many bottles to this destination today, and what would it cost? A destination it may not ship to answers 200 with allowed: false and a reason. Ask before you sell, not at checkout.
Feed it to openapi-generator for a typed client, or import it into Postman or Insomnia. It is generated from the running router, so it describes what is deployed rather than what was last written down.
curl -s https://winery.sipspos.com/api/v1/market \
-H "Authorization: Bearer sk_live_…"
403 feature_not_enabled rather than failing quietly.Ask for a test key first. It reaches a separate copy of a winery — invented wines, customers, club and orders — and it cannot see or touch the real one.
curl -si https://winery.sipspos.com/api/v1/customers \
-H "Authorization: Bearer sk_test_…"
HTTP/1.1 200 OK
X-Sipspos-Environment: test
POST /v1/orders genuinely writes an order and you genuinely read it back. The write path is the half worth rehearsing, and a recorded fixture can't rehearse it.X-Sipspos-Environment: test or live. Read from the tenant that served the request, not from the token you sent, so it is worth asserting on in your test suite.sk_live_ key still requires it.Two complete applications, both standard-library Python, both run against this API on every build — so neither can quietly drift away from the documents on this page.
A wine-club retention dashboard: which members are failing billing, which have no card on file, which have gone quiet, and which have already left. A real application a winery would run, and also the reference client — sipspos.py is the part you copy, and it knows nothing about wine clubs.
Bearer auth, cursor pagination, the error envelope as a typed exception, X-Sipspos-Environment, and 429 backoff with Retry-After — the five things every integration has to get right and which are tedious to rediscover.
A delivery log for the pack bench, and the reference receiver. Verification is the part of an integration where getting it nearly right is indistinguishable from getting it right, until somebody forges a delivery — so this one is built to be copied rather than described.
Constant-time signature comparison, the raw bytes rather than the parsed body, the replay window, idempotency on the delivery id, and answering before doing the work. Its tests sign with the same function this platform signs with, so it cannot drift from what we actually send.
Every request re-checks the scope, so a key stops working the moment the winery narrows or revokes it — not at its next renewal.
| Scope | What it unlocks |
|---|---|
bookings:read |
Your bookable experiences and the reservations against them, plus live availability for any date. Availability is computed by us from your schedule, blackouts, venue hours and capacity — a partner never sees the raw schedule, so nobody can reimplement those rules and double-book a Saturday. |
bookings:write |
Take and cancel bookings. It CANNOT take payment, so an experience that charges at booking time is refused and the guest is sent to your own booking page; it CANNOT book a time outside your published slots, which your own staff can do and a partner cannot; and cancelling always applies your cancellation window — there is no override. |
catalog:read |
Your product list and product groups — names, SKUs, prices, vintages, ABV and bottle counts. What a storefront needs to show your wines. |
catalog:write |
Create and edit products — name, SKU, price, vintage, varietal, and whether each one is active and listed online. It CANNOT change stock levels, build bundles or set up a sale; those stay with your own staff. Nothing is ever deleted. |
customers:read |
Your customer records and the whole club side: tiers, members, releases and their allocations. This is personal data — grant it only to a partner who needs it. |
customers:write |
Create and edit customer records — name, contact details, address, and marketing consent. It CANNOT touch club membership or loyalty points: those are run by your billing and loyalty engines, and a partner sending one gets an error explaining why. Nothing is ever deleted. |
fulfillment:read |
The pack queue: what is waiting to ship, where it is going and what is already on its way. The scope a 3PL needs. |
fulfillment:write |
Mark a parcel shipped with its carrier and tracking number. The other half of the 3PL integration, and the only scope on this list that changes anything — grant it to the warehouse that actually ships, and to nothing else. |
inventory:read |
Bottles on hand per product, and the movement ledger behind those numbers — every restock, adjustment, wastage and import. |
inventory:write |
Move stock and record why — a delivery received, breakage, a stocktake correction. Every change lands in your ledger with its reason and is attributed to the API key, not to a member of staff. It CANNOT record a sale, a refund or a club allocation: those reasons belong to the systems that do them, and a partner writing one would make your inventory reports describe a transaction that never happened. |
market:read |
Your market configuration — country, currency, time zone, what to call a state or a postal code, and which subdivisions you may ship to. Read-only, and harmless: no customer or sales data is behind it. |
orders:read |
Online orders, their line items and totals — and the shipping-eligibility check, which answers a question about an order that has not been placed yet. |
orders:write |
Place orders you have already taken payment for, and price a cart before you do. It CANNOT charge a card: you are the merchant of record, you send us your own payment reference, and the amount must match the total we compute. Every order goes through the same compliance, age, stock and tax rules as your own shop — a partner cannot place an order your storefront would refuse. Nothing is ever cancelled or refunded through it. |
sales:read |
Register sales: tasting-room transactions, their line items, payments and refunds. What a reconciliation or accounting integration reads. |
35 endpoints. Full parameters, response codes and schemas are in the OpenAPI document.
The winery's own configuration — read this first.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/market |
Read this winery's market configuration | market:read |
Products and the groups they are organised into.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/product-groups |
List product groups | catalog:read |
GET /v1/products |
List products | catalog:read |
POST /v1/products |
Create a product | catalog:write |
GET /v1/products/{product_id} |
Read one product | catalog:read |
PATCH /v1/products/{product_id} |
Update a product | catalog:write |
Customer records, including the club status on each.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/customers |
List customers | customers:read |
POST /v1/customers |
Create a customer | customers:write |
GET /v1/customers/{customer_id} |
Read one customer | customers:read |
PATCH /v1/customers/{customer_id} |
Update a customer | customers:write |
GET /v1/customers/{customer_id}/communications |
Read a customer's communication history | customers:read |
POST /v1/customers/{customer_id}/notes |
Log a note or call against a customer | customers:write |
Tiers, members, releases and the allocations in them.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/club/members |
List club members | customers:read |
GET /v1/club/shipments |
List club releases | customers:read |
GET /v1/club/shipments/{shipment_id} |
Read one club release | customers:read |
GET /v1/club/shipments/{shipment_id}/allocations |
List a release's member allocations | customers:read |
GET /v1/club/tiers |
List club tiers | customers:read |
Online-shop orders, and the shipping quote behind a checkout.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/orders |
List online-shop orders | orders:read |
POST /v1/orders |
Place an order you have already charged for | orders:write |
POST /v1/orders/quote |
Price a cart before you charge for it | orders:read |
GET /v1/orders/{order_id} |
Read one order | orders:read |
POST /v1/shipping/quote |
Price a shipment, and check it is allowed | orders:read |
Register (tasting-room) transactions.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/sales |
List register sales | sales:read |
GET /v1/sales/{sale_id} |
Read one register sale | sales:read |
Bottles on hand and the movement ledger behind them.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/inventory |
List bottles on hand, per product | inventory:read |
POST /v1/inventory/adjustments |
Move stock, and say why | inventory:write |
GET /v1/inventory/movements |
List stock movements | inventory:read |
The pack queue: what ships, where, and what has gone.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/fulfillments |
List the pack queue | fulfillment:read |
GET /v1/fulfillments/{fulfillment_id} |
Read one fulfillment | fulfillment:read |
POST /v1/fulfillments/{fulfillment_id}/ship |
Mark a parcel shipped, with carrier and tracking | fulfillment:write |
Bookable experiences, live availability, and the reservations against them.
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/bookings |
List bookings | bookings:read |
POST /v1/bookings |
Take a booking | bookings:write |
POST /v1/bookings/{booking_id}/cancel |
Cancel a booking | bookings:write |
GET /v1/experiences |
List bookable experiences | bookings:read |
GET /v1/experiences/{experience_id}/availability |
Bookable slots for one date | bookings:read |
{"amount_cents": 4200, "currency": "CAD"}. null means unknown — it never means zero.prod_…, cus_…, ord_…. Store them whole: the prefix is how an id sent to the wrong endpoint is refused at the boundary instead of resolving to the wrong object. Integer primary keys never cross the boundary.GET /v1/market names. A naive timestamp you send is read in that zone.next_cursor verbatim; there is no page number and no offset. Cursors are signed and bound to the filters they were minted under, so one cannot be replayed against a differently filtered list. An unrecognised query parameter is a 400, not a shrug.{"error": {"type": …, "message": …, "request_id": …}}, always with a real status code. Branch on type; quote request_id (also in the X-Request-Id header, on successes too) and we can find the one log line.429 with Retry-After. Limits are per key, not per IP, so one partner's polling cannot throttle another's.12 events fire as things happen, signed so you can prove they came from us.
A winery adds your endpoint in their admin and picks which events to send. Each delivery carries X-SipsPOS-Signature — a timestamp and an HMAC-SHA256 over timestamp.body, the same scheme Stripe uses — so you can verify it without a shared session, and an X-SipsPOS-Delivery id you can quote back to us.
order.paid, order.fulfilled, order.refunded, sale.completedfulfillment.created, fulfillment.shipped, shipment.generatedcustomer.created, customer.updated, club.member_joined, club.member_cancelledproduct.updatedTwo things worth knowing before you build. order.refunded is what another platform would call order.cancelled: cancelling a paid order here is a refund — the capture is reversed, stock restocked, loyalty reversed — so the event is named for what happened to the money. And customer.updated means one of a watched set of fields changed, not that a human edited the record: re-fetch the customer rather than treating the payload as a diff.
There is a worked receiver — Parcel Post, above.
Retries are 1 minute, 5, 30, 2 hours, 12 hours, after which the endpoint is disabled and the winery is told. A 2xx is success; anything else retries. Deliveries and their responses are visible to the winery in their own admin, so a failing integration is something they can see rather than something they hear about from you.
Not a wish list — each of these is a real call sequence against endpoints and events that exist today, with the one thing that would otherwise bite you. Both example applications above are two of them, built out.
fulfillment.created, pull the parcel from /v1/fulfillments — what is in it, where it is going, whether it is a shop order or a club allocation — push it into your WMS, then POST /v1/fulfillments/{id}/ship with the carrier and tracking number when it leaves. That marks the parcel shipped, emails the customer their tracking, and emits fulfillment.shipped, so nobody retypes a tracking number into a second system. Needs fulfillment:read and fulfillment:write. The bite: that write reaches a customer's inbox, which almost nothing else on this API does — ask for fulfillment:write only if you are the party that actually ships, and expect a winery to say no otherwise.sale.completed and order.paid tell you when there is something to post; /v1/sales and /v1/orders carry the line items and a tax_breakdown per rate, in cents with an explicit currency — a sale also carries its payments and any refunds against it. Enough to write a journal entry per day without inferring tax back out of a total. Needs sales:read, plus orders:read if the winery sells online. The bite: orders:read requires the online store, so a tasting-room-only winery cannot issue it however much they want to — build the register half to stand on its own, or your integration works at one winery and not the next./v1/experiences lists what is bookable, /v1/experiences/{id}/availability gives real slots for a date, and POST /v1/bookings takes the reservation. A regional tourism site can show five wineries' Saturdays on one page. Needs bookings:read and bookings:write. The bite: availability is computed by us from the winery's schedule, blackouts, venue hours and capacity — you never see the raw schedule, which is precisely what stops anybody reimplementing those rules and double-booking a Saturday. An experience that charges at booking time is refused, because this API cannot take payment; send that guest to the winery's own page./v1/club/members filtered by club_status finds the members whose billing is failing; /v1/customers/{id}/communications says whether the winery has already reached out and whether the message actually arrived; POST /v1/customers/{id}/notes records that you called, onto the winery's own timeline rather than into a parallel record only you can see; and tags segments what is left. Needs customers:read and customers:write. The bite: the timeline is per customer, so a "who have we contacted this week" sweep is one request each — read the history for the names you are about to work, not for the whole club./v1/inventory for what the system thinks is there, POST /v1/inventory/adjustments for each correction with a reason, /v1/inventory/movements for the ledger behind the numbers. Every change is attributed to the API key rather than to a member of staff who was not there. Needs inventory:read and inventory:write. The bite: a partner may write restock, adjustment, wastage and import — and deliberately not sale, refund or allocation, because those reasons belong to the systems that do them and writing one would make the winery's reports describe a transaction that never happened.customer.created and customer.updated push the change to you; you re-read the customer and mirror it, consent flags included. Needs customers:read. The bite: there is no ?updated_since= on customers — that column does not exist and we will not fake it out of created_at, which would find new customers and silently never report an edit. So the webhook is the sync primitive here rather than a nicety, and a mirror built on polling would go quietly stale on exactly the fields that matter: addresses and opt-outs./v1/market tells you per winery whether to say "province" or "state", which currency the money is in and where it may ship, so there is no country conditional in your code. Then /v1/products to list, POST /v1/shipping/quote to ask before you sell, POST /v1/orders/quote to price the cart, and POST /v1/orders to record it once you have charged. Needs market:read, catalog:read and orders:write. The bite: you are the merchant of record. There is no hosted checkout to hand a shopper and nothing here charges a card — you take the money on your own account and send us your payment reference.Something else in mind? The endpoint table above is the whole of v1, and the honest list below is what it will not do. If the gap between them is what is stopping you, tell us what you are building. Both example applications on this page were written against the real API, and each one turned up something the API was missing — the customer write-back exists because the retention example had a call list and nowhere to record that anybody worked it.
Said plainly, because finding out mid-integration is worse than reading it now.
POST /v1/orders records an order you have already charged for on your own account — you are the merchant of record and you send us your payment reference. There is no hosted checkout link to hand a customer, because that needs an order state this platform does not have. If you need the winery's own processor to take the money, send the customer to their shop.POST /v1/inventory/adjustments, which records a reason, rather than by setting a number. Sending either is a 400 that says which endpoint owns the change.GET /v1/customers/{id}/communications reads one customer's history — what the winery sent them, whether it arrived, and the calls their staff logged. There is no /v1/communications, so “who have we contacted this week” is one request per customer, and we would rather say that than watch you build the loop. Two things that timeline does not tell you: which order or campaign an entry was about (four of the eight things an entry can point at are not API resources at all, so the field would be null for reasons you could not see), and who on staff wrote it — attribution is staff / api / system, because reading a customer's history should not hand you a winery's employee roster.