Developers

InkeepX Business API

A two-way REST API for your business data. Read and write products, stock, sales, expenses and customers from your own code, Zapier, Make, n8n — or an AI agent.

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.

Base URL
https://inkeepx.com/api/v1
Version
v1 · 1.1.0
Format
JSON over HTTPS
Auth
Bearer API key
Who can use it API access is included on plans that carry the API benefit. The business owner generates keys in Settings → API Access and chooses what each key may read and write.

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 return 404.
  • 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-Version and X-API-Scopes headers 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.

ScopeGrants
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.
Belt and braces for sales Also send your own order number as 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.",
}
StatusCodeMeaning
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:

  1. 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.
  2. Give the agent a key with the narrowest scopes it needs. A reporting assistant gets read scopes only. An order bot gets sales:write and customers:write, not products:write. Revoke and re-issue keys per agent.
  3. Make every write idempotent. Agents retry. Have the agent mint an Idempotency-Key per action (and pass its own order id as a sale reference) 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 /me first and remember currency, settings.payment_methods and whether tax_inclusive is on.
  • Never invent product ids: resolve by sku/barcode or search by name, and ask when more than one matches.
  • Prefer stock adjustments over absolute quantities; only use quantity for a stock take the user explicitly describes.
  • Confirm amounts and the customer before any write that moves money (sales, payments).
  • On 422/409, read error and message and 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 writes Content-Type: application/json plus Idempotency-Key set 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 and sort=updated_at&order=asc; de-duplicate on id.

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.

GET /v1/me

Verify key & account context

any key · returns 200

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"
    }
}
GET /v1/stores

List stores

any key · returns 200

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

GET /v1/products

List products

Scope products:read · returns 200

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

NameDescription
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 /v1/products/{id}

Get a product

Scope products:read · returns 200

One product with its variations. To look a product up by code instead, call the list endpoint with ?sku= or ?barcode=.

Parameters

NameDescription
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"
    }
}
POST /v1/products

Create a product

Scope products:write · accepts Idempotency-Key · returns 201

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)

FieldDescription
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-Key header 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"
    }
}
PATCH /v1/products/{id}

Update a product

Scope products:write · accepts Idempotency-Key · returns 200

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

NameDescription
idrequired
path · integer
The product id.

Request body (application/json)

FieldDescription
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

  • PUT is accepted as an alias of PATCH; 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"
    }
}
POST /v1/products/{id}/stock

Adjust stock

Scope products:write · accepts Idempotency-Key · returns 200

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

NameDescription
idrequired
path · integer
The product id.

Request body (application/json)

FieldDescription
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 /v1/products/{id}

Delete a product

Scope products:write · accepts Idempotency-Key · returns 200

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

NameDescription
idrequired
path · integer
The product id.

Notes

  • Products with transactions answer {"id": 1042, "deleted": false, "deactivated": true} — reactivate with PATCH {"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
    }
}
GET /v1/categories

List categories

Scope products:read · returns 200

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
        }
    ]
}
GET /v1/brands

List brands

Scope products:read · returns 200

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
        }
    ]
}
GET /v1/services

List services

Scope products:read · returns 200

Services the business sells (no stock). Sell one on a sale line with service_id.

Parameters

NameDescription
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

GET /v1/sales

List sales

Scope sales:read · returns 200

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

NameDescription
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 /v1/sales/{id}

Get a sale

Scope sales:read · returns 200

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

NameDescription
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
        }
    }
}
POST /v1/sales

Record a sale

Scope sales:write · accepts Idempotency-Key · returns 201

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)

FieldDescription
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 unique reference): 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
        }
    }
}
PATCH /v1/sales/{id}

Annotate a sale

Scope sales:write · accepts Idempotency-Key · returns 200

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

NameDescription
idrequired
path · integer
The sale id.

Request body (application/json)

FieldDescription
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
        }
    }
}
POST /v1/sales/{id}/payments

Collect a payment on a credit sale

Scope sales:write · accepts Idempotency-Key · returns 200

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

NameDescription
idrequired
path · integer
The sale id.

Request body (application/json)

FieldDescription
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

GET /v1/expenses

List expenses

Scope expenses:read · returns 200

Paginated expenses (newest first) with a summary — the total and a breakdown by category — over the same filters.

Parameters

NameDescription
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 /v1/expenses/{id}

Get an expense

Scope expenses:read · returns 200

One expense.

Parameters

NameDescription
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"
    }
}
GET /v1/expense-categories

List expense categories

Scope expenses:read · returns 200

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
        }
    ]
}
POST /v1/expenses

Record an expense

Scope expenses:write · accepts Idempotency-Key · returns 201

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)

FieldDescription
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-Key so 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"
    }
}
PATCH /v1/expenses/{id}

Update an expense

Scope expenses:write · accepts Idempotency-Key · returns 200

Changes only the fields you send.

Parameters

NameDescription
idrequired
path · integer
The expense id.

Request body (application/json)

FieldDescription
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 /v1/expenses/{id}

Delete an expense

Scope expenses:write · accepts Idempotency-Key · returns 200

Removes the expense (soft delete).

Parameters

NameDescription
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

GET /v1/customers

List customers

Scope customers:read · returns 200

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

NameDescription
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 /v1/customers/{id}

Get a customer

Scope customers:read · returns 200

One customer with balances and spend.

Parameters

NameDescription
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"
    }
}
POST /v1/customers

Create a customer

Scope customers:write · accepts Idempotency-Key · returns 201

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)

FieldDescription
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"
    }
}
PATCH /v1/customers/{id}

Update a customer

Scope customers:write · accepts Idempotency-Key · returns 200

Changes only the fields you send. Balances and points are not editable — they move through sales, payments and the loyalty ledger.

Parameters

NameDescription
idrequired
path · integer
The customer id.

Request body (application/json)

FieldDescription
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-Key retries; 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_stock filter; /me now describes settings and payment methods; OpenAPI document and this reference.
  • 1.0.0 24 Jul 2026 — Read-only launch: /me, /stores, /categories, /brands, /products with updated_since sync.

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.