Commerce7 is a direct-to-consumer commerce platform built specifically for wineries, and secondarily for breweries, distilleries and cideries. It bundles the things a winery actually sells through: ecommerce, a tasting room point of sale, wine club management with recurring club packages, reservations for tastings, allocations, and alcohol shipping compliance. Its API is public, fully documented, small enough to read end to end in an afternoon, and pleasantly conventional: one base URL, basic auth plus a tenant header, resources at /v1/<noun>, money in cents and dates in UTC. If you are building a commerce database over wine DTC merchants, Commerce7 is one of the few platforms in that niche where a complete integration is straightforward.
At a glance
What it is
Commerce7 is a Canadian company serving wineries mostly in North America, with customers in Australia, New Zealand and Europe. It is a single-tenant-per-winery SaaS: each client is a "tenant" with a short string identifier such as spectrawinery, and that identifier appears in every API call.
The domain model is ordinary ecommerce plus four wine-specific pieces a generic connector will not expect. Clubs and club memberships: a recurring wine club is first class, and club shipments generate orders on a schedule. Allocations: limited-release wines are offered to named customers with a per-customer quantity, and an order can carry an allocationId. Reservations: tasting room bookings, with their own object and webhooks. Compliance: every order carries a complianceStatus (Compliant, Forced, Not Checked, No Compliance Required, Quarantined, Void) because alcohol shipping is regulated per state, so an order can be paid and still not shippable.
channel on an order (Inbound, Web, POS, Club) tells you which of those the sale came from, and it is the field to split revenue on.
API access
Two routes.
- App. Register in the Commerce7 App Developer Center. You get an App ID and an App Secret Key, wineries install your app, and you call their data with the app credentials plus their tenant identifier. This is the multi-merchant route and the one that supports app extensions in the Commerce7 admin.
- Single client data migration. A winery generates credentials for a one-off migration or their own integration, and calls are authenticated with the
tenantheader.
The documentation does not list official SDKs for server languages. Plan on calling REST directly.
Versioning is in the path (/v1), and Commerce7 publishes a separate API versioning page and an announcements feed for breaking changes.
Authentication
Base URL: https://api.commerce7.com/v1/{endpoint}.
- Take your App ID (for example
demo-app) as the username and your App Secret Key as the password, and send them as HTTP basic auth. - Send a
tenantheader naming the winery whose data you want. - Send
Content-Type: application/jsonon any request with a JSON body.
curl --request GET \
--url 'https://api.commerce7.com/v1/order?channel=Web&orderPaidDate=gte:2026-09-01' \
--user "${APP_ID}:${APP_SECRET_KEY}" \
--header 'tenant: spectrawinery' \
--header 'Content-Type: application/json'
The documentation states plainly: do not put the App Secret Key in JavaScript, because it can be read by other users. It belongs in backend code only.
Multi-account is simple and is the platform's strong point: one App ID and secret, and the tenant header selects the winery. A connector stores one credential pair and a list of tenant identifiers, with no per-merchant token refresh at all.
The other authentication, for app extensions
Separate mechanism, easy to confuse with the above. When your app renders inside the Commerce7 admin, Commerce7 passes a JWT as an account URL parameter (or as appToken when the app renders on a winery's storefront instead). To verify who the user is, call the account endpoint with that JWT in Authorization and the tenant in tenant:
curl --request GET \ --url https://api.commerce7.com/v1/account/user \ --header 'Authorization: <JWT>' \ --header 'tenant: some-tenant'
A success returns the user's ID, name, email and role. A failure returns 401, and the documented expectation is that your app responds 401 with a friendly message linking to your support docs. This JWT identifies a user, it does not replace the app credentials for data calls.
Objects we can read
Two conventions apply everywhere and both bite if missed. All currency amounts are in cents, so 4500 is $45.00. All dates are UTC, normally ISO datetime.
Orders
GET /order lists, GET /order/{id} fetches one. Filters on the list endpoint: q, customerId, id, orderTagId, tagId, posProfileId, allocationId, queryId, channel (Inbound, Web, POS, Club), orderDeliveryMethod (Pickup, Carry Out, Ship), fulfillmentStatus (Fulfilled, Not Fulfilled, Partially Fulfilled, No Fulfillment Required) and complianceStatus.
Date filters take YYYY-MM-DD and support the operators =, gt:, gte:, lt:, lte: and btw: on updatedAt, orderPaidDate, orderFulfilledDate and orderSubmittedDate. Examples from the reference: /order?channel=Web&orderPaidDate=gte:2021-01-01 and /order?orderPaidDate=btw:2021-01-01|2021-02-01. For an incremental sync, filter on updatedAt and page with the cursor, not with page.
{
"orders": [{
"id": "cf61d2ca-41d5-44f4-8e46-17ef90c83920",
"orderSubmittedDate": "2018-12-04T19:10:27.905Z",
"orderPaidDate": "2018-12-04T19:10:28.143Z",
"orderNumber": 1248, "orderSource": "Internal",
"paymentStatus": "Paid", "complianceStatus": "No Compliance",
"fulfillmentStatus": "Not Fulfilled", "channel": "Web",
"cartId": "cf61d2ca-41d5-44f4-8e46-17ef90c83920",
"customerId": "c48d6fbd-e235-4ffd-a527-d9dd25e8d2c2",
"orderDeliveryMethod": "Ship", "taxSaleType": "Offsite",
"subTotal": 4500, "shipTotal": 0, "taxTotal": 540,
"total": 5040, "totalAfterTip": 5040,
"createdAt": "2018-12-02T18:45:17.895Z", "updatedAt": "2018-12-04T19:10:28.144Z",
"shipping": [{ "id": "fe92be1e-...", "title": "Ground", "code": "FEX",
"service": "Ground", "price": 0, "tax": 0 }],
"items": [{ "id": "4c785d18-...", "productTitle": "2014 Spectra Cabernet Sauvignon",
"type": "Wine", "productId": "94d672d2-...", "productVariantTitle": "750ml",
"sku": "2014-CS", "price": 4500, "quantity": 1, "tax": 540, "taxType": "Wine" }],
"tenders": [{ "id": "a0a385e8-...", "tenderType": "Credit Card",
"chargeType": "Capture", "chargeStatus": "Success",
"amountTendered": 5040, "paymentDate": "2018-12-04T19:10:28.128Z" }]
}],
"total": 2
}
Note the order has no single status. paymentStatus, fulfillmentStatus and complianceStatus are three independent axes and all three matter for a wine order.
Order items and payments
items[] is embedded in the order: id, productId, productTitle, productVariantTitle, sku, type, price, quantity, tax, taxType. tenders[] is also embedded and holds the payment records: tenderType, chargeType, chargeStatus, amountTendered, paymentDate. Shipping quotes are in shipping[] with title, code, service, price and tax.
Products and listings
GET /product lists, GET /product/{id} fetches one. Filters: q (product name), updatedAt with the same operator set, webStatus, adminStatus, collectionId. Example: /product?q=cab&adminStatus=Available. A product carries id, title, slug, type (for example Wine), webStatus, adminStatus, seo, and a variants[] array. Each variant carries id, title (for example 750ml), sku, price, comparePrice, inventoryPolicy (for example Dont Sell), taxType, costOfGood, volumeInML, hasInventory, and an inventory[] array with inventoryLocationId, availableForSaleCount, allocatedCount and reserveCount. In other words, a single product read already gives you stock per location, which avoids a second call for most catalogue syncs.
webStatus and adminStatus are separate: a wine can be orderable by staff in the tasting room but hidden from the website.
Inventory
GET /inventory lists, GET /inventory/{id} fetches one, POST /inventory initialises a record. Each inventory row carries id, inventoryLocationId, inventoryLocationTitle, productId, productTitle, productVariantId, productVariantTitle, sku, upcCode, hasInventory, inventoryPolicy, availableForSaleCount, reserveCount and allocatedCount. Three counts, three meanings: availableForSaleCount is sellable, reserveCount is held back (library stock, staff reserve), allocatedCount is committed to allocations or unfulfilled orders. Map availableForSaleCount to quantity_available and allocatedCount to quantity_reserved.
Locations
GET /inventory-location lists, GET /inventory-location/{id} fetches one, POST /inventory-location creates. Fields: id, title, address, address2, city, stateCode, zipCode, countryCode, phone, isWebShipLocation, isInboundShipLocation, isClubShipLocation, isAPickupLocation, createdAt, updatedAt. Those four booleans are how you tell a tasting room from a fulfilment warehouse.
Customers
GET /customer, plus /customer-address and /customer-credit-card as separate resources. All PII, and credit card data is tokenised rather than raw. Customers, addresses and carts all support cursor pagination.
Returns and settlements
No returns resource and no settlement or payout resource appear in the documentation index. Refunds surface as tender records on the order with a negative or refund chargeType. Money movement to the winery's bank comes from the payment processor, not from Commerce7.
Writing back: listings, price and stock
Stock changes go through a transaction rather than a field write. action is Reset (set to an absolute value) or Adjust (apply a delta), and the row is identified by sku plus inventoryLocationId.
{
"action": "Reset",
"sku": "83747572",
"notes": "Reset count to match fulfillment",
"availableForSaleCount": 30,
"reserveCount": 5,
"inventoryLocationId": "88145cdc-f5a6-11e8-a80a-06d2aa911ccc"
}
The response is a transaction record with transactionType, transactionDetails, transactionDate, the resulting availableForSaleCount, reserveCount and allocatedCount, and a productVariantId, which gives you an audit trail for free. Fulfillment writes the shipment:
{
"type": "Shipped",
"packageCount": 1,
"items": [{ "sku": "70010001-01", "quantityFulfilled": 1 }],
"sendTransactionEmail": true,
"fulfillmentDate": "2018-07-03T07:00:00.000Z",
"shipped": { "trackingNumbers": ["Z1234231123523234"], "carrier": "UPS" }
}
type is Shipped, Picked Up or No Fulfillment Required. For a pickup, send a pickedUp object with pickedUpBy instead of shipped. The items array is omitted on the /fulfillment/all endpoint. All fulfillment endpoints return the order, except delete which returns 204.
PUT semantics are asymmetric and will silently destroy data if you get them wrong. Root-level fields can be updated selectively: pass only what you want to change. Sub-objects cannot: on nested objects such as variants, any element you do not pass is overwritten with null. Always read the object, mutate it, and write the whole sub-object back.
When upserting an order, pass isSendTransactionEmail: false to stop the confirmation email reaching the customer. orderNumber must be unique and accepts integers up to 2147483647. If orderFulfilledDate is omitted on creation, Commerce7 uses orderPaidDate.
Webhooks and notifications
Create a webhook with POST /web-hook, passing object, action and url. Manage them with GET /web-hook, GET /web-hook/{id}, PUT /web-hook/{id} and DELETE /web-hook/{id}. They can also be configured from the Commerce7 admin.
Objects: Allocation, Cart, Club, Club Package, Club Membership, Collection, Customer, Customer Address, Customer Credit Card, Coupon, Group, Product, Promotion, Order, Reservation, Tag, Transaction Email. Actions: Create, Update, Bulk Update, Delete, Send. Subscribe once per object and action pair. The delivered body carries the object type, the triggering action, the entire entity, the email of the user who made the change, and the tenant identifier.
Bulk tagging does not produce one event per record. Instead Commerce7 sends a single Bulk Update notification containing a callback URL, which you then walk with cursor pagination in batches of 50 to get the affected records. The documentation is explicit that if your integration depends on tags for orders, customers, reservations or club memberships, you must subscribe to Bulk Update as well as Update or you will miss them.
Two operational traps. First, no signature verification scheme is documented: the webhooks pages read contain no HMAC header, no shared secret and no signing algorithm. Treat the endpoint as unauthenticated, put an unguessable path on it, and verify anything you receive by reading the object back from the API before acting on it. Second, if your endpoint keeps erroring for more than 48 hours the webhook is disabled permanently. It cannot be re-enabled; you have to create a new one. Monitor delivery failures.
Rate limits and pagination
The limit is 100 requests per minute per tenant. Because a connector's credentials are the same across all its tenants, the limit scales with the number of wineries you serve rather than throttling you globally. Two pagination models, and the choice interacts with rate limiting.
- Standard:
?page=n&limit=n,limitbetween 1 and 50, default 50, response is an array plus atotal. After 100 pages it is throttled to one request per 60 seconds, which makes it useless for a backfill of any size. - Cursor: start with
?cursor=start, pass the cursor returned in each response, stop when a response comes back without one. Documented as having no rate limits. Available on/v1/cart,/v1/club-membership,/v1/customer,/v1/customer-address,/v1/note,/v1/order,/v1/product,/v1/reservationand/v1/trash.
Use cursor pagination for everything it supports and standard pagination only for small resources such as inventory locations and webhooks. /v1/trash is worth noting: it is how you find deleted records, which a mirror needs in order to apply deletions. Error codes are not documented on the API overview page read.
Mapping to the unified model
Gaps and open questions
- No webhook signature or shared secret is documented. This is the biggest security gap in the integration and should be raised with Commerce7 before going live.
- Webhooks are permanently disabled after 48 hours of persistent errors and cannot be re-enabled, only recreated. Alerting on delivery failure is mandatory, not optional.
- No returns resource, no settlement or payout resource, no documented sandbox, and no HTTP error code reference on the API overview page.
- Currency is not present on the order sample, so it is presumably a tenant-level setting; confirm per tenant. No official server-side SDKs are listed either.
- Standard pagination collapses to one request per minute after 100 pages, so any backfill must use cursor pagination, which is available on nine endpoints only. Resources outside that list cannot be backfilled quickly.