API & webhooks
When the ready-made integrations don’t cover what you need, build your own. Your systems can read and write Syncendio data with an API key, and Syncendio can tell them the moment something happens.
What it’s for
| Use | Example |
|---|---|
| API: your system asks Syncendio | A website shows live stock; a dashboard pulls this week’s orders; another system creates products in bulk |
| Webhooks: Syncendio tells your system | A new order starts your fulfilment process; a shipped order emails your customer; low stock alerts a buyer |
An add-on. API & webhooks is $49 a month on Starter, Standard and Growth, and included in Pro. Workspaces that already had API keys or webhooks before it became an add-on keep them at no charge.
Turn it on
- Open Integrations → API. If the add-on is off, the page says so.
- An Owner clicks Add for $49/month, or turns on API & webhooks under Settings → Add-ons. On Pro it’s already on.
Only an Owner can create API keys and webhooks, because a key can read your whole workspace.
Create an API key
- In Integrations → API, under API keys, name what will use the key, such as “Website stock feed”. Name the system, not the person.
- Tick only the permissions it needs.
- Click Create key, then copy the key straight away. It’s shown once and can’t be retrieved later.
| Permission | Lets the key |
|---|---|
read:products | Read products and their stock |
write:products | Create products |
read:inventory | Read stock by location, locations and stock movements |
write:inventory | Adjust stock and transfer it between locations |
read:orders | Read sales orders, customers and sales invoices |
write:orders | Create sales orders and customers |
read:purchasing | Read purchase orders, suppliers and supplier bills |
write:purchasing | Create purchase orders and suppliers |
read:financials | Read the chart of accounts and journal entries |
manage:webhooks | Add and remove webhooks through the API |
Writes follow the same rules as the app. An order through the API is refused for an inactive customer, over a credit limit, from a location whose stock is out on consignment, or without an exchange rate — exactly as on screen. Orders created with a key are marked as coming from the API.
A key that leaks can be stopped at once with Revoke. Last used shows whether anything still uses a key before you revoke it.
Call the API
The base URL is shown at the top of Integrations → API and ends in /api/v1. Send the key with every request:
curl https://YOUR-API-ADDRESS/api/v1/products?limit=20 \ -H "Authorization: Bearer syn_your_key_here"
| Rule | Detail |
|---|---|
| Authentication | Authorization: Bearer syn_… or X-API-Key: syn_… |
| Finding your way in | The base URL itself — /api/v1 with nothing after it — answers without a key and describes the permissions, paging and errors below. Every other address needs one. |
| Lists | Take limit (up to 200) and offset; return data and pagination with the total |
| Errors | Return error (a code for your program) and message (for a person) |
| Money | In the document’s own currency, always with currency beside it |
| Unknown parameters | Refused with 400, so a typo in a filter never returns everything |
| Rate limit | 600 requests a minute per key; after that, 429 with how long to wait |
Endpoints
| Request | Permission | What it does |
|---|---|---|
GET /products | read:products | Products with stock totals and their attributes. Filter by sku, status, or by attribute |
GET /products/{id or sku} | read:products | One product, with stock per location |
POST /products | write:products | Create a product: sku and name required; optional attributes |
GET /inventory | read:inventory | Stock per product and location. Filter by locationId, lowOnly=true |
GET /inventory/movements | read:inventory | Adjustments, transfers, receipts and counts with their lines. Filter by type, since, productId |
GET /locations | read:inventory | Your locations, with consignment direction and owner |
POST /inventory/adjustments | write:inventory | Adjust stock: productId or sku, locationId, quantityChange, reason. An increase completes at once; a decrease is a draft |
POST /inventory/movements/{id}/authorise, /complete, /cancel | write:inventory | Move a draft adjustment through its stages |
POST /inventory/transfers | write:inventory | Move stock: fromLocationId, toLocationId, lines of productId or sku and quantity |
GET /orders | read:orders | Sales orders. Filter by status, since |
GET /orders/{id or number} | read:orders | One order with its lines and total |
POST /orders | write:orders | Create a sales order: customerId, locationId, lines of productId or sku, quantity and optional unitPrice (left out, the customer’s own price is used). quote: true creates a quote |
GET /customers, GET /customers/{id} | read:orders | Customers with their attributes. Filter by q, status, or by attribute |
POST /customers | write:orders | Create a customer: name; optional email, paymentTermsDays, currency, attributes |
GET /invoices, GET /invoices/{id or number} | read:orders | Sales invoices with total, paid and outstanding. Filter by status, customerId, since |
GET /purchase-orders | read:purchasing | Purchase orders. Filter by status |
POST /purchase-orders | write:purchasing | Create a draft purchase order: supplierId, locationId, lines of productId or sku, quantity, unitCost |
GET /suppliers | read:purchasing | Suppliers with their attributes. Filter by q, or by attribute |
POST /suppliers | write:purchasing | Create a supplier: name and leadTimeDays; optional contact, email, currency |
GET /purchase-invoices | read:purchasing | Supplier bills with total, paid and outstanding. Filter by status, supplierId, since |
GET /accounts | read:financials | The chart of accounts |
GET /journal-entries | read:financials | Journal entries with their lines. Filter by from, to, source |
GET/POST /webhooks, DELETE /webhooks/{id} | manage:webhooks | Manage webhook endpoints from your own code |
Webhooks
- Under Webhooks, choose an event in Tell me when.
- Enter the address in Send it to. It must start with
https://. - Click Add endpoint and copy the signing secret. Like a key, it’s shown once.
- Click Send test, then Attempts to see whether it arrived.
| Event | Sent when |
|---|---|
order.created | A sales order is created |
order.shipped | An order ships, in part or in full |
invoice.created | A sales invoice is raised |
invoice.paid | A sales invoice is paid in full |
purchase_order.created | A purchase order is created |
purchase_order.received | Stock is received against a purchase order |
product.created, product.updated | A product is created or changed |
stock.low | A product falls to or below its reorder point |
If your endpoint doesn’t answer with a 2xx status, the delivery is retried 6 times over about a day (30 seconds, 2 minutes, 8 minutes, 32 minutes, 2 hours, 8 hours). After 20 failures in a row, the endpoint is switched off and the reason is shown; switch it back on once it’s fixed. Retries reuse the same Syncendio-Delivery id, so you can ignore a delivery you’ve already processed.
Check the signature
Every delivery carries Syncendio-Signature: t=<unix time>,v1=<hex>. v1 is an HMAC-SHA256 of <t>.<raw request body>, keyed with the endpoint’s signing secret. Recompute it, compare, and reject anything older than a few minutes. This proves the request came from Syncendio.
// Node.js
const crypto = require('crypto');
function isFromSyncendio(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
If the add-on is switched off
- API calls return 403 with
addon_not_enabled; the keys aren’t deleted. - No webhooks are sent. Events that happen while it’s off show as not sent in Attempts, and aren’t delivered later.
- Turning the add-on back on makes your existing keys and endpoints work again.