API and webhooks
The Kartlo API and webhooks let a developer connect your store to other systems, such as a warehouse, an ERP or an accounting tool. Both live in SettingsDevelopers and work for one store at a time.
API keys#
- Go to SettingsDevelopers and click Create API key.
- Name the key after the app that will use it and tick only the permissions it needs.
- Copy the key. Kartlo shows it once and keeps only a fingerprint of it, so nobody at Kartlo can read it back.
Send the key on every request as a Bearer token:
curl https://kartlo.app/api/v1/orders?limit=20 \
-H "Authorization: Bearer kartlo_sk_..."| Permission | Allows |
|---|---|
read_products | GET /api/v1/products and /api/v1/products/{id} |
read_orders | GET /api/v1/orders and /api/v1/orders/{id} |
read_customers | GET /api/v1/customers and /api/v1/customers/{id} |
read_inventory | GET /api/v1/inventory |
write_inventory | PATCH /api/v1/inventory/{variantId}, and also reading inventory |
Endpoints#
| Method and path | Returns |
|---|---|
| GET /api/v1/products | Products with their variants and prices, newest first. Cost prices are never included. |
| GET /api/v1/products/{id} | One product. |
| GET /api/v1/orders | Orders with their line items, totals, addresses and statuses, newest first. |
| GET /api/v1/orders/{id} | One order. |
| GET /api/v1/customers | Customers with contact details and order totals, newest first. |
| GET /api/v1/customers/{id} | One customer. |
| GET /api/v1/inventory | Stock per variant: available, reserved, tracking and backorders. |
| PATCH /api/v1/inventory/{variantId} | Sets the available quantity. The change is saved as a stock count correction in the stock history. |
curl -X PATCH https://kartlo.app/api/v1/inventory/VARIANT_ID \
-H "Authorization: Bearer kartlo_sk_..." \
-H "Content-Type: application/json" \
-d '{"available": 12}'Every answer is JSON. A list answer has data and next_cursor; a single record answer has data. An error answer has error.code and error.message.
| Status | Meaning |
|---|---|
| 401 | The key is missing, wrong or revoked. |
| 402 | The store is not on the Pro plan or above. |
| 403 | The key does not have the permission for this endpoint, or the subscription has ended (changes only). |
| 404 | No record with this id in the store of the key. |
| 422 | The body is not valid, for example a negative quantity. |
| 429 | Too many requests. Wait a minute and retry. |
Pagination and rate limits#
Lists return up to limit records (1 to 100, 50 by default). When there are more, the answer has a next_cursor. Send it back as ?cursor= to get the next page, and stop when next_cursor is null.
Each key can make 120 requests a minute. Above that the API answers 429 with a Retry-After header.
Webhooks#
- Go to SettingsDevelopers and click Add webhook.
- Enter an address that starts with
https://and pick the events you want. - Copy the signing secret. Kartlo shows it once.
- Click Send test event to check your server answers.
| Event | Sent when |
|---|---|
| order.created | An order is placed, at checkout or by your team. |
| order.paid | An order becomes paid in full. |
| order.fulfilled | Every item of an order is fulfilled. |
| order.cancelled | An order is cancelled. |
| product.created | A product is added. |
| product.updated | A product or one of its variants changes. |
| customer.created | A customer is added. |
| inventory.updated | The available quantity of a variant changes. |
Kartlo sends a POST with a JSON body and the headers X-Kartlo-Event, X-Kartlo-Delivery (unique per delivery, use it to ignore repeats) and X-Kartlo-Signature. The record in data has the same shape as the API returns, read when the event is first sent.
{
"id": "4f0c...",
"event": "order.paid",
"created_at": "2026-10-24T09:12:03.120Z",
"store_id": "8a1d...",
"data": { "id": "...", "order_number": "1042", "payment_status": "paid", "items": [ ... ] }
}Answer with any 2xx status within 10 seconds. Otherwise Kartlo retries after 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours, 8 attempts in all. Deliveries go out within about 5 minutes of the event. After 50 failed deliveries in a row the webhook turns off and Settings shows why; turn it back on once your server is fixed. Recent deliveries in the webhook shows each attempt, and Retry now sends one again; if that fails too, the normal retries carry on. Delivery history is kept for 30 days.
Checking the signature#
X-Kartlo-Signature looks like t=1792831923,v1=5f2c.... v1 is the HMAC-SHA256, in hex, of the timestamp, a dot and the raw request body, keyed with your signing secret. Compare it in constant time and refuse timestamps older than 5 minutes. Always check the raw body, before parsing the JSON.
import crypto from 'node:crypto';
export function verifyKartlo(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const got = Buffer.from(parts.v1 ?? '', 'hex');
return got.length === 32 && crypto.timingSafeEqual(got, Buffer.from(expected, 'hex'));
}import hmac, hashlib, time
def verify_kartlo(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))