Commerce API
The complete reference for building on the CogniChat Commerce API. Authentication, request bodies, responses, and copy-paste examples in your language. Pick a language in the top bar and it applies to every example.
Introduction
The CogniChat Commerce API is a headless REST API for building your own storefront, app, or integration on top of a CogniChat workspace. It exposes the same catalog, cart, checkout, bookings, customers, and order data that power the hosted store and the WhatsApp agent.
Every request is authenticated with a workspace API key, scoped to that one workspace, and rate limited per key. All responses are JSON. The API is versioned in the path; this document describes v1.
Base URL
https://cognichat-api.vendyi.com/api/public/v1Conventions
| Detail | |
|---|---|
| Protocol | HTTPS only. Requests over plain HTTP are refused. |
| Content type | Send and receive application/json. Send Content-Type: application/json on any request with a body. |
| Money | Every amount is an integer in the smallest currency unit (e.g. cents / pesewas) named *_cents. Never a float. |
| Money-blind | Your client never sends prices. Send ids + quantities; the server prices every line against the live catalog and returns a pay link. |
| Time | Timestamps are ISO-8601 UTC strings (e.g. 2026-07-08T14:20:00Z). |
| Ids | Order and cart handles in URLs are opaque tokens, not guessable sequential ids. |
Authentication
Authenticate every request with a workspace API key. Send it either as a Bearer token or in the X-Api-Key header — pick one, they are equivalent.
curl "https://cognichat-api.vendyi.com/api/public/v1/catalog/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
# ...or, equivalently:
curl "https://cognichat-api.vendyi.com/api/public/v1/catalog/" \
-H "X-Api-Key: pk_live_YOUR_KEY"Two kinds of key
| Key | Prefix | Use it in | Can access |
|---|---|---|---|
| Publishable | pk_live_ | Browser / client code | Catalog, services, cart, checkout, order status |
| Secret | sk_live_ | Server-side only | Everything a publishable key can, plus customer records and webhook configuration |
sk_) key in a browser, mobile app, or any client a user can inspect. If a key leaks, revoke it in the dashboard and mint a new one — keys are shown in full exactly once, at creation.How keys are handled
Keys are stored only as a SHA-256 hash — a database leak cannot reconstruct a usable key. Revoking a key takes effect immediately; the next request with it returns 401. Each key carries its own rate-limit bucket and usage counters.
| Failure | Status | Meaning |
|---|---|---|
| No key sent | 401 | Missing Authorization / X-Api-Key header. |
| Unknown or revoked key | 401 | The key does not resolve to an active credential. |
| Workspace not on plan | 403 | not_entitled — the headless API is not enabled on this workspace's plan. |
| Publishable key on a secret endpoint | 403 | secret_key_required — use an sk_ key. |
Errors
Errors use conventional HTTP status codes and a single, stable JSON shape you can branch on. 2xx is success, 4xx is a problem with the request, 5xx is a problem on our side.
{
"error": {
"code": "not_entitled",
"message": "The headless commerce API is not enabled on this plan."
}
}Always branch on error.code (stable, machine-readable), not error.message (human-readable, may change).
Common codes
| Status | code | When |
|---|---|---|
| 400 | unknown_item / unknown_service | A referenced product or service does not exist or is inactive. |
| 400 | mixed_cart | A cart holds a booking; products and bookings can't share a cart. |
| 400 | stock_changed | An item sold out between preview and checkout. |
| 401 | — | Missing, invalid, or revoked API key. |
| 403 | not_entitled | The workspace's plan does not include the headless API. |
| 403 | secret_key_required | Endpoint needs an sk_ key; a pk_ key was used. |
| 404 | cart_not_found / not_found | The token or id does not resolve within this workspace. |
| 429 | — | Rate limit exceeded. Retry after the Retry-After header. |
Rate limits
Rate limits are applied per API key, not per IP, so one integration can never starve another. A well-behaved storefront stays comfortably under the limit.
When you exceed the limit the API returns 429 Too Many Requests with a Retry-After header (seconds). Back off for that long, then retry. Build retries with exponential backoff and jitter for resilience.
Webhooks
Webhooks push commerce events to your server the moment they happen, so you don't have to poll. Register one or more endpoint URLs (with a secret sk_ key), choose which events you want, and we POST a signed JSON body to each.
Event types
| Event | Fires when |
|---|---|
order.paid | A product order's payment is confirmed. |
booking.paid | A booking's payment is confirmed. |
order.status_changed | An order moves between fulfillment states. |
order.review_submitted | A customer submits or edits a review for an order (money-blind: rating only, no price). |
Verifying the signature
Every delivery carries an X-Webhook-Signature header. It is the HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret (returned once when you create the endpoint), prefixed with sha256=. Recompute it over the raw body and compare in constant time before trusting the payload.
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected),
);
}
// Express: use the RAW body, not the parsed JSON, for the HMAC.
app.post("/webhooks/cognichat", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["x-webhook-signature"];
if (!verify(req.body, sig, process.env.COGNICHAT_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString());
// handle event.type ...
res.sendStatus(200);
});Delivery headers
| Header | Value |
|---|---|
X-Webhook-Signature | sha256=<hmac_sha256(secret, raw_body)> |
X-Webhook-Event | The event type, e.g. order.paid. |
X-Webhook-Delivery | A unique delivery id — dedupe on it. |
Retries
Return a 2xx quickly to acknowledge. Any non-2xx or a timeout is retried with backoff (roughly 1m, 5m, 30m, 2h, 6h — up to 6 attempts). Because a delivery may arrive more than once, make your handler idempotent by deduping on X-Webhook-Delivery.
Catalog
Read the workspace's product catalog. Read-only and customer-safe — cost prices and stock internals are never exposed.
Get the catalog
Returns the full category tree, each category carrying its active items with modifiers, portions, and an is_orderable flag (effective, stock-aware availability). The response also carries a store block with the workspace's aggregate customer rating for social proof.
branchintegerScope is_orderable availability to a specific branch id.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/catalog/?branch=1" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"categories": [
{
"id": 3,
"parent": null,
"name": "Mains",
"brand_tag": "kanzo",
"sort_order": 0,
"image_url": "https://.../mains.jpg",
"items": [
{
"id": 42,
"category": 3,
"parent": null,
"variant_name": "",
"variant_group": "",
"name": "Jollof & Chicken",
"description": "Smoky party jollof with grilled chicken.",
"price_cents": 5500,
"image_url": "https://.../jollof.jpg",
"is_drink": false,
"sort_order": 0,
"modifiers": [],
"modifier_groups": [],
"portions": [],
"is_orderable": true
}
]
}
],
"store": {
"rating": 4.8,
"review_count": 126,
"rating_distribution": { "1": 2, "2": 3, "3": 8, "4": 30, "5": 83 }
}
}Reviews are per-order, so store is the whole-workspace aggregate (not per-item). It carries only customer-safe numbers (the average rating, the review count, and the star distribution), never cost or internal metrics.
Get one product
Returns a single active product by id, with the same customer-safe shape as an item in the catalog tree.
idintegerrequiredThe product id.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/catalog/items/42/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"id": 42,
"category": 3,
"parent": null,
"variant_name": "",
"variant_group": "",
"name": "Jollof & Chicken",
"description": "Smoky party jollof with grilled chicken.",
"price_cents": 5500,
"image_url": "https://.../jollof.jpg",
"is_drink": false,
"sort_order": 0,
"modifiers": [],
"modifier_groups": [],
"portions": [],
"is_orderable": true
}Returns 404 with code not_found if the item does not exist or is inactive in this workspace.
Services
Read bookable services (stays and appointments) and their open availability.
List services
Returns the workspace's active bookable services.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/services/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"services": [
{
"id": 12,
"name": "Deluxe Room",
"description": "Queen bed, garden view.",
"category": "Rooms",
"price_cents": 42000,
"price_label": "per night",
"duration_minutes": null,
"booking_mode": "stay",
"max_capacity": 2
}
]
}Get availability
Returns open dates/slots for one service, using the same availability engine as the dashboard and the agent. The window is capped at 60 days.
idintegerrequiredThe service id.
startdateWindow start, YYYY-MM-DD. Defaults to today; clamped to today at the earliest.
enddateWindow end, YYYY-MM-DD. Defaults to 30 days out; capped at 60 days from start.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/services/42/availability/?start=2026-07-10&end=2026-07-10" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"service_id": 12,
"availability": {
"2026-07-10": { "available": true },
"2026-07-11": { "available": false }
}
}The inner shape depends on booking_mode: a stay returns per-date availability; an appointment returns per-date open time slots.
Cart
A server-side cart addressed by an opaque token. Every price in the preview is computed by the server — the cart is never trusted for money. A cart holds either products or a single booking, never both.
Create a cart
Creates an empty cart and returns its opaque token. The cart expires after 7 days.
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/cart/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"token": "hV3k...opaque...9Qz",
"expires_at": "2026-07-15T14:20:00Z"
}Get or update a cart
GET returns the cart with a server-priced preview. PATCH sets a coupon code. The priced block reflects live prices; if the cart is currently invalid (e.g. an item sold out) it carries an error instead of a subtotal rather than failing the request.
tokenstringrequiredThe cart token.
coupon_codestringPATCH only. Apply a coupon (max 40 chars). Send empty to clear.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/cart/hV3k9Qz/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"token": "hV3k...9Qz",
"coupon_code": "",
"expires_at": "2026-07-15T14:20:00Z",
"raw_lines": [
{
"id": 88,
"kind": "product",
"menu_item_id": 42,
"service_id": null,
"qty": 2,
"selection": {}
}
],
"priced": {
"kind": "product",
"lines": [
{ "menu_item_id": 42, "name": "Jollof & Chicken", "qty": 2, "line_total_cents": 11000 }
],
"subtotal_cents": 11000
}
}Add a line
Adds a product line, or replaces the cart with a single booking line when kind is booking. Adding the same product with the same selection increments its quantity. Returns the re-priced cart.
tokenstringrequiredThe cart token.
kindstringproduct (default) or booking.
menu_item_idintegerProduct line: the item to add. Required when kind is product.
qtyintegerProduct line: quantity (min 1, default 1).
selectionobjectProduct line: chosen modifiers / portion. Echoed to checkout; never a price.
service_idintegerBooking line: the service to book. Required when kind is booking.
check_in_datedateBooking (stay): YYYY-MM-DD.
check_out_datedateBooking (stay): YYYY-MM-DD.
datedateBooking (appointment): YYYY-MM-DD.
start_timestringBooking (appointment): HH:MM.
num_guestsintegerBooking: party size.
special_requestsstringBooking: free-text note.
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/cart/hV3k9Qz/items/" \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"menu_item_id":42,"qty":2}'{
"token": "hV3k...9Qz",
"coupon_code": "",
"expires_at": "2026-07-15T14:20:00Z",
"raw_lines": [
{ "id": 88, "kind": "product", "menu_item_id": 42, "service_id": null, "qty": 2, "selection": {} }
],
"priced": {
"kind": "product",
"lines": [ { "menu_item_id": 42, "name": "Jollof & Chicken", "qty": 2, "line_total_cents": 11000 } ],
"subtotal_cents": 11000
}
}Returns 400 with unknown_item / unknown_service for a bad id, or mixed_cart when adding a product to a cart that holds a booking.
Update or remove a line
PATCH sets a new quantity (a quantity of 0 removes the line). DELETE removes the line outright. Both return the re-priced cart.
tokenstringrequiredThe cart token.
line_idintegerrequiredThe raw_lines[].id to change.
qtyintegerPATCH only. New quantity; 0 removes the line.
curl -X PATCH "https://cognichat-api.vendyi.com/api/public/v1/cart/hV3k9Qz/items/88/" \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"qty":3}'{
"token": "hV3k...9Qz",
"coupon_code": "",
"expires_at": "2026-07-15T14:20:00Z",
"raw_lines": [
{ "id": 88, "kind": "product", "menu_item_id": 42, "service_id": null, "qty": 3, "selection": {} }
],
"priced": { "kind": "product", "lines": [ ... ], "subtotal_cents": 16500 }
}Checkout
Turn a cart (or a set of raw lines) into a paid order or booking. Checkout is money-blind: you send ids, quantities, a customer, and an optional coupon; the server prices every cent and returns a pay link. No card data ever touches your integration.
Place an order or booking
Provide either a cart_token (recommended — uses the cart you built) or an inline lines array. The server prices the order, creates it as pending payment, and returns a pay_url to send the customer to, plus an opaque order_token for status lookups.
cart_tokenstringCheck out an existing cart. Its coupon is used unless you override coupon_code.
linesarrayAlternative to cart_token: inline line objects ({kind, menu_item_id, qty} or a booking line).
customerobjectBuyer contact: { name, phone, email }.
fulfillment_typestringe.g. pickup (default) or delivery.
coupon_codestringDiscount code; overrides the cart's coupon when present.
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/checkout/" \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"cart_token":"hV3k...9Qz","customer":{"name":"Ada Owusu","phone":"+233201234567","email":"ada@example.com"},"fulfillment_type":"pickup"}'{
"kind": "product",
"order_number": "1042",
"order_token": "Xa7...opaque...b2",
"pay_url": "https://checkout.paystack.com/xxxxxxxx",
"total_cents": 11000
}For a booking, kind is booking. Errors return 400 with codes like stock_changed, mixed_cart, delivery_unavailable, or paystack_not_configured.
Get order status
Look up an order or booking by its opaque order_token (from checkout). Guessable numbers like order_number are never accepted here.
tokenstringrequiredThe order_token returned by checkout.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/orders/hV3k9Qz/" \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"kind": "product",
"order_number": "1042",
"status": "confirmed",
"payment_status": "paid",
"total_cents": 11000
}Returns 404 with not_found if no order matches the token in this workspace.
Submit a review
Submit (or edit) the customer's star rating for an order, resolved by the same opaque order_token you got from checkout. Re-submitting for the same order updates the existing review. Money-blind: a rating and an optional comment only.
tokenstringrequiredThe order_token returned by checkout.
ratingintegerrequiredThe rating, an integer from 1 (poor) to 5 (great).
commentstringOptional free-text feedback (trimmed and capped at 500 chars).
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/orders/hV3k9Qz/review/" \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"rating":5,"comment":"Fast delivery and the jollof was perfect."}'{
"ok": true,
"review": {
"rating": 5,
"comment": "Fast delivery and the jollof was perfect.",
"submitted_at": "2026-07-09T14:20:00Z"
}
}Returns 400 with invalid_rating if the rating is missing or outside 1-5, or 404 with not_found if no order matches the token in this workspace. Emits an order.review_submitted webhook.
Customers
Identify and look up the workspace's customers — the same customer records the WhatsApp agent and dashboard use. This is PII and the business's own CRM, so these endpoints require a secret (sk_) key.
Identify / upsert a customer
Resolves a customer by phone (creating one if new), and updates their email and your own external_id when provided. Idempotent by phone.
phonestringrequiredThe customer's phone number (E.164, e.g. +233201234567).
namestringDisplay name; set on first sight.
emailstringEmail to store.
external_idstringYour own id for this customer (max 120 chars).
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/customers/" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+233201234567","name":"Ada Owusu","email":"ada@example.com","external_id":"cus_1042"}'{
"id": 501,
"name": "Ada Owusu",
"phone": "+233201234567",
"email": "ada@example.com",
"external_id": "cus_1042",
"total_orders": 3,
"total_bookings": 0,
"total_spent_cents": 33000,
"first_seen_at": "2026-05-01T10:00:00Z",
"last_seen_at": "2026-07-08T14:20:00Z"
}Returns 400 with invalid_phone if the number can't be parsed.
Look up a customer
Read-only lookup by your external_id or by phone. Never creates a record.
external_idstringFind by the id you assigned via upsert.
phonestringFind by phone number.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/customers/?external_id=value&phone=value" \
-H "Authorization: Bearer sk_live_YOUR_KEY"{
"id": 501,
"name": "Ada Owusu",
"phone": "+233201234567",
"email": "ada@example.com",
"external_id": "cus_1042",
"total_orders": 3,
"total_bookings": 0,
"total_spent_cents": 33000,
"first_seen_at": "2026-05-01T10:00:00Z",
"last_seen_at": "2026-07-08T14:20:00Z"
}Returns 404 with not_found when no customer matches.
Webhook endpoints
Register and manage the URLs we POST events to. These require a secret (sk_) key. See the Webhooks concept above for signing and retries.
Register an endpoint
Registers a URL to receive events. The signing secret is returned once, in this response only — store it to verify signatures.
urlstringrequiredYour HTTPS endpoint URL.
eventsarraySubset of event types to receive. Omit or empty to receive all.
descriptionstringA label for your own reference.
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/webhooks/" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.com/webhooks/cognichat","events":["order.paid","booking.paid"],"description":"Prod fulfillment"}'{
"id": 7,
"url": "https://yourapp.com/webhooks/cognichat",
"events": ["order.paid", "booking.paid"],
"description": "Prod fulfillment",
"active": true,
"created_at": "2026-07-08T14:20:00Z",
"secret": "whsec_9s...shown once...Kf"
}Returns 400 with invalid_events if an event name is not recognized.
List endpoints
Lists your registered endpoints. The signing secret is never returned again after creation.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/webhooks/" \
-H "Authorization: Bearer sk_live_YOUR_KEY"[
{
"id": 7,
"url": "https://yourapp.com/webhooks/cognichat",
"events": ["order.paid", "booking.paid"],
"description": "Prod fulfillment",
"active": true,
"created_at": "2026-07-08T14:20:00Z"
}
]Delete an endpoint
Removes an endpoint. We stop delivering to it immediately.
idintegerrequiredThe endpoint id.
curl -X DELETE "https://cognichat-api.vendyi.com/api/public/v1/webhooks/42/" \
-H "Authorization: Bearer sk_live_YOUR_KEY"(empty body)Send a test event
Enqueues a signed ping delivery so you can verify your receiver end to end.
idintegerrequiredThe endpoint id.
curl -X POST "https://cognichat-api.vendyi.com/api/public/v1/webhooks/42/test/" \
-H "Authorization: Bearer sk_live_YOUR_KEY"{
"delivery_id": 331,
"status": "pending"
}List deliveries
Returns the recent delivery log for an endpoint (most recent first), including status, attempts, and the last error.
idintegerrequiredThe endpoint id.
curl -X GET "https://cognichat-api.vendyi.com/api/public/v1/webhooks/42/deliveries/" \
-H "Authorization: Bearer sk_live_YOUR_KEY"[
{
"id": 331,
"event_type": "order.paid",
"status": "delivered",
"attempts": 1,
"last_error": "",
"created_at": "2026-07-08T14:20:00Z",
"delivered_at": "2026-07-08T14:20:01Z"
}
]Notifications
Send WhatsApp notifications programmatically. These endpoints live under a separate base, https://cognichat-api.vendyi.com/api/notify/v1 (a sibling of the Commerce API), and require a secret (`sk_`) key. You send one of your approved templates to a recipient who has opted in; each accepted send draws one message credit from your prepaid balance. A send is idempotent on idempotency_key, so a retry with the same key never sends or charges twice. Delivery is reported back on your notification.sent / notification.delivered / notification.failed webhooks (add notify.* events to a webhook endpoint).
Send a notification
Sends an approved template to one recipient. Returns 402 insufficient_credit when your balance is too low (top up and retry the same key), 403 not_opted_in when the recipient has not opted in for the template's category, 409 template_not_approved / 409 line_paused when the template is not approved or the line is paused for quality, and 422 bad_params when the parameter count does not match the template.
tostringrequiredRecipient phone in E.164 (e.g. +233244000000).
templatestringrequiredThe name of one of your APPROVED templates.
paramsstring[]requiredBody variables in order; length must equal the template's variable count.
idempotency_keystringYour unique id for this send. Reusing it returns the original result without re-sending or re-charging. Auto-generated if omitted.
curl -X POST "https://cognichat-api.vendyi.com/api/notify/v1/messages/" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"+233244000000","template":"appointment_reminder","params":["Ama","your delivery"],"idempotency_key":"appt-9f2c"}'{
"id": 481,
"status": "sent",
"to": "+233244000000",
"template": "appointment_reminder",
"category": "utility",
"wamid": "wamid.HBg...",
"price_pesewas": 15,
"created_at": "2026-07-10T14:20:00Z"
}Get a send's status
Returns the current status of a send: sent, delivered, read, or failed. Status updates arrive on your webhook; poll this if you are not using webhooks.
idintegerrequiredThe send id returned when you created it.
curl -X GET "https://cognichat-api.vendyi.com/api/notify/v1/messages/42/" \
-H "Authorization: Bearer sk_live_YOUR_KEY"{
"id": 481,
"status": "delivered",
"to": "+233244000000",
"template": "appointment_reminder",
"category": "utility",
"wamid": "wamid.HBg...",
"price_pesewas": 15,
"created_at": "2026-07-10T14:20:00Z"
}Register a contact opt-in
Records a recipient's consent so you may notify them. Utility/authentication templates need utility_opt_in; marketing templates need marketing_opt_in. Idempotent per phone (creates or updates the contact).
phonestringrequiredContact phone in E.164 (Ghana-local like 0244000000 is accepted).
namestringDisplay name.
utility_opt_inbooleanConsent to transactional/utility notifications. Defaults to true.
marketing_opt_inbooleanExplicit consent to marketing/promotions. Defaults to false.
curl -X POST "https://cognichat-api.vendyi.com/api/notify/v1/contacts/" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+233244000000","name":"Ama Mensah","marketing_opt_in":true}'{
"phone": "+233244000000",
"utility_opt_in": true,
"marketing_opt_in": true
}