← Back to kaynko.app

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.

Phase 1: Kaynko is an offline-first device app — all data lives on the device. Integration is push-based: the app sends events to your server as things happen. A REST API is planned for a future phase.

Integration Methods

MethodDescriptionPlan required
WebhooksHTTPS POST to your server on POS eventsGrowth+
Google SheetsAutomatic sync of sales and expensesGrowth+
Excel / CSV exportManual export from the appAll plans
REST APIComing in a future phase

Quick Start

  1. Make sure your Kaynko account is on the Growth plan or above.
  2. In the app, go to Settings → Integrations.
  3. Tap Add Webhook and enter your HTTPS endpoint URL.
  4. Select the events you want (e.g. sale.completed).
  5. Optionally add a Secret Key for HMAC signature verification.
  6. Tap Test — your server should receive a ping request.
  7. 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.

Important: Webhooks only fire for businesses on Growth, Business, or Partner plan. No events are sent on the Starter or Trial plan.

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

HeaderValueNotes
Content-Typeapplication/jsonAlways present
X-Kaynko-Evente.g. sale.completedAlways present
X-Kaynko-Signaturesha256=<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
Always use a constant-time comparison function — 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.

The delivery log is in-memory only — it resets when the app restarts. Persist delivery results on your own server for long-term auditing.

Available Events

sale.completed

A sale is successfully recorded. Includes receipt number, totals, payment method, and line items.

sale.voided

A sale is voided. Stock is restored before this event fires.

session.opened

A cash session is opened with an opening float.

session.closed

A cash session is closed with the counted closing amount.

debt.settled

A debt payment is recorded against a credit sale.

product.created

A new product is added to the catalog.

product.updated

An 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"
}
FieldTypeNullableNotes
paymentMethodstringNocash, mobile_money, bank_transfer, card, credit, other
customerNamestringYesNull for walk-in customers
customerTypestringYesretail or wholesale
items[].productCodestringYesSKU/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.