Introduction
The InkeepX Business API gives your own tools the same view of the business the web app has — and lets them act on it. Read your catalogue, stock levels, sales, expenses and customers; record sales and expenses, adjust stock, collect payments on credit sales and keep customers in sync. Everything an automation writes is a real record: a sale posted here deducts stock, appears in every report and prints as a receipt, exactly as if a cashier rang it up.
Quick start
Create a key
Settings → API Access → Generate key. Pick its scopes (read-only, or read & write per resource). Copy it once — it is never shown again.
Verify it
Call GET /me. You get the business, its currency, the key's scopes and the settings that shape the API's behaviour.
Read, then write
List products or sales, then record an expense or a sale. Send an Idempotency-Key on every write so retries are safe.
curl "https://inkeepx.com/api/v1/me" \
-H "Authorization: Bearer ikx_your_key_here" \
-H "Accept: application/json"
Every successful reply wraps its payload in data; lists add meta and links; every failure is {"error": "<code>", "message": "…"}.
Authentication
Send your key on every request as a Bearer token. X-API-Key: <key> is accepted as an alternative header.
Authorization: Bearer ikx_ab12cd34ef56gh78…
- Keys start with
ikx_and belong to one business. Everything a key sees or changes is scoped to that business — ids from another business simply return404. - Only a hash of the key is stored. If a key leaks, revoke it in Settings and generate a new one; integrations using the old key stop immediately.
- Responses may include cost prices and margins. Treat a key like a password: keep it server-side, never in a browser or a mobile app.
- Every reply carries
X-API-VersionandX-API-Scopesheaders so a client can see what the key is allowed to do.
Scopes & permissions
Each key carries scopes chosen when it is created. Read lets a key see a resource; write lets it create, change and (where allowed) delete. Write never implies read — an automation that only records expenses has no business reading sales. A request without the needed scope gets 403 insufficient_scope naming the scope it lacks.
| Scope | Grants |
|---|---|
| products:read | List products, stock levels, prices, variations, categories, brands and services. |
| products:write | Create and update products, adjust stock, delete products. |
| sales:read | List sales with items, payments and refunds; sales summaries. |
| sales:write | Record sales (deducts stock), add notes, and collect payments on credit sales. |
| expenses:read | List expenses and totals by category. |
| expenses:write | Record, update and delete expenses. |
| customers:read | List customers with balances, loyalty points and spend. |
| customers:write | Create and update customers. |
GET /me and GET /stores work with any valid key. Keys created before scopes existed are read-only.
Rate limits
Limits are counted per key: 300 reads and 120 writes per minute. Over the limit you get 429 rate_limited with a Retry-After header and retry_after in the body — wait that long, then continue. Poll with updated_since rather than re-pulling everything, and page at per_page=100 for bulk reads.
Idempotency & retries
Networks fail, workflows re-run, agents time out. Without protection a retried POST /sales would ring the same sale up twice and deduct stock twice. So every write accepts an Idempotency-Key header:
- Send any unique string (a UUID is ideal) — one per logical request.
- The first delivery runs normally. A retry with the same key and the same body returns the original reply (with
Idempotent-Replayed: true) without running again. Kept for 7 days. - The same key with a different body is refused (
422 idempotency_key_reused) — that is a bug in the caller, not a retry. - Only successful replies are stored: a failed write leaves nothing behind, so the retry runs for real.
reference when recording a sale. It must be unique, so the same order can never become two sales even without the header.
Errors
Failures use conventional HTTP status codes and a stable machine-readable error code you can branch on. The message is written for a person; validation failures add errors (field → messages).
{
"error": "insufficient_stock",
"message": "items[0]: insufficient stock for Golden Penny Semovita 1kg. Available: 2.",
}
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | No key, an unknown key, or a revoked key. |
| 403 | forbidden | The business's plan no longer includes API access. |
| 403 | insufficient_scope | The key does not hold the scope this endpoint needs. The reply lists required_scope and key_scopes. |
| 403 | plan_limit_reached | The plan's product or customer limit is used up. |
| 404 | not_found | No such resource in this business (ids never leak across businesses). |
| 409 | duplicate_name | A product with that name already exists in the store. existing names it — update that one instead. |
| 409 | duplicate_barcode | Another product in the store already carries that barcode. |
| 409 | duplicate_reference | A sale with that reference already exists — the same order is never recorded twice. |
| 409 | duplicate_phone | A customer with that phone number already exists (existing is returned). Pass allow_duplicate_phone: true to insist. |
| 422 | validation_failed | A field is missing or malformed. errors maps each field to its messages. |
| 422 | invalid_parameter | A query parameter (e.g. updated_since) could not be parsed. |
| 422 | invalid_store | store_id does not belong to this business. |
| 422 | customer_not_found | customer_id does not belong to this business. |
| 422 | below_cost | Selling price at or below cost, and the business does not allow selling below cost. |
| 422 | insufficient_stock | The movement or sale would take stock below zero, and negative stock is off. |
| 422 | sale_rejected | A sale line could not be resolved or priced; the message names the line (items[2]: …). |
| 422 | use_stock_endpoint | quantity was sent to PATCH /products/{id}. Use POST /products/{id}/stock. |
| 422 | variation_required | The product tracks stock per variation; send variation_id (the reply lists them). |
| 422 | customer_locked | The sale's customer cannot be changed through the API (it already has one, or money is still owed). |
| 422 | nothing_due | A payment was posted to a sale with no outstanding balance. |
| 422 | payment_rejected | The payment could not be applied (over-payment with nowhere to go, closed register, …). |
| 422 | nothing_to_update | A PATCH with no recognised fields. |
| 422 | idempotency_key_reused | The Idempotency-Key was already used with a different body. Use a fresh key per new request. |
| 422 | write_failed | The database refused the write; the message says why. |
| 429 | rate_limited | Too many requests. Wait retry_after seconds (also in the Retry-After header). |
Pagination, filtering & sync
List endpoints are paginated. Use page and per_page (max 100); the reply's meta gives the total and last page, links.next is the ready-made URL for the next page (null on the last).
{
"data": [ … ],
"meta": { "current_page": 1, "per_page": 25, "total": 412, "last_page": 17 },
"links": { "next": "https://inkeepx.com/api/v1/products?page=2", "prev": null }
}
Incremental sync. Every list accepts updated_since (ISO 8601). Store the time you started your last pull, then ask only for what changed since. For products this includes stock movements from sales and purchases; for sales it includes later payments, refunds and edits.
GET /products?updated_since=2026-09-24T00:00:00Z&sort=updated_at&order=asc&per_page=100
GET /sales?updated_since=2026-09-24T00:00:00Z
Dates and money. Timestamps are ISO 8601 with the business's timezone offset. Amounts are plain decimal numbers in the business currency (see currency on /me) — never strings, never minor units. Quantities allow up to 3 decimals.
AI agents & automations
The API is designed to be driven by software that reasons about what to do next — an AI agent answering "how much Semovita is left?", a bot that turns a WhatsApp order into a sale, a bookkeeper that posts the day's expenses from bank alerts. Three things make it work well:
- Import the OpenAPI document. https://inkeepx.com/api/v1/openapi.json describes every endpoint, parameter, body and example. ChatGPT custom actions, Claude tool definitions, LangChain / OpenAI-agents toolkits, Postman and most code generators read it directly — the agent then knows the schema without you writing tool definitions by hand.
- Give the agent a key with the narrowest scopes it needs. A reporting assistant gets read scopes only. An order bot gets
sales:writeandcustomers:write, notproducts:write. Revoke and re-issue keys per agent. - Make every write idempotent. Agents retry. Have the agent mint an
Idempotency-Keyper action (and pass its own order id as a salereference) so nothing is ever recorded twice.
A minimal tool set for an agent
If you write tool definitions yourself (OpenAI / Anthropic function calling), these five cover most assistants. Each maps to one endpoint:
[
{ "name": "check_stock", "description": "Find products by name, SKU or barcode and return stock and prices.",
"endpoint": "GET /products?search={query}" },
{ "name": "record_sale", "description": "Ring up a sale. Lines by product_id/sku/barcode + quantity. Needs payment_method.",
"endpoint": "POST /sales" },
{ "name": "record_expense", "description": "Record money the business spent (category, description, amount, date).",
"endpoint": "POST /expenses" },
{ "name": "adjust_stock", "description": "Add or remove stock on a product with a signed adjustment and a note.",
"endpoint": "POST /products/{id}/stock" },
{ "name": "sales_summary", "description": "Revenue, tax, collected and outstanding for a date range.",
"endpoint": "GET /sales?from={date}&to={date}" }
]
System-prompt guidance that keeps agents safe
- Call
GET /mefirst and remembercurrency,settings.payment_methodsand whethertax_inclusiveis on. - Never invent product ids: resolve by
sku/barcodeor search by name, and ask when more than one matches. - Prefer stock adjustments over absolute quantities; only use
quantityfor a stock take the user explicitly describes. - Confirm amounts and the customer before any write that moves money (sales, payments).
- On
422/409, readerrorandmessageand fix the request — do not retry the same body blindly.
Zapier, Make & n8n
Any tool with an HTTP / "API request" step works. Configure it once:
- URL:
https://inkeepx.com/api/v1/…Method: GET/POST/PATCH/DELETE as documented. - Headers:
Authorization: Bearer <key>,Accept: application/json, and for writesContent-Type: application/jsonplusIdempotency-Keyset to the trigger's record id (order id, row id, message id). - Body: raw JSON, mapped from the trigger. Numbers as numbers, not text.
- Polling triggers: hit a list endpoint with
updated_since= the last run time andsort=updated_at&order=asc; de-duplicate onid.
Typical flows: new Shopify/WooCommerce order → POST /sales (use the platform order number as reference); bank alert email → POST /expenses; Google Sheet row → POST /products/{id}/stock; daily 6 pm → GET /sales?from=today → Slack/WhatsApp summary.
Recipes
Find-or-create a customer, then sell on credit
GET /customers?phone=08031234567 → data[0].id, or empty
POST /customers {"name": "Amaka Okafor", "phone": "08031234567"} (if empty)
POST /sales {"customer_id": 57, "payment_method": "credit",
"items": [{"sku": "GOL-0007", "quantity": 3}], "reference": "WA-1187"}
POST /sales/8813/payments {"amount": 5160, "payment_method": "transfer"} (when they pay)
Receive a delivery
GET /products?barcode=6151100014429 → data[0].id = 1042
POST /products/1042/stock {"adjustment": 24, "note": "Received — Dangote invoice 2231"}
Day-end summary for a chat channel
GET /sales?from=2026-09-24&to=2026-09-24&per_page=1 → summary.revenue, .collected, .outstanding
GET /expenses?from=2026-09-24&to=2026-09-24&per_page=1 → summary.total_amount
Keep an online catalogue in sync
every 10 min: GET /products?updated_since={last_run}&sort=updated_at&order=asc&per_page=100
follow links.next until null; upsert by id (or sku); set last_run = start time
Account
Verify a key and discover the account it belongs to — currency, stores, settings that change how the API behaves.
Verify key & account context
Confirms the key works and returns what an integration needs before its first real call: the key's scopes, the business currency, the payment methods a sale may use, and the settings that change how the API behaves (tax inclusive pricing, negative stock, loyalty).
Example request
curl "https://inkeepx.com/api/v1/me" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/me", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/me",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"business": {
"id": 1,
"name": "Amaka Stores",
"unique_business_id": "IKX-000001",
"email": "hello@amakastores.ng",
"phone": "08031234567",
"timezone": "Africa/Lagos"
},
"currency": "NGN",
"currency_symbol": "₦",
"plan": "Professional",
"stores_count": 2,
"key": {
"id": 4,
"name": "Bookkeeping bot",
"scopes": [
"products:read",
"sales:read",
"expenses:read",
"expenses:write"
],
"created_at": "2026-09-24T08:00:00+01:00"
},
"settings": {
"tax_enabled": true,
"tax_inclusive": false,
"tax_label": "VAT",
"taxes": [
{
"name": "VAT",
"rate": 7.5
}
],
"loyalty_enabled": true,
"variations_enabled": true,
"allow_negative_stock": false,
"allow_below_cost": false,
"payment_methods": [
"cash",
"card",
"transfer",
"cheque",
"credit",
"split",
"POS Terminal"
]
},
"rate_limits": {
"reads_per_minute": 300,
"writes_per_minute": 120
},
"idempotency": {
"header": "Idempotency-Key",
"kept_for_hours": 168
},
"api_version": "v1",
"documentation": "https://inkeepx.com/api-docs",
"openapi": "https://inkeepx.com/api/v1/openapi.json"
}
}
List stores
The business's stores and warehouses. Products and sales belong to a store; pass a store's id as store_id where a write needs one (the first active store is used when you don't).
Example request
curl "https://inkeepx.com/api/v1/stores" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/stores", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/stores",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 1,
"name": "Main Store",
"type": "retail",
"address": "12 Adeola Odeku St, Lagos",
"phone": "08031234567",
"is_active": true,
"products_count": 412,
"created_at": "2026-01-04T09:00:00+01:00"
},
{
"id": 2,
"name": "Ikeja Warehouse",
"type": "warehouse",
"address": null,
"phone": null,
"is_active": true,
"products_count": 380,
"created_at": "2026-03-19T09:00:00+01:00"
}
]
}
Products & catalogue
Everything about what the business sells: products with cost and selling prices, stock per store, variations, categories, brands and services. Stock moves through a dedicated endpoint so an automation can never overwrite a live till.
Scopes: products:read products:write
List products
Paginated products with cost and selling prices, stock, variations and store. Use updated_since to sync only what changed since your last pull; stock movements touch the product, so sales and restocks are caught too.
Parameters
| Name | Description |
|---|---|
| search query · string |
Free text over name, SKU and barcode. |
| sku query · string |
Exact SKU (product or variation level). |
| barcode query · string |
Exact barcode (product or variation level). |
| category query · string |
Exact category name. |
| brand query · string |
Exact brand name. |
| store_id query · integer |
Only one store. |
| in_stock query · boolean |
1 = quantity above zero only. |
| low_stock query · boolean |
1 = at or below the product's minimum (or the business threshold). |
| has_variations query · boolean |
1 or 0. |
| include_inactive query · boolean |
1 = include inactive products (hidden by default). |
| updated_since query · datetime |
ISO 8601 — only products changed at or after this moment. |
| sort query · string |
name (default), updated_at, created_at, quantity, price. |
| order query · string |
asc (default) or desc. |
| page query · integer |
Page number, from 1. |
| per_page query · integer |
Rows per page, 1–100 (default 25). |
Notes
- Responses include
cost_price— the key belongs to the business, so it may see its own margins. Treat keys like passwords.
Example request
curl "https://inkeepx.com/api/v1/products?search=semovita&in_stock=1&per_page=50" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/products?search=semovita&in_stock=1&per_page=50", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/products?search=semovita&in_stock=1&per_page=50",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 1042,
"name": "Golden Penny Semovita 1kg",
"description": null,
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"price": 1600,
"quantity": 48,
"in_stock": true,
"minimum_quantity": 10,
"is_low_stock": false,
"expiry_date": "2027-03-31",
"has_variations": false,
"variations": [],
"is_active": true,
"is_taxable": true,
"image_url": null,
"store": {
"id": 1,
"name": "Main Store",
"type": "retail"
},
"created_at": "2026-09-01T09:12:44+01:00",
"updated_at": "2026-09-24T08:03:10+01:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"next": "https://inkeepx.com/api/v1/…?page=2",
"prev": null
}
}
Get a product
One product with its variations. To look a product up by code instead, call the list endpoint with ?sku= or ?barcode=.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The product id. |
Example request
curl "https://inkeepx.com/api/v1/products/1042" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/products/1042", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/products/1042",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 1042,
"name": "Golden Penny Semovita 1kg",
"description": null,
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"price": 1600,
"quantity": 48,
"in_stock": true,
"minimum_quantity": 10,
"is_low_stock": false,
"expiry_date": "2027-03-31",
"has_variations": false,
"variations": [],
"is_active": true,
"is_taxable": true,
"image_url": null,
"store": {
"id": 1,
"name": "Main Store",
"type": "retail"
},
"created_at": "2026-09-01T09:12:44+01:00",
"updated_at": "2026-09-24T08:03:10+01:00"
}
}
Create a product
Adds a product to a store with the same rules as the product form: one product per name per store (a clash returns 409 duplicate_name with the existing product, so update that one instead), one barcode per store, selling price above cost unless the business allows below-cost selling, and the plan's product limit. A missing sku is generated from the name.
Request body (application/json)
| Field | Description |
|---|---|
| namerequired string |
Product name (unique within the store). |
| selling_pricerequired number |
Price before tax (under inclusive pricing the till grosses it up). |
| cost_price number |
What it costs the business. Default 0. |
| quantity number |
Opening stock. Default 0. Ignored when variations are sent (their sum is used). |
| store_id integer |
Which store. Default: the first active store. |
| sku string |
Stock code. Generated from the name when omitted. |
| barcode string |
Unique within the store. |
| category string |
Category name; created in the business's list if new. |
| brand string |
Brand name; created if new. |
| unit string |
Unit of measure, e.g. Pack, kg, Litre. |
| description string |
Up to 1000 characters. |
| minimum_quantity number |
Low-stock alert level. |
| expiry_date date |
YYYY-MM-DD. |
| is_active boolean |
Default true. |
| is_taxable boolean |
Whether the flagged tax applies (only meaningful when tax is enabled). |
| image_url string |
A public https:// image URL to show for the product. |
| variations array |
Variants (when the business uses variations). The parent's stock becomes their sum. |
| variations[].color string |
e.g. Red. |
| variations[].size string |
e.g. XL. |
| variations[].value string |
A free label when colour/size don't fit. |
| variations[].sku string |
Variant SKU. |
| variations[].barcode string |
Variant barcode. |
| variations[].quantity number |
Variant stock. |
| variations[].selling_price number |
Variant price; falls back to the parent's. |
Notes
- Send an
Idempotency-Keyheader so a retried request cannot create the product twice.
Example request
curl -X POST "https://inkeepx.com/api/v1/products" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"name": "Golden Penny Semovita 1kg",
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"quantity": 48,
"minimum_quantity": 10,
"expiry_date": "2027-03-31",
"is_taxable": true
}'
const res = await fetch("https://inkeepx.com/api/v1/products", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"name": "Golden Penny Semovita 1kg",
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"quantity": 48,
"minimum_quantity": 10,
"expiry_date": "2027-03-31",
"is_taxable": true
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/products",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Golden Penny Semovita 1kg",
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"quantity": 48,
"minimum_quantity": 10,
"expiry_date": "2027-03-31",
"is_taxable": True
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 201
{
"data": {
"id": 1042,
"name": "Golden Penny Semovita 1kg",
"description": null,
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"price": 1600,
"quantity": 48,
"in_stock": true,
"minimum_quantity": 10,
"is_low_stock": false,
"expiry_date": "2027-03-31",
"has_variations": false,
"variations": [],
"is_active": true,
"is_taxable": true,
"image_url": null,
"store": {
"id": 1,
"name": "Main Store",
"type": "retail"
},
"created_at": "2026-09-01T09:12:44+01:00",
"updated_at": "2026-09-24T08:03:10+01:00"
}
}
Update a product
Changes only the fields you send; everything else is left alone. Renaming re-checks name uniqueness. quantity is refused here (422 use_stock_endpoint) — stock moves through the stock endpoint so an old copy of a figure can never overwrite live sales. Sending variations replaces the set: rows with an id are updated in place, rows without one are added, rows left out are removed.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The product id. |
Request body (application/json)
| Field | Description |
|---|---|
| name string |
|
| selling_price number |
|
| cost_price number |
|
| sku string |
|
| barcode string |
|
| category string |
|
| brand string |
|
| unit string |
|
| description string |
|
| minimum_quantity number |
|
| expiry_date date |
|
| is_active boolean |
false hides the product from the till and the API list without deleting it. |
| is_taxable boolean |
|
| image_url string |
|
| variations array |
Same rows as on create, plus id to update an existing variant. |
Notes
PUTis accepted as an alias ofPATCH; both are partial updates.
Example request
curl -X PATCH "https://inkeepx.com/api/v1/products/1042" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req-7f3a2c" \
-d '{
"selling_price": 1700,
"minimum_quantity": 12
}'
const res = await fetch("https://inkeepx.com/api/v1/products/1042", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"selling_price": 1700,
"minimum_quantity": 12
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.patch(
"https://inkeepx.com/api/v1/products/1042",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"selling_price": 1700,
"minimum_quantity": 12
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 1042,
"name": "Golden Penny Semovita 1kg",
"description": null,
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1700,
"price": 1700,
"quantity": 48,
"in_stock": true,
"minimum_quantity": 12,
"is_low_stock": false,
"expiry_date": "2027-03-31",
"has_variations": false,
"variations": [],
"is_active": true,
"is_taxable": true,
"image_url": null,
"store": {
"id": 1,
"name": "Main Store",
"type": "retail"
},
"created_at": "2026-09-01T09:12:44+01:00",
"updated_at": "2026-09-24T08:03:10+01:00"
}
}
Adjust stock
Moves stock. Send EITHER adjustment (a signed change: 12 received, -3 damaged) OR quantity (an absolute stock-take figure). Adjustments are the safe choice for automations — they apply on top of the live figure, so a till selling at the same moment is never overwritten. A product with variations moves stock per variation_id; the parent becomes their sum. Every movement is written to the product's history.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The product id. |
Request body (application/json)
| Field | Description |
|---|---|
| adjustment number |
Signed change. Exactly one of adjustment / quantity. |
| quantity number |
Absolute new level (stock take). |
| variation_id integer |
Required when the product has variations. |
| note string |
Why, e.g. "Received from supplier inv 2231". Shown in the history. |
Notes
- Taking stock below zero is refused (
422 insufficient_stock) unless the business allows negative stock.
Example request
curl -X POST "https://inkeepx.com/api/v1/products/1042/stock" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"adjustment": 24,
"note": "Received — Dangote invoice 2231"
}'
const res = await fetch("https://inkeepx.com/api/v1/products/1042/stock", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"adjustment": 24,
"note": "Received — Dangote invoice 2231"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/products/1042/stock",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"adjustment": 24,
"note": "Received — Dangote invoice 2231"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 1042,
"name": "Golden Penny Semovita 1kg",
"description": null,
"sku": "GOL-0007",
"barcode": "6151100014429",
"category": "Groceries",
"brand": "Golden Penny",
"unit": "Pack",
"cost_price": 1350,
"selling_price": 1600,
"price": 1600,
"quantity": 72,
"in_stock": true,
"minimum_quantity": 10,
"is_low_stock": false,
"expiry_date": "2027-03-31",
"has_variations": false,
"variations": [],
"is_active": true,
"is_taxable": true,
"image_url": null,
"store": {
"id": 1,
"name": "Main Store",
"type": "retail"
},
"created_at": "2026-09-01T09:12:44+01:00",
"updated_at": "2026-09-24T08:03:10+01:00"
},
"movement": {
"variation_id": null,
"previous_quantity": 48,
"new_quantity": 72,
"adjustment": 24
}
}
Delete a product
Removes the product from the catalogue (a soft delete). A product that has any transaction on record (sale, purchase, invoice, transfer, refund, stock take…) is never deleted — it is deactivated instead and the response carries "deleted": false, "deactivated": true.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The product id. |
Notes
- Products with transactions answer
{"id": 1042, "deleted": false, "deactivated": true}— reactivate withPATCH {"is_active": true}.
Example request
curl -X DELETE "https://inkeepx.com/api/v1/products/1042" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/products/1042", {
method: "DELETE",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.delete(
"https://inkeepx.com/api/v1/products/1042",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 1042,
"deleted": true
}
}
List categories
Distinct product categories in use, with product counts.
Example request
curl "https://inkeepx.com/api/v1/categories" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/categories", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/categories",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"name": "Beverages",
"products_count": 61
},
{
"name": "Groceries",
"products_count": 214
}
]
}
List brands
Distinct product brands in use, with product counts.
Example request
curl "https://inkeepx.com/api/v1/brands" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/brands", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/brands",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"name": "Dangote",
"products_count": 9
},
{
"name": "Golden Penny",
"products_count": 17
}
]
}
List services
Services the business sells (no stock). Sell one on a sale line with service_id.
Parameters
| Name | Description |
|---|---|
| search query · string |
Name contains. |
| include_inactive query · boolean |
1 = include inactive services. |
| page query · integer |
Page number, from 1. |
| per_page query · integer |
Rows per page, 1–100 (default 25). |
Example request
curl "https://inkeepx.com/api/v1/services" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/services", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/services",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 3,
"name": "Home delivery (Lekki)",
"description": null,
"category": "Logistics",
"price": 1500,
"duration": null,
"is_active": true,
"store_id": null,
"created_at": "2026-02-01T09:00:00+01:00",
"updated_at": "2026-02-01T09:00:00+01:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"next": "https://inkeepx.com/api/v1/…?page=2",
"prev": null
}
}
Sales
Every sale with its lines, taxes, payments and refunds. Recording a sale through the API deducts stock, writes product history and updates customer credit and loyalty — exactly like the till.
Scopes: sales:read sales:write
List sales
Paginated sales (newest first) plus a summary — count, revenue, tax, collected, outstanding, discount, refunded — computed over the same filters, so "sales this week" is one call. Revenue excludes any shipping pass-through. updated_since catches later payments, refunds and edits, not just new sales.
Parameters
| Name | Description |
|---|---|
| from query · datetime |
Sold at or after — a date (2026-09-01) or ISO timestamp. |
| to query · datetime |
Sold at or before (a bare date covers the whole day). |
| updated_since query · datetime |
Changed at or after (payments, refunds, edits included). |
| status query · string |
paid, partial, credit, outstanding (anything still owed), refunded (full or part), partial_refund, cancelled. |
| payment_method query · string |
Exact, e.g. cash, transfer. |
| customer_id query · integer |
|
| store_id query · integer |
|
| reference query · string |
Exact receipt reference. |
| channel query · string |
Where the sale came from, e.g. API, WooCommerce, Shopify. |
| search query · string |
Reference or customer name contains. |
| sort query · string |
created_at (default), updated_at, total. |
| order query · string |
desc (default) or asc. |
| page query · integer |
Page number, from 1. |
| per_page query · integer |
Rows per page, 1–100 (default 25). |
Example request
curl "https://inkeepx.com/api/v1/sales?from=2026-09-01&to=2026-09-30&status=outstanding" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/sales?from=2026-09-01&to=2026-09-30&status=outstanding", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/sales?from=2026-09-01&to=2026-09-30&status=outstanding",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 8813,
"reference": "API-260924-K3X9Q",
"store": {
"id": 1,
"name": "Main Store"
},
"customer": {
"id": 57,
"name": "Amaka Okafor",
"phone": "08031234567",
"email": null
},
"served_by": "Business Owner",
"channel": "API",
"subtotal": 4800,
"discount": 0,
"tax": 360,
"shipping_fee": 0,
"total": 5160,
"amount_paid": 5160,
"amount_due": 0,
"refund_amount": 0,
"payment_method": "transfer",
"payment_status": "paid",
"is_credit": false,
"due_date": null,
"notes": "WhatsApp order #1187",
"sold_at": "2026-09-24T10:41:03+01:00",
"created_at": "2026-09-24T10:41:03+01:00",
"updated_at": "2026-09-24T10:41:03+01:00",
"items_count": 1
}
],
"summary": {
"count": 1,
"revenue": 5160,
"tax": 360,
"collected": 5160,
"outstanding": 0,
"discount": 0,
"refunded": 0
},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"next": "https://inkeepx.com/api/v1/…?page=2",
"prev": null
}
}
Get a sale
A sale in full: lines (with the variation sold and what has been refunded on each), the per-tax split, split-payment rows, payments collected later on a credit sale, refund details and loyalty points.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The sale id. |
Example request
curl "https://inkeepx.com/api/v1/sales/8813" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/sales/8813", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/sales/8813",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 8813,
"reference": "API-260924-K3X9Q",
"store": {
"id": 1,
"name": "Main Store"
},
"customer": {
"id": 57,
"name": "Amaka Okafor",
"phone": "08031234567",
"email": null
},
"served_by": "Business Owner",
"channel": "API",
"subtotal": 4800,
"discount": 0,
"tax": 360,
"shipping_fee": 0,
"total": 5160,
"amount_paid": 5160,
"amount_due": 0,
"refund_amount": 0,
"payment_method": "transfer",
"payment_status": "paid",
"is_credit": false,
"due_date": null,
"notes": "WhatsApp order #1187",
"sold_at": "2026-09-24T10:41:03+01:00",
"created_at": "2026-09-24T10:41:03+01:00",
"updated_at": "2026-09-24T10:41:03+01:00",
"items": [
{
"id": 20411,
"item_type": "product",
"product_id": 1042,
"variation_id": null,
"service_id": null,
"name": "Golden Penny Semovita 1kg",
"variation": null,
"sku": "GOL-0007",
"unit": "Pack",
"quantity": 3,
"unit_price": 1600,
"total_price": 4800,
"quantity_refunded": 0
}
],
"taxes": [
{
"name": "VAT",
"rate": 7.5,
"amount": 360
}
],
"split_payments": [],
"credit_payments": [],
"refund": null,
"loyalty": {
"points_earned": 5,
"points_redeemed": 0
}
}
}
Record a sale
Rings up a sale exactly as the till would. Each line is a product (product_id, plus variation_id when it has variations), a service (service_id), or a code (sku / barcode, product or variation level) within the store. A line without unit_price is charged at the catalogue price (grossed up when pricing is tax inclusive); a line with one is charged at that price. Tax is worked out from the business's tax rules unless you send tax. In one transaction: stock is deducted (batches, variations and recipe ingredients included), product history is written, a credit balance goes onto the customer, loyalty points are earned. Any line that cannot be resolved or would overdraw stock fails the whole sale — nothing is half-recorded.
Request body (application/json)
| Field | Description |
|---|---|
| itemsrequired array |
One or more lines (max 200). |
| items[].product_id integer |
A product in the store. |
| items[].variation_id integer |
Which variant (required when the product has variations). |
| items[].service_id integer |
A service instead of a product. |
| items[].sku string |
Resolve the line by SKU instead of an id. |
| items[].barcode string |
Resolve the line by barcode instead of an id. |
| items[].quantityrequired number |
Up to 3 decimals (e.g. 2.5 kg). |
| items[].unit_price number |
Price actually charged per unit. Omit to use the catalogue price. |
| payment_methodrequired string |
cash, card, transfer, cheque, credit, split, or one of the business's custom methods (see GET /me → settings.payment_methods). |
| payments array |
For split only: how the money came in. |
| payments[].method string |
e.g. cash. |
| payments[].amount number |
|
| payments[].reference string |
Transfer/terminal reference. |
| amount_paid number |
Default: the full total (0 for credit). Less than the total makes a part-paid sale — needs a customer. |
| customer_id integer |
Required for credit / part-paid sales; earns loyalty points when the program is on. |
| store_id integer |
Default: the first active store. |
| discount number |
Amount off the subtotal. |
| tax number |
Override the computed tax (e.g. a marketplace already charged it). |
| reference string |
Your own order number. Must be unique — a repeat returns 409 duplicate_reference with the sale already recorded. |
| notes string |
Up to 500 characters; printed on the receipt. |
| sold_at datetime |
Back-date the sale (ISO 8601). Default: now. |
| due_date date |
Pay-by date (YYYY-MM-DD) for a credit or part-paid sale. Shows on the Credit Sales page and drives the due / overdue reminders. Ignored when nothing is owed. |
Notes
- Always send an
Idempotency-Key(or a uniquereference): a retried request must never ring the sale up twice. wallet(prepaid balance) and hotel room charges are till-only payment methods.- A sale recorded here is a real sale: it appears in every report, prints as a receipt, and can be refunded or edited on the web.
Example request
curl -X POST "https://inkeepx.com/api/v1/sales" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"store_id": 1,
"customer_id": 57,
"items": [
{
"product_id": 1042,
"quantity": 3
},
{
"sku": "DAN-0002",
"quantity": 1,
"unit_price": 5200
}
],
"payment_method": "transfer",
"reference": "WA-1187",
"notes": "WhatsApp order #1187"
}'
const res = await fetch("https://inkeepx.com/api/v1/sales", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"store_id": 1,
"customer_id": 57,
"items": [
{
"product_id": 1042,
"quantity": 3
},
{
"sku": "DAN-0002",
"quantity": 1,
"unit_price": 5200
}
],
"payment_method": "transfer",
"reference": "WA-1187",
"notes": "WhatsApp order #1187"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/sales",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"store_id": 1,
"customer_id": 57,
"items": [
{
"product_id": 1042,
"quantity": 3
},
{
"sku": "DAN-0002",
"quantity": 1,
"unit_price": 5200
}
],
"payment_method": "transfer",
"reference": "WA-1187",
"notes": "WhatsApp order #1187"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 201
{
"data": {
"id": 8813,
"reference": "API-260924-K3X9Q",
"store": {
"id": 1,
"name": "Main Store"
},
"customer": {
"id": 57,
"name": "Amaka Okafor",
"phone": "08031234567",
"email": null
},
"served_by": "Business Owner",
"channel": "API",
"subtotal": 4800,
"discount": 0,
"tax": 360,
"shipping_fee": 0,
"total": 5160,
"amount_paid": 5160,
"amount_due": 0,
"refund_amount": 0,
"payment_method": "transfer",
"payment_status": "paid",
"is_credit": false,
"due_date": null,
"notes": "WhatsApp order #1187",
"sold_at": "2026-09-24T10:41:03+01:00",
"created_at": "2026-09-24T10:41:03+01:00",
"updated_at": "2026-09-24T10:41:03+01:00",
"items": [
{
"id": 20411,
"item_type": "product",
"product_id": 1042,
"variation_id": null,
"service_id": null,
"name": "Golden Penny Semovita 1kg",
"variation": null,
"sku": "GOL-0007",
"unit": "Pack",
"quantity": 3,
"unit_price": 1600,
"total_price": 4800,
"quantity_refunded": 0
}
],
"taxes": [
{
"name": "VAT",
"rate": 7.5,
"amount": 360
}
],
"split_payments": [],
"credit_payments": [],
"refund": null,
"loyalty": {
"points_earned": 5,
"points_redeemed": 0
}
}
}
Annotate a sale
Changes notes, and can attach a customer_id to a fully paid sale that has none. Everything else on a sale moves stock or money, and those corrections belong on the web's edit and refund screens where a person confirms them.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The sale id. |
Request body (application/json)
| Field | Description |
|---|---|
| notes string |
Replaces the note. |
| customer_id integer |
Only when the sale has no customer and nothing is owed. |
Example request
curl -X PATCH "https://inkeepx.com/api/v1/sales/8813" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req-7f3a2c" \
-d '{
"notes": "Delivered 24 Sep, signed by gateman"
}'
const res = await fetch("https://inkeepx.com/api/v1/sales/8813", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"notes": "Delivered 24 Sep, signed by gateman"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.patch(
"https://inkeepx.com/api/v1/sales/8813",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"notes": "Delivered 24 Sep, signed by gateman"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 8813,
"reference": "API-260924-K3X9Q",
"store": {
"id": 1,
"name": "Main Store"
},
"customer": {
"id": 57,
"name": "Amaka Okafor",
"phone": "08031234567",
"email": null
},
"served_by": "Business Owner",
"channel": "API",
"subtotal": 4800,
"discount": 0,
"tax": 360,
"shipping_fee": 0,
"total": 5160,
"amount_paid": 5160,
"amount_due": 0,
"refund_amount": 0,
"payment_method": "transfer",
"payment_status": "paid",
"is_credit": false,
"due_date": null,
"notes": "Delivered 24 Sep, signed by gateman",
"sold_at": "2026-09-24T10:41:03+01:00",
"created_at": "2026-09-24T10:41:03+01:00",
"updated_at": "2026-09-24T10:41:03+01:00",
"items": [
{
"id": 20411,
"item_type": "product",
"product_id": 1042,
"variation_id": null,
"service_id": null,
"name": "Golden Penny Semovita 1kg",
"variation": null,
"sku": "GOL-0007",
"unit": "Pack",
"quantity": 3,
"unit_price": 1600,
"total_price": 4800,
"quantity_refunded": 0
}
],
"taxes": [
{
"name": "VAT",
"rate": 7.5,
"amount": 360
}
],
"split_payments": [],
"credit_payments": [],
"refund": null,
"loyalty": {
"points_earned": 5,
"points_redeemed": 0
}
}
}
Collect a payment on a credit sale
Records money received against a sale that still has a balance (a credit or part-paid sale) and reduces the customer's debt. Money over the balance is refused unless you say where it goes: apply_excess_to_outstanding settles the customer's other balances oldest-first, deposit_excess_to_wallet banks it as prepaid credit.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The sale id. |
Request body (application/json)
| Field | Description |
|---|---|
| amountrequired number |
|
| payment_method string |
How it came in, e.g. transfer. |
| reference string |
Transfer reference. |
| notes string |
|
| apply_excess_to_outstanding boolean |
Surplus → the customer's other balances. |
| deposit_excess_to_wallet boolean |
Surplus → the customer's prepaid balance. |
Notes
- Refunds are not available through the API; they are confirmed by a person on the web or mobile app.
Example request
curl -X POST "https://inkeepx.com/api/v1/sales/8813/payments" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"amount": 5000,
"payment_method": "transfer",
"reference": "GTB-88213"
}'
const res = await fetch("https://inkeepx.com/api/v1/sales/8813/payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 5000,
"payment_method": "transfer",
"reference": "GTB-88213"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/sales/8813/payments",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 5000,
"payment_method": "transfer",
"reference": "GTB-88213"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 8813,
"reference": "API-260924-K3X9Q",
"store": {
"id": 1,
"name": "Main Store"
},
"customer": {
"id": 57,
"name": "Amaka Okafor",
"phone": "08031234567",
"email": null
},
"served_by": "Business Owner",
"channel": "API",
"subtotal": 4800,
"discount": 0,
"tax": 360,
"shipping_fee": 0,
"total": 5160,
"amount_paid": 5000,
"amount_due": 160,
"refund_amount": 0,
"payment_method": "credit",
"payment_status": "partial",
"is_credit": true,
"due_date": "2026-10-24",
"notes": "WhatsApp order #1187",
"sold_at": "2026-09-24T10:41:03+01:00",
"created_at": "2026-09-24T10:41:03+01:00",
"updated_at": "2026-09-24T10:41:03+01:00",
"items": [
{
"id": 20411,
"item_type": "product",
"product_id": 1042,
"variation_id": null,
"service_id": null,
"name": "Golden Penny Semovita 1kg",
"variation": null,
"sku": "GOL-0007",
"unit": "Pack",
"quantity": 3,
"unit_price": 1600,
"total_price": 4800,
"quantity_refunded": 0
}
],
"taxes": [
{
"name": "VAT",
"rate": 7.5,
"amount": 360
}
],
"split_payments": [],
"credit_payments": [
{
"id": 771,
"amount": 5000,
"method": "transfer",
"reference": "GTB-88213",
"notes": null,
"taken_by": "Business Owner",
"paid_at": "2026-09-24T12:00:00+01:00"
}
],
"refund": null,
"loyalty": {
"points_earned": 5,
"points_redeemed": 0
}
},
"allocation": {
"payment_id": 771,
"to_sale": 5000,
"to_other_balances": 0,
"to_wallet": 0
}
}
Expenses
What the business spends, by category and store. The resource accounting tools and AI bookkeepers post to, so profit reports stay right.
Scopes: expenses:read expenses:write
List expenses
Paginated expenses (newest first) with a summary — the total and a breakdown by category — over the same filters.
Parameters
| Name | Description |
|---|---|
| from query · date |
expense_date on or after. |
| to query · date |
expense_date on or before. |
| updated_since query · datetime |
Created or edited at or after. |
| category query · string |
Exact. |
| store_id query · integer |
One store (business-wide expenses are always included). |
| payment_method query · string |
Exact. |
| search query · string |
Description, category or notes contains. |
| sort query · string |
expense_date (default), amount, created_at, updated_at. |
| order query · string |
desc (default) or asc. |
| page query · integer |
Page number, from 1. |
| per_page query · integer |
Rows per page, 1–100 (default 25). |
Example request
curl "https://inkeepx.com/api/v1/expenses?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/expenses?from=2026-09-01&to=2026-09-30", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/expenses?from=2026-09-01&to=2026-09-30",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 391,
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"payment_method": "transfer",
"expense_date": "2026-09-23",
"notes": "Meter 0451-2231",
"store": {
"id": 1,
"name": "Main Store"
},
"recorded_by": "Business Owner",
"created_at": "2026-09-23T16:02:11+01:00",
"updated_at": "2026-09-23T16:02:11+01:00"
}
],
"summary": {
"total_amount": 42500,
"by_category": [
{
"category": "Utilities",
"total": 42500,
"count": 1
}
]
},
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"next": "https://inkeepx.com/api/v1/…?page=2",
"prev": null
}
}
Get an expense
One expense.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The expense id. |
Example request
curl "https://inkeepx.com/api/v1/expenses/391" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/expenses/391", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/expenses/391",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 391,
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"payment_method": "transfer",
"expense_date": "2026-09-23",
"notes": "Meter 0451-2231",
"store": {
"id": 1,
"name": "Main Store"
},
"recorded_by": "Business Owner",
"created_at": "2026-09-23T16:02:11+01:00",
"updated_at": "2026-09-23T16:02:11+01:00"
}
}
List expense categories
The suggested categories plus every category this business has already used, with counts and totals. Any string is accepted as a category when recording an expense; reusing these keeps reports tidy.
Example request
curl "https://inkeepx.com/api/v1/expense-categories" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/expense-categories", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/expense-categories",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"name": "Utilities",
"expenses_count": 14,
"total": 388200
},
{
"name": "Rent",
"expenses_count": 2,
"total": 1200000
},
{
"name": "Supplies",
"expenses_count": 0,
"total": 0
}
]
}
Record an expense
Records what the business spent. Attributed to the business owner and labelled with the key's name; the owner and staff who watch expenses are notified in-app.
Request body (application/json)
| Field | Description |
|---|---|
| categoryrequired string |
e.g. Utilities, Rent, Supplies, Transport, Salaries, Marketing, Other. |
| descriptionrequired string |
What it was for. |
| amountrequired number |
Greater than zero. |
| expense_date date |
YYYY-MM-DD. Default: today. |
| store_id integer |
Default: the first active store. |
| payment_method string |
e.g. Cash, Bank Transfer, Card, Cheque. |
| notes string |
Up to 2000 characters. |
Notes
- Send an
Idempotency-Keyso a retried request cannot record the expense twice.
Example request
curl -X POST "https://inkeepx.com/api/v1/expenses" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"expense_date": "2026-09-23",
"payment_method": "transfer",
"notes": "Meter 0451-2231"
}'
const res = await fetch("https://inkeepx.com/api/v1/expenses", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"expense_date": "2026-09-23",
"payment_method": "transfer",
"notes": "Meter 0451-2231"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/expenses",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"expense_date": "2026-09-23",
"payment_method": "transfer",
"notes": "Meter 0451-2231"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 201
{
"data": {
"id": 391,
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 42500,
"payment_method": "transfer",
"expense_date": "2026-09-23",
"notes": "Meter 0451-2231",
"store": {
"id": 1,
"name": "Main Store"
},
"recorded_by": "Business Owner",
"created_at": "2026-09-23T16:02:11+01:00",
"updated_at": "2026-09-23T16:02:11+01:00"
}
}
Update an expense
Changes only the fields you send.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The expense id. |
Request body (application/json)
| Field | Description |
|---|---|
| category string |
|
| description string |
|
| amount number |
|
| expense_date date |
|
| store_id integer |
null makes it business-wide. |
| payment_method string |
|
| notes string |
Example request
curl -X PATCH "https://inkeepx.com/api/v1/expenses/391" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req-7f3a2c" \
-d '{
"amount": 43100,
"notes": "Corrected after reconciliation"
}'
const res = await fetch("https://inkeepx.com/api/v1/expenses/391", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 43100,
"notes": "Corrected after reconciliation"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.patch(
"https://inkeepx.com/api/v1/expenses/391",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 43100,
"notes": "Corrected after reconciliation"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 391,
"category": "Utilities",
"description": "PHCN electricity bill — September",
"amount": 43100,
"payment_method": "transfer",
"expense_date": "2026-09-23",
"notes": "Corrected after reconciliation",
"store": {
"id": 1,
"name": "Main Store"
},
"recorded_by": "Business Owner",
"created_at": "2026-09-23T16:02:11+01:00",
"updated_at": "2026-09-23T16:02:11+01:00"
}
}
Delete an expense
Removes the expense (soft delete).
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The expense id. |
Example request
curl -X DELETE "https://inkeepx.com/api/v1/expenses/391" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/expenses/391", {
method: "DELETE",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.delete(
"https://inkeepx.com/api/v1/expenses/391",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 391,
"deleted": true
}
}
Customers
Customers with what they owe, their prepaid balance and loyalty points. Balances are read-only: money moves through sales and payments.
Scopes: customers:read customers:write
List customers
Paginated customers with what they owe (credit_used), prepaid balance, loyalty points and lifetime spend. phone / email are exact lookups for find-or-create flows.
Parameters
| Name | Description |
|---|---|
| search query · string |
Name, phone, email or code contains. |
| phone query · string |
Exact phone. |
| email query · string |
Exact email. |
| has_debt query · boolean |
1 = only customers who owe money. |
| updated_since query · datetime |
|
| sort query · string |
name (default), created_at, updated_at, credit_used, loyalty_points. |
| order query · string |
asc (default) or desc. |
| page query · integer |
Page number, from 1. |
| per_page query · integer |
Rows per page, 1–100 (default 25). |
Example request
curl "https://inkeepx.com/api/v1/customers?has_debt=1&sort=credit_used&order=desc" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/customers?has_debt=1&sort=credit_used&order=desc", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/customers?has_debt=1&sort=credit_used&order=desc",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": [
{
"id": 57,
"name": "Amaka Okafor",
"code": null,
"phone": "08031234567",
"email": "amaka@example.com",
"address": "12 Adeola Odeku St",
"city": "Lagos",
"state": "Lagos",
"birth_date": null,
"gender": "F",
"notes": null,
"credit_limit": 50000,
"credit_used": 12000,
"deposit_balance": 0,
"loyalty_points": 140,
"total_spent": 218400,
"total_purchases": 31,
"created_at": "2026-02-11T12:00:00+01:00",
"updated_at": "2026-09-24T10:41:03+01:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"next": "https://inkeepx.com/api/v1/…?page=2",
"prev": null
}
}
Get a customer
One customer with balances and spend.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The customer id. |
Example request
curl "https://inkeepx.com/api/v1/customers/57" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json"
const res = await fetch("https://inkeepx.com/api/v1/customers/57", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
},
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests
r = requests.get(
"https://inkeepx.com/api/v1/customers/57",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 57,
"name": "Amaka Okafor",
"code": null,
"phone": "08031234567",
"email": "amaka@example.com",
"address": "12 Adeola Odeku St",
"city": "Lagos",
"state": "Lagos",
"birth_date": null,
"gender": "F",
"notes": null,
"credit_limit": 50000,
"credit_used": 12000,
"deposit_balance": 0,
"loyalty_points": 140,
"total_spent": 218400,
"total_purchases": 31,
"created_at": "2026-02-11T12:00:00+01:00",
"updated_at": "2026-09-24T10:41:03+01:00"
}
}
Create a customer
Adds a customer. A phone number already on another customer is refused with 409 duplicate_phone (and that customer returned) so a retried or careless automation cannot fill the list with duplicates; send allow_duplicate_phone: true when two people really share a number. Subject to the plan's customer limit.
Request body (application/json)
| Field | Description |
|---|---|
| namerequired string |
|
| phone string |
Up to 20 characters. |
| email string |
|
| code string |
Your own customer number. |
| address string |
|
| city string |
|
| state string |
|
| birth_date date |
YYYY-MM-DD. |
| gender string |
M, F or Other. |
| credit_limit number |
How much they may owe (informational at the till). |
| notes string |
|
| allow_duplicate_phone boolean |
Skip the duplicate-phone check. |
Example request
curl -X POST "https://inkeepx.com/api/v1/customers" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1187" \
-d '{
"name": "Amaka Okafor",
"phone": "08031234567",
"email": "amaka@example.com",
"city": "Lagos",
"state": "Lagos",
"credit_limit": 50000
}'
const res = await fetch("https://inkeepx.com/api/v1/customers", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"name": "Amaka Okafor",
"phone": "08031234567",
"email": "amaka@example.com",
"city": "Lagos",
"state": "Lagos",
"credit_limit": 50000
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.post(
"https://inkeepx.com/api/v1/customers",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Amaka Okafor",
"phone": "08031234567",
"email": "amaka@example.com",
"city": "Lagos",
"state": "Lagos",
"credit_limit": 50000
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 201
{
"data": {
"id": 57,
"name": "Amaka Okafor",
"code": null,
"phone": "08031234567",
"email": "amaka@example.com",
"address": "12 Adeola Odeku St",
"city": "Lagos",
"state": "Lagos",
"birth_date": null,
"gender": "F",
"notes": null,
"credit_limit": 50000,
"credit_used": 0,
"deposit_balance": 0,
"loyalty_points": 0,
"total_spent": 0,
"total_purchases": 0,
"created_at": "2026-02-11T12:00:00+01:00",
"updated_at": "2026-09-24T10:41:03+01:00"
}
}
Update a customer
Changes only the fields you send. Balances and points are not editable — they move through sales, payments and the loyalty ledger.
Parameters
| Name | Description |
|---|---|
| idrequired path · integer |
The customer id. |
Request body (application/json)
| Field | Description |
|---|---|
| name string |
|
| phone string |
|
| email string |
|
| code string |
|
| address string |
|
| city string |
|
| state string |
|
| birth_date date |
|
| gender string |
|
| credit_limit number |
|
| notes string |
Example request
curl -X PATCH "https://inkeepx.com/api/v1/customers/57" \
-H "Authorization: Bearer $INKEEPX_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req-7f3a2c" \
-d '{
"email": "amaka.okafor@example.com",
"notes": "Prefers WhatsApp"
}'
const res = await fetch("https://inkeepx.com/api/v1/customers/57", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.INKEEPX_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"email": "amaka.okafor@example.com",
"notes": "Prefers WhatsApp"
}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${payload.error}: ${payload.message}`);
console.log(payload.data);
import os, requests, uuid
r = requests.patch(
"https://inkeepx.com/api/v1/customers/57",
headers={
"Authorization": f"Bearer {os.environ['INKEEPX_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"email": "amaka.okafor@example.com",
"notes": "Prefers WhatsApp"
},
)
payload = r.json()
if not r.ok:
raise RuntimeError(f"{payload['error']}: {payload['message']}")
print(payload["data"])
Example response 200
{
"data": {
"id": 57,
"name": "Amaka Okafor",
"code": null,
"phone": "08031234567",
"email": "amaka.okafor@example.com",
"address": "12 Adeola Odeku St",
"city": "Lagos",
"state": "Lagos",
"birth_date": null,
"gender": "F",
"notes": "Prefers WhatsApp",
"credit_limit": 50000,
"credit_used": 12000,
"deposit_balance": 0,
"loyalty_points": 140,
"total_spent": 218400,
"total_purchases": 31,
"created_at": "2026-02-11T12:00:00+01:00",
"updated_at": "2026-09-24T10:41:03+01:00"
}
}
OpenAPI specification
The whole API is published as an OpenAPI 3.1 document, generated from the same source as this page:
https://inkeepx.com/api/v1/openapi.json
Import it into Postman or Insomnia for a ready-made collection, into Swagger UI or Redoc to browse, into ChatGPT custom actions or an agent framework as the tool schema, or run it through an OpenAPI code generator for a typed client in your language. It is public, CORS-open and cached for an hour.
Changelog
- 1.1.0 24 Sep 2026 — The API is now two-way. Per-key scopes;
Idempotency-Keyretries; per-key rate limits with JSON 429s. New: create/update/delete products, adjust stock, list/record sales, annotate sales, collect credit payments, full expenses CRUD, customers read/write, services, expense categories;low_stockfilter;/menow describes settings and payment methods; OpenAPI document and this reference. - 1.0.0 24 Jul 2026 — Read-only launch:
/me,/stores,/categories,/brands,/productswithupdated_sincesync.
Support
Stuck, or need an endpoint that isn't here? Email support@inkeepx.com with the request you sent (minus the key), the reply you got and the X-Request-Id if your client logs one. Refunds and sale edits are deliberately not exposed — they are confirmed by a person on the web or mobile app.