Developers
PLUX API & webhooks
Connect your own website, accounting or other tools to your business on PLUX. Read your price list, place and follow orders, and get told the moment something happens.
Keys
The business owner creates keys in Settings → Developers. A key is shown once; PLUX keeps only a fingerprint of it. Send it on every request:
curl https://plux.africa/api/v1/me \
-H "Authorization: Bearer plux_live_..."- Live keys (
plux_live_…) work with real business data. - Test keys (
plux_test_…) can read the price list and try out orders: the order is priced and checked for real, then discarded (livemode: false). They never read orders, customers, payments or stock. - Each key carries only the scopes it was given. Keep keys on your server — never in a web page or mobile app.
- Revoke a key in Settings and it stops working at once.
catalog:read | Products, prices and availability. |
orders:read | Orders with their items, status and totals. |
orders:write | Place orders, priced by PLUX from your price list. |
customers:read | Customer names, phone numbers and credit limits. |
payments:read | Payments and refunds recorded on orders. |
stock:read | Stock items and quantity on hand per branch. |
events:read | The event feed — the same events webhooks deliver. |
Requests and answers
- JSON in and out, over https. Money is whole kobo (
250000= ₦2,500). Times are ISO 8601 in UTC. - Lists return
{ data, has_more, next_after }. Ask for the next page with?after=thenext_aftervalue;limitis 1–100 (25 by default). - Errors return
{ error, code }with status 401 (bad key), 403 (missing scope, or a test key on live data), 404, 422 (invalid input) or 429. - Each key may make 120 requests a minute.
Endpoints
| GET | /api/v1/meany key | The business, its branches and what this key may do. |
| GET | /api/v1/productscatalog:read | The price list: products, prices, options and availability. |
| GET | /api/v1/menucatalog:read | A branch's menu for a storefront: categories in order, photos, options, availability, tax rate and what the business has switched on (ordering, online payment, coupons). Filter: branch_id. |
| POST | /api/v1/orders/quoteorders:write | The exact price of a cart (tax, options, an optional coupon_code) without keeping anything: no order number, no customer, no coupon use. |
| GET | /api/v1/ordersorders:read | Orders, newest first. Filters: status, branch_id, created_after, updated_after. |
| GET | /api/v1/orders/{id}orders:read | One order with its items, totals, stage and payment status. |
| POST | /api/v1/ordersorders:write | Place an order. PLUX prices it from your price list. Optional coupon_code is applied after the order exists and never blocks it. |
| POST | /api/v1/orders/{id}/paymentorders:write | Start online payment for an order your key placed; send the customer to checkout_url. return_url must be one of the business's allowed sites. |
| POST | /api/v1/orders/{id}/payment/verifyorders:write | Ask the provider (through PLUX) what happened to a payment reference of that order. |
| GET | /api/v1/customerscustomers:read | Customers. Filter: phone. |
| GET | /api/v1/paymentspayments:read | Payments and refunds. Filters: order_id, created_after. |
| GET | /api/v1/stockstock:read | Stock items with quantity on hand per branch. |
| GET | /api/v1/eventsevents:read | The event feed, oldest first. Filter: type. Continue with after. |
Placing orders
Send product ids and quantities. PLUX works out the prices, charges and total from your price list — a price in the request is ignored. The order then follows the business's own rules, exactly like an online order: it is sent on automatically or waits for staff, and large orders can wait for a manager.
curl -X POST https://plux.africa/api/v1/orders \
-H "Authorization: Bearer plux_live_..." \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "web-order-20481",
"customer": { "name": "Chidi Okafor", "phone": "08030001111" },
"lines": [ { "product_id": "PRODUCT_ID", "quantity": 2, "option_ids": [] } ],
"notes": "Call on arrival"
}'idempotency_key(8–80 characters) is required. Sending the same key again returns the same order instead of creating another, so a retry after a timeout is safe.- Optional:
branch_id(the first branch otherwise) andservice_type(takeaway,dine_inordelivery). - Order
statusis one ofnew,confirmed,queued,in_progress,ready,handed_over,completed,cancelled;stagegives the business's own step name.
Webhooks
Add an https URL in Settings → Developers and choose the events you want. PLUX POSTs each event to it as JSON within a few seconds. Answer with any 2xx status within 8 seconds.
{
"id": "evt_cmg1x...",
"type": "order.ready",
"created_at": "2026-10-02T11:04:31.512Z",
"business_id": "...",
"branch_id": "...",
"entity": { "type": "order", "id": "..." },
"order_id": "...",
"payment_id": null,
"channel": "WEB",
"amount_kobo": null,
"reason": null,
"actor": { "kind": "staff", "role": "MANAGER" },
"data": {}
}Check the signature
Every delivery carries PLUX-Signature: t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256 of `${t}.${rawBody}` with your signing secret and compare. Reject anything older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
function fromPlux(rawBody, header, secret) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const mine = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
return mine.length === v1.length && timingSafeEqual(Buffer.from(mine), Buffer.from(v1));
}- A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After that it is marked failed and can be sent again from Settings.
- The same event can arrive more than once and not always in order: use
idto ignore repeats andcreated_atto order. - After 15 failed attempts in a row PLUX switches the webhook off and flags it on the owner's dashboard. Switch it back on in Settings.
- Redirects are not followed. Use “Send test” to receive a
webhook.testevent. - Missed something?
GET /api/v1/events?after=evt_…returns the same events in order.
Events
Event names never change once published. New fields may be added to an event; ignore the ones you don't use.
Orders
order.createdA new order or job was placed, from any channel.order.confirmedStaff (or the customer, where approval is asked) confirmed the order.order.items_addedItems were added to an open order.order.item_voidedAn item was removed from an order, with a reason.order.custom_item_addedA one-off item that isn't on the price list was sold.order.price_overriddenStaff sold an item at a price different from the list price.order.discount_appliedA discount was applied to or removed from an order.order.handed_overThe order was served, collected or delivered.order.completedThe order is finished and fully settled.order.purgedA PLUX Super Admin permanently removed a test, duplicate or corrupt order that never carried money (data.number, reason). The order's earlier events stay in the history.order.marked_testThe owner confirmed this order was a test/training transaction (reason). It stays on record in full but is left out of live sales, VAT, targets and vendor settlements.order.unmarked_testA test classification was withdrawn (reason): the order counts as live business again.order.cancelledThe order was cancelled, with a reason.
Production
order.sent_to_productionItems were sent to be prepared or produced.order.stage_changedThe work moved to another stage (data.from, data.to).order.production_startedSomeone started working on the order.order.items_preparedA batch of items was finished.order.readyThe order is ready for the customer.order.assignedSomeone was made responsible for the order (data.selfTaken, data.reassigned).order.unassignedThe order no longer has a person responsible.
Payments
payment.initiatedAn online payment was started.payment.receivedMoney was received and verified (amount_kobo).payment.failedAn online payment did not go through.payment.refundedA payment was refunded (amount_kobo).transfer.receivedA bank transfer arrived in a customer's account and was confirmed (data.appliedKobo, data.unappliedKobo). Each order it paid also gets payment.received.
Invoices
invoice.issuedAn invoice was issued for an order.invoice.voidedAn invoice was voided; a new one may replace it.
Stock
stock.receivedStock was received into a branch.stock.usedStock was used.stock.soldA sale took stock (data.items).stock.transferredStock moved from one branch to another.stock.adjustedA count changed the quantity on hand (data.before, data.after).stock.wastedStock was written off as waste.
Money
expense.recordedAn expense was recorded (amount_kobo).expense.voidedAn expense was voided, with a reason.cash_shift.cash_movedCash was put into or taken out of a till during a shift.credit.extendedAn order was put on a customer's account (data.customerId, data.dueAt).cash_shift.openedA cash shift was opened with a float.books.month_closedA month was closed: its figures can no longer change (data.month, amount_kobo = sales).books.month_reopenedA closed month was reopened, with a reason.cash_shift.closedA cash shift was counted and closed (amount_kobo = variance).
Approvals
approval.requestedAn action is waiting for a second person.approval.grantedA request was approved and carried out.approval.refusedA request was refused, with a reason.
People
employee.clocked_inA staff member clocked in (data.method qr|office code|pin, data.distanceM, data.flagged when it waits for a manager's review).employee.clocked_outA staff member clocked out (data.hours; data.auto when PLUX closed the shift at the business's maximum length).
Learning
recommendation.shownPLUX showed a suggestion learned from the business's own records (data.kind, data.confidence, data.suggestedKobo).recommendation.acceptedThe person used the suggestion as it was.recommendation.modifiedThe person used the suggestion but changed it (data.chosenKobo vs data.suggestedKobo).recommendation.rejectedThe person dismissed the suggestion.
Value
offer.createdA coupon or other offer was created (data.code, data.discountType, data.valueBps / valueKobo, data.campaign).offer.status_changedAn offer was paused, reactivated or ended (data.from, data.to).offer.grantedA customer was given their own code for an offer by a person (data.customerId, data.source segment:away | manual, data.last4). Delivery, if any, is a separate message.sent.coupon.appliedA coupon was applied to an order (amount_kobo = the discount; data.code).coupon.releasedA coupon came off an order (removed, or the order was cancelled; data.why).gift_card.issuedA gift card was issued (amount_kobo = its value — a liability until redeemed; data.funding PAID|COMPLIMENTARY, data.last4).gift_card.redeemedA gift card paid part or all of an order (amount_kobo; data.remainingKobo). Not new money: the matching payment.received has method GIFT_CARD.gift_card.creditedA refund of a gift-card payment went back onto the card (amount_kobo; data.balanceKobo).gift_card.voidedA gift card was cancelled by the business (amount_kobo = the balance written off).
Partners
partner.thread_openedA business partner opened a comment, explanation request or review request (data.threadId, data.kind, data.subjectType). Message text is never included.partner.message_addedA message was added to a partner thread (data.threadId, data.side). Message text is never included.partner.thread_status_changedA partner thread moved to another status (data.threadId, data.from, data.to).
Vendors
vendor.attribution_resolvedA person decided who is responsible for a sold item that had no vendor assignment at its branch (data.vendorId, null = the host; reason).vendor.history_postedPast sales were attributed to a vendor at a branch after review and approval (data.vendorId, data.branchId, data.basis VERIFIED|ESTIMATED, data.lines, data.chargedKobo, data.postingId).vendor.history_reversedA historical attribution posting was reversed before any statement covered it (data.vendorId, data.branchId, data.postingId, reason).vendor.status_changedA vendor was suspended, reactivated or made inactive (data.from, data.to, data.vendorId).vendor_agreement.activatedA vendor commission agreement was approved and took effect (data.vendorId, data.version, data.effectiveFrom, data.commissionBps). A change to the terms is always a new version.vendor_agreement.endedA vendor agreement ended, with a reason (data.vendorId, data.version).vendor_settlement.approvedA vendor statement was approved (amount_kobo = net payable; data.vendorId, data.number, data.salesKobo, data.commissionKobo). Not a payment.vendor_settlement.voidedAn approved vendor statement was voided, with a reason; its sales return to the next statement (data.vendorId, data.number).vendor_settlement.settledA vendor period was settled: paid in full and every payment reconciled by a second person. It is never reopened (amount_kobo = net; data.vendorId, data.number).vendor_payment.recordedThe host paid a vendor against a statement (amount_kobo; data.vendorId, data.number, data.method). This is money out, not a customer payment.vendor_payment.reconciledA payment to a vendor was matched to the bank or cash record (amount_kobo; data.vendorId, data.number).vendor_payment.reversedA payment to a vendor was reversed, with a reason (amount_kobo; data.vendorId, data.number).
Purchasing
purchase_order.placedA purchase order was placed with a supplier (amount_kobo = expected value, when known).purchase_order.receivedStock arrived against a purchase order (data.complete, data.received).purchase_order.cancelledA placed purchase order was cancelled, or closed with part of it not delivered.
Messages
message.sentA WhatsApp or SMS update was sent to a customer (channel, data.template).