SipsPOS
Developer API

Build on a winery's own data.

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.

Two questions no other winery API answers

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.

Getting a key

The winery issues it. You send one header.

curl -s https://winery.sipspos.com/api/v1/market \
  -H "Authorization: Bearer sk_live_…"
  • The key is minted by the winery's admin in Admin → Setup → API keys, and shown once — only a hash is stored, so a lost key is replaced, never recovered. The same screen issues test keys against a sandbox; ask for one of those first (below).
  • A key is bound to one winery and carries only the scopes its admin ticked. There is no account id in any path, no cookie and no CSRF token.
  • The base URL is the winery's own host (or its custom domain, if it has one). Their admin can read it off the address bar.
  • API access is the Developer API add-on — $49/month, or $490/year, on the Growth and Estate plans. A key on a winery without it answers 403 feature_not_enabled rather than failing quietly.
  • The winery's admin also has a full integration guide with a worked example for every endpoint, inside the app at Resources → API Integration Guide. Ask them to send it over — or send them this page.
Sandbox

Build against a winery that isn't real.

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
  • It is a second tenant, not a mock. Same endpoints, same serializers, same pagination, same idempotency, same webhooks — because it is the same code. Nothing to drift out of date with these docs.
  • Writes are real writes. 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.
  • A test key cannot reach live data, and a live key cannot reach the sandbox. The two resolve to different wineries, so it is not a rule that could be forgotten — it is which rows exist.
  • Every response says which one answered, in 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.
  • The winery can rebuild it whenever the data gets messy. Your keys keep working; only what is behind them is replaced. Nothing you do in the sandbox is ever visible to their real customers.
  • A test key does not need the Developer API add-on. A winery evaluating the API can hand you one before buying anything. A sk_live_ key still requires it.
Examples

Start from something that already works.

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.

Calling the API — Club Pulse

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.

Receiving webhooks — Parcel Post

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.

Scopes

A key can only read what it was granted.

Every request re-checks the scope, so a key stops working the moment the winery narrows or revokes it — not at its next renewal.

ScopeWhat 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.
Endpoints

Everything v1 answers.

35 endpoints. Full parameters, response codes and schemas are in the OpenAPI document.

Market

The winery's own configuration — read this first.

EndpointWhat it doesScope
GET /v1/market Read this winery's market configuration market:read

Catalog

Products and the groups they are organised into.

EndpointWhat it doesScope
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

Customers

Customer records, including the club status on each.

EndpointWhat it doesScope
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

Club

Tiers, members, releases and the allocations in them.

EndpointWhat it doesScope
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

Orders

Online-shop orders, and the shipping quote behind a checkout.

EndpointWhat it doesScope
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

Sales

Register (tasting-room) transactions.

EndpointWhat it doesScope
GET /v1/sales List register sales sales:read
GET /v1/sales/{sale_id} Read one register sale sales:read

Inventory

Bottles on hand and the movement ledger behind them.

EndpointWhat it doesScope
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

Fulfillment

The pack queue: what ships, where, and what has gone.

EndpointWhat it doesScope
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

Bookings

Bookable experiences, live availability, and the reservations against them.

EndpointWhat it doesScope
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
Conventions

Six things that are true everywhere.

Money is an object, never a float.
{"amount_cents": 4200, "currency": "CAD"}. null means unknown — it never means zero.
Ids are opaque and prefixed.
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.
Timestamps carry the winery's own offset.
ISO-8601 in the winery's time zone, which GET /v1/market names. A naive timestamp you send is read in that zone.
Lists are cursor-paginated.
Send back 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.
Errors all share one shape.
{"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.
Rate limits are per key.
A throttled request answers 429 with Retry-After. Limits are per key, not per IP, so one partner's polling cannot throttle another's.
Webhooks

You do not have to poll.

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.

  • Orders and salesorder.paid, order.fulfilled, order.refunded, sale.completed
  • Fulfillmentfulfillment.created, fulfillment.shipped, shipment.generated
  • Customers and clubcustomer.created, customer.updated, club.member_joined, club.member_cancelled
  • Catalogproduct.updated

Two 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 receiverParcel 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.

What to build

Things this surface already supports.

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.

A warehouse or 3PL bridge.
Subscribe to 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.
Nightly accounting reconciliation.
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.
A concierge, hotel or wine-route booking widget.
/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.
A club retention loop that does not phone anybody twice.
/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.
A stocktake app for a phone.
Somebody walks the cellar with a scanner: /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.
A CRM or mailing-list mirror.
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.
A regional marketplace across several wineries.
One integration, many wineries: /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.

What is not here yet

The honest list.

Said plainly, because finding out mid-integration is worse than reading it now.

  • Paying through us. 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.
  • Cancelling or refunding an order. Cancelling a paid order means refunding it, and for an order you charged for, that money is not ours to reverse. Refund on your own account and tell the winery.
  • Changing club membership or stock levels by assignment. Both are published and neither is writable. Club membership is run by the winery's billing engine; stock moves through 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.
  • Sweeping the communication log across customers. 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.
  • Deleting anything. Nothing in v1 deletes.
  • OAuth. A key belongs to one winery. An app that acts for many wineries holds one key per winery today.
  • An app marketplace. There is no directory and no install flow. A winery issues you a key directly.