Developer Documentation
Integrate your systems with Kaynko POS using webhooks. Get real-time notifications when sales complete, cash sessions open and close, debts are settled, and more.
Integration Methods
| Method | Description | Plan required |
|---|---|---|
| Webhooks | HTTPS POST to your server on POS events | Growth+ |
| Google Sheets | Automatic sync of sales and expenses | Growth+ |
| Excel / CSV export | Manual export from the app | All plans |
| REST API | Coming in a future phase | — |
Quick Start
- Make sure your Kaynko account is on the Growth plan or above.
- In the app, go to Settings → Integrations.
- Tap Add Webhook and enter your HTTPS endpoint URL.
- Select the events you want (e.g.
sale.completed). - Optionally add a Secret Key for HMAC signature verification.
- Tap Test — your server should receive a ping request.
- That's it. Events fire automatically as sales and other actions happen.
How Webhooks Work
When a POS event occurs, the app sends an HTTPS POST request to every registered endpoint that subscribes to that event type. Delivery is fire-and-forget — failures are logged in the app but do not affect POS operations.
Your endpoint must respond with an HTTP 2xx status code within 5 seconds. Return 200 immediately and process the payload asynchronously to avoid timeouts.
Registering an Endpoint
URL Requirements
Webhook URLs must start with https://. Plain HTTP and private network addresses (localhost, 192.168.x.x, etc.) are rejected by the app.
Secret Key
Adding a secret key enables HMAC-SHA256 signature verification on every request. Strongly recommended for production — see Payload Signing.
Events
An endpoint can subscribe to any combination of event types. Multiple endpoints can subscribe to the same event. Events not subscribed are silently skipped for that endpoint.
Payload Format
Every webhook request uses the same JSON envelope:
{
"event": "sale.completed",
"data": { ... },
"timestamp": "2026-06-11T14:30:00.000Z"
}
The event field is the event type string. The data field contains event-specific fields — see each event below. The timestamp is the ISO 8601 UTC time the event was dispatched.
Request Headers
| Header | Value | Notes |
|---|---|---|
| Content-Type | application/json | Always present |
| X-Kaynko-Event | e.g. sale.completed | Always present |
| X-Kaynko-Signature | sha256=<hmac-hex> | Only if secret key configured |
Payload Signing
If you configure a secret key, the app signs every payload with HMAC-SHA256. Verify this signature before processing to confirm the request came from Kaynko POS.
signature = HMAC-SHA256(key=secretKey, message=requestBodyBytes)
header = "sha256=" + hex(signature)
Verify in Node.js
const crypto = require("crypto");
function verifySignature(rawBody, secret, header) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-kaynko-signature"] || "";
if (!verifySignature(req.body, process.env.KAYNKO_SECRET, sig)) {
return res.status(401).send("Unauthorized");
}
const event = JSON.parse(req.body);
processAsync(event); // handle in background
res.sendStatus(200);
});
Verify in Python
import hmac, hashlib
def verify_signature(raw_body: bytes, secret: str, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-Kaynko-Signature", "")
if not verify_signature(request.get_data(), KAYNKO_SECRET, sig):
return "Unauthorized", 401
event = request.get_json()
process_async.delay(event) # e.g. Celery task
return "OK", 200
hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js. Never use === or == for signature comparison.Delivery Log
The app keeps the last 10 delivery results in memory. To view them: go to Settings → Integrations, scroll to Recent Deliveries, and tap Refresh. Each entry shows the event type, endpoint name, time, and success/failure status.
Available Events
sale.completedA sale is successfully recorded. Includes receipt number, totals, payment method, and line items.
sale.voidedA sale is voided. Stock is restored before this event fires.
session.openedA cash session is opened with an opening float.
session.closedA cash session is closed with the counted closing amount.
debt.settledA debt payment is recorded against a credit sale.
product.createdA new product is added to the catalog.
product.updatedAn existing product is edited.
sale.completed
{
"event": "sale.completed",
"data": {
"saleId": 42,
"receiptNumber": "KNK-0042",
"cashierId": 1,
"subtotal": 15.50,
"discountAmount": 0.00,
"vatAmount": 0.00,
"total": 15.50,
"paymentMethod": "cash",
"currency": "USD",
"customerName": "Jane Smith",
"customerType": "retail",
"createdAt": "2026-06-11T14:30:00.000Z",
"items": [
{
"productName": "Cooking Oil 2L",
"productCode": "SKU-001",
"quantity": 2,
"unitPrice": 3.50,
"lineTotal": 7.00
}
]
},
"timestamp": "2026-06-11T14:30:00.000Z"
}
| Field | Type | Nullable | Notes |
|---|---|---|---|
| paymentMethod | string | No | cash, mobile_money, bank_transfer, card, credit, other |
| customerName | string | Yes | Null for walk-in customers |
| customerType | string | Yes | retail or wholesale |
| items[].productCode | string | Yes | SKU/barcode; null if not set |
sale.voided
{
"event": "sale.voided",
"data": {
"saleId": 42,
"receiptNumber": "KNK-0042",
"total": 15.50,
"paymentMethod": "cash",
"currency": "USD",
"cashierId": 1,
"voidedByCashierId": 2,
"voidReason": "Customer returned items",
"voidedAt": "2026-06-11T15:00:00.000Z"
},
"timestamp": "2026-06-11T15:00:00.000Z"
}
voidedByCashierId is null when the original cashier voided their own sale. Stock is automatically restored before this event fires.
session.opened / session.closed
// session.opened
{
"event": "session.opened",
"data": {
"sessionId": 5,
"cashierId": 1,
"sessionDate": "2026-06-11T00:00:00.000Z",
"openingCash": 20.00
}
}
// session.closed
{
"event": "session.closed",
"data": {
"sessionId": 5,
"cashierId": 1,
"sessionDate": "2026-06-11T00:00:00.000Z",
"closingCash": 187.50
}
}
debt.settled
{
"event": "debt.settled",
"data": {
"debtId": 7,
"paymentAmount": 50.00,
"cashierId": 1
},
"timestamp": "2026-06-11T11:20:00.000Z"
}
product.created / product.updated
{
"event": "product.created",
"data": {
"productId": 22,
"productName": "Cooking Oil 2L",
"productCode": "SKU-022",
"action": "created"
}
}
API Keys
API keys are generated in Settings → Integrations → API Key. They require the Business plan or above.
In Phase 1, API keys are stored securely on the device but there are no REST API endpoints yet. They are infrastructure for a future REST API. Store your key securely — it cannot be recovered after generation, only regenerated.