REST · JSONScoped API keysHMAC-signed webhooksv1

Public REST API & Webhooks

Connect ERPs, couriers, warehouse scanners, CRMs and custom apps to your Joomni store, using scoped secret keys and real-time, HMAC-signed webhooks.

BASEhttps://api.joomni.com/api/v1
120
requests / min per key
10
webhook topics
3
example languages
In the dashboard:Settings › API & WebhooksPlan:Business and above (and during free trial)

Get started

Quick Start

From zero to your first response in about two minutes.

  1. 1

    Create an API key

    In Settings › API & Webhooks, create a key with only the scopes you need. Copy it now: it is shown once.
  2. 2

    Make your first call

    Send it as Authorization: Bearer. Your store’s name in the response means the key works.
  3. 3

    Add a webhook

    Add an https:// endpoint per topic, copy its signing secret, and press Send test.
GET /storebash
curl https://api.joomni.com/api/v1/store \
-H "Authorization: Bearer jk_live_your_key_here"

Security

Authentication & API Keys

Every request needs a secret key starting with jk_live_. A key belongs to one store and can only do what its scopes allow.

Authorization
Bearer jk_live_your_key_here
X-Joomni-Access-Token
Alternative to Authorization, if your HTTP client makes that header awkward.

Keys are server-side secrets

Never put a key in browser JavaScript, a mobile app or a git repository. Anyone holding it can act on your store within its scopes. Revoke a leaked key at once in Settings › API & Webhooks.

Permissions

Permission Scopes

Grant each key the smallest set it needs. A request outside the key’s scopes returns 403.

store:read
Store name, currency, timezone, locale and vertical.
products:read
List, search and read products with their variants.
products:write
Create, update and delete products.
orders:read
List and read orders, line items, shipments and buyer details.
orders:write
Create orders, fulfill them with tracking, and cancel with restock.
inventory:write
Adjust stock by a delta or set an absolute level, with an audit reason.
leads:read
Read contact-form inquiries, quote requests and AI qualification scores.
leads:write
Create leads and update their status and tags.
bookings:read
Read bookable services, free slots and appointments with their customer details. Needs the Appointments & Bookings extension.
bookings:write
Book, reschedule, confirm, complete and cancel appointments.
reviews:read
Read product reviews, ratings, photos and store replies. Needs the Product Reviews extension. Reviewer emails are never included.
reviews:write
Publish, hide or mark reviews as spam, and post the store’s public reply.

inventory:read, customers:* and webhooks:manage appear in the key form but have no public endpoints yet. Bookings and reviews endpoints answer 403 with EXTENSION_NOT_INSTALLED while their extension isn’t installed on the store.

Reference

Endpoints

All paths are relative to https://api.joomni.com/api/v1. Request and response bodies are JSON.

Products

Catalog items and their variants.

products:readproducts:write
  • GET/products

    List products. Query: page, limit (max 100), status (ACTIVE, DRAFT, ARCHIVED), search, collectionId.

  • GET/products/:id

    One product with all its variants.

  • POST/products

    Create a product. title and price are required; the handle is generated if omitted.

  • PUT/products/:id

    Update any subset of fields.

  • DELETE/products/:id

    Delete a product. Past orders keep their line-item snapshot.

Orders & Fulfillment

Read orders, push tracking, cancel with restock.

orders:readorders:write
  • GET/orders

    List orders, newest first. status takes an order (PENDING), payment (PAID) or fulfillment (UNFULFILLED) status. Also page, limit, search.

  • GET/orders/:id

    Full order with line items, shipments and customer.

  • POST/orders

    Create an order. Prices and totals are recorded as sent, like a dashboard order.

  • POST/orders/:id/fulfill

    Mark fulfilled. trackingNumber, trackingCompany and trackingUrl are saved as a shipment. Emails the customer.

  • POST/orders/:id/cancel

    Cancel. restockItems defaults to true; an optional reason goes on the order timeline.

Inventory

Stock updates for warehouses and barcode scanners.

inventory:write
  • POST/inventory/adjust

    Send productId or variantId, then adjustment (± delta) or level (absolute count).

POST /inventory/adjustjson
{
"variantId": "cmg1var0000001",
"adjustment": -1,
"reason": "MANUAL_ADJUSTMENT"
}

Leads & Inquiries

Sync contact forms and quote requests with a CRM.

leads:readleads:write
  • GET/leads

    List leads. Query: page, limit, status.

  • GET/leads/:id

    One lead, with AI intent and qualification score when AI features are on.

  • POST/leads

    Create a lead. firstName, email and message are required.

  • PUT/leads/:id

    Update status (NEW, CONTACTED, QUALIFIED, LOST, CONVERTED, SPAM) and tags.

Bookings & Appointments

Sync appointments with a calendar, CRM or front-desk app. Needs the Appointments & Bookings extension.

bookings:readbookings:write
  • GET/bookings/services

    Bookable services with duration, price, capacity, category and staff. include_inactive=true adds switched-off ones.

  • GET/bookings/availability

    Free slots. service_id and date (YYYY-MM-DD) for a day’s slots, or month (YYYY-MM) for the days that have any. Optional staff_id, party_size.

  • GET/bookings/appointments

    List appointments, newest first. Query: page, limit, status (comma list of PENDING, CONFIRMED, CHECKED_IN, COMPLETED, CANCELLED, NO_SHOW), start/end (ISO), service_id, staff_id, search.

  • GET/bookings/appointments/:id

    One appointment with service, staff, resource, customer, payment and timestamps.

  • POST/bookings/appointments

    Book a slot. serviceId, startTime (a slot from availability), customerName and customerEmail are required. Checked by the same slot engine as the widget, so it can’t double-book; the customer gets the confirmation email.

  • PUT/bookings/appointments/:id/status

    Move to CONFIRMED, CHECKED_IN, COMPLETED, NO_SHOW or CANCELLED, with an optional reason. notify: false skips the customer email.

  • POST/bookings/appointments/:id/reschedule

    Move to a new startTime (and optionally staffMemberId). Refused with 409 if the slot is taken.

POST /bookings/appointmentsjson
{
"serviceId": "cmg1svc0000001",
"startTime": "2026-10-28T05:15:00.000Z",
"customerName": "Priya Shrestha",
"customerEmail": "[email protected]",
"customerPhone": "+977-9801234567",
"partySize": 1,
"notes": "First visit"
}

Product Reviews

Moderate reviews and show them elsewhere. Needs the Product Reviews extension.

reviews:readreviews:write
  • GET/reviews

    List reviews, newest first. Query: page, limit, status (PENDING, PUBLISHED, REJECTED, SPAM), product_id, updated_since (ISO).

  • GET/reviews/:id

    One review with its product, rating, text, photos, helpful count, verified-purchase flag and store reply.

  • PUT/reviews/:id/status

    Set status to PUBLISHED, REJECTED (hidden), SPAM or PENDING. Product ratings are recalculated.

  • PUT/reviews/:id/reply

    Post or replace the store’s public reply (up to 3000 characters).

Store

Details of the store the key belongs to.

store:read
  • GET/store

    Name, domains, currency, timezone, locale, vertical and contact details.

Conventions

Responses & Pagination

Results are wrapped in data. Lists add meta: pass page (from 1) and limit (1–100), and stop when page reaches meta.totalPages.

GET /products?page=1&limit=20json
{
"data": [ { "id": "cmg1prod0000001", "title": "Handmade Lokta Notebook", "price": 1200, } ],
"meta": { "total": 137, "page": 1, "limit": 20, "totalPages": 7 }
}

Money

A plain number in the store’s currency, not cents. Read the currency from GET /store.

Timestamps

ISO 8601, in UTC.

IDs

Strings. Use id as your foreign key, not orderNumber, which is for display.

New fields

Can appear at any time. Ignore fields you don’t recognise.

Bookings and reviews use snake_case fields (start_time, verified_purchase). Every field of orders, products, variants and leads, with all status values, is in the object reference.

Conventions

Errors & Rate Limits

Errors use standard status codes and a JSON body with statusCode, message and error. For validation errors, message is a list.

400
Meaning:Invalid body or query
What to do:Read message and fix the request. Don’t retry it unchanged.
401
Meaning:Key missing, wrong, expired or revoked
What to do:Check the key and the Bearer header.
403
Meaning:Missing scope, or no API access on your plan
What to do:Add the scope to the key.
404
Meaning:No such record in this store
What to do:Check the id and that the URL includes /api/v1.
429
Meaning:Rate limit reached
What to do:Wait Retry-After seconds, then retry. Always safe.
5xx
Meaning:Failure on our side
What to do:Retry reads with backoff; check the record before retrying a write.
X-RateLimit-Limit
120 requests per minute, per key
X-RateLimit-Remaining
Requests left in the current one-minute window
X-RateLimit-Reset
Unix time the window resets
Retry-After
On 429 only: seconds to wait

Try it

Live API Sandbox Console

Run read requests against your store and inspect real responses and headers. Writes are shown as a dry run and never change your data.

Live REST API Sandbox Console

v1 REST

Test live requests, inspect JSON schemas, and verify response formats directly from your browser.

Manage API Keys

Leave blank to test using your current logged-in store session, or paste an API key created under Settings › API & Webhooks.

Events

Real-Time Webhooks

Instead of polling, receive an HTTPS POST the moment something happens. Add one subscription per topic in Settings › API & Webhooks.

orders/createAll channels

An order is placed on the storefront, POS, dashboard or API.

orders/updatedAll channels

An order’s status, payment, fulfillment or contents change.

orders/fulfilledAll channels

An order is marked fulfilled. Its shipments carry the tracking.

orders/cancelledAll channels

An order is cancelled.

leads/createAll channels

A contact form, quote request or API call creates a lead.

leads/updatedAll channels

A lead’s status or tags change.

bookings/createAll channels

An appointment is booked on the widget, in the dashboard, by AI or through the API.

bookings/updatedAll channels

An appointment is confirmed, checked in, completed, marked no-show, rescheduled or edited.

bookings/cancelledAll channels

An appointment is cancelled by the customer or the store.

reviews/createStorefront

A shopper submits a product review (spam excluded), whatever its status.

reviews/publishedAll channels

A review goes live, automatically or by a moderator, dashboard, AI or API.

products/create · update · deleteDashboard & API

A product is created, modified or removed.

inventory/level_updateAPI only

Stock is adjusted.

Coming later

Customer topics are listed in the dashboard but don’t fire yet. Product events fire for dashboard and API changes alike; inventory events fire only for API stock adjustments.

Webhooks

Webhook Payload

Every delivery uses the same envelope, with the record itself in data. Send test deliveries have test: true and placeholder data.

POST to your endpointjson
{
"id": "evt_0a1b2c3d4e5f60718293a4b5",
"topic": "orders/create",
"api_version": "2026-10-15",
"created_at": "2026-10-15T09:30:00.000Z",
"store_id": "cmg1store0000demo",
"test": false,
"data": { "id": "cmg1ord00000001", "orderNumber": "#1001", "total": 2500, "items": [ ] }
}
X-Joomni-Signature
t=<unix seconds>,v1=<hex HMAC-SHA256>
X-Joomni-Event-Id
Same as the body’s id. Use it to ignore repeats.
X-Joomni-Topic
The event topic, e.g. orders/create.
X-Joomni-Timestamp
The Unix time that was signed.
X-Joomni-Store-Id
The store the event belongs to.
X-Joomni-Webhook-Id
The subscription that matched.
X-Joomni-Test-Mode
true on Send test deliveries.
X-Joomni-Hmac-Sha256
Deprecated: body-only signature that can be replayed. Use X-Joomni-Signature.

Webhooks

Verifying Webhook Signatures

Compute HMAC-SHA256(secret, t + "." + rawBody) in hex and compare it in constant time with each v1 value. Reject requests whose t is more than 5 minutes from now, so a captured request can’t be replayed.

Verify the raw body, not parsed JSON

Parsing and re-serialising changes whitespace and key order, and the signature stops matching. Use express.raw() in Express, request.get_data() in Flask, request.body in Django or $request->getContent() in Laravel. The key is the whole secret, including whsec_.
Webhook signature verificationjavascript
import crypto from 'crypto';
const TOLERANCE_SECONDS = * ;
// rawBody: Buffer or string exactly as received; header: X-Joomni-Signature
export function verifyJoomniWebhook(rawBody, secret, header) {
let t = null;
const signatures = [];
for (const part of String(header || '').split(',')) {
const [key, value] = part.trim().split('=', );
if (key === 't' && /^\d+$/.test(value ?? '')) t = Number(value);
if (key === 'v1' && value) signatures.push(value);
}
if (t === null || !signatures.length) return false;
if (Math.abs(Date.now() / - t) > TOLERANCE_SECONDS) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf8').digest('hex'),
);
return signatures.some((sig) => {
const given = Buffer.from(sig);
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}

All three are tested against the same signature test vectors, which are worth running against your own code too.

Webhooks

Testing Webhooks Locally

Joomni delivers only to public HTTPS addresses. While you build a receiver, you have two options.

No store needed

The example repo’s tools/send-test-webhook.mjs sends signed sample events for every topic to localhost, including duplicates, bad signatures and expired timestamps.

Real events through a tunnel

Run cloudflared tunnel --url http://localhost:4242 or ngrok http 4242, add the HTTPS address as an endpoint, and press Send test.

Webhooks

Responding & Delivery Guarantees

Reply 2xx within 10 seconds

Verify, store the event, reply, then do slow work in a background job. Redirects are not followed.

Not retried yet

A failed delivery shows in the endpoint’s log but isn’t resent. Run a periodic job that lists recent records to catch misses.

Expect duplicates

Save each processed id (a unique database column works well) and skip ids you have seen.

Order isn’t guaranteed

Compare data.updatedAt with what you stored and ignore older updates.

Production

Go-Live Checklist

  • One API key per integration, with only the scopes it uses.
  • Keys and secrets in environment variables or a secret manager.
  • Signatures verified on the raw body, with the 5-minute window.
  • Event ids deduplicated in a database; slow work queued.
  • A reconcile job that covers missed deliveries.
  • 429 retried after Retry-After; other 4xx logged, not retried.
  • No full order or lead payloads in logs: they hold personal data.
  • Send test reaches the production URL and gets a 200.

Open source

Guides & Examples on GitHub

Everything below lives in joomni225/joomni-api-examples, alongside runnable code in three languages.

joomni225/joomni-api-examples

Clone working examples in JavaScript, Python and PHP

API clients, webhook receivers, signature tests and a local event sender. No dependencies to start.

View on GitHub

Open source

Ready-made tools

Built on this API. Use them as they are, or as a starting point for your own integration.

Need help?