For developers

Build on Ino Loyalty

A real-time API for your POS, store and apps, signed webhooks for everything that happens, and a full OpenAPI reference. Documented in the open.

Your first request in three steps

  1. 1

    Get your credentials

    During onboarding you receive your API host and a channel code (for example MODERN_ECOM). Your IT admin creates the API key and secret in the Developers console of Ino Loyalty.

  2. 2

    Sign the request

    Every call carries X-Api-Key, X-Timestamp (Unix seconds, within 5 minutes) and X-Signature: the lowercase hex HMAC-SHA256 of "{timestamp}.{raw body}" with your secret. Sign the exact bytes you send.

  3. 3

    Record a purchase

    Call transaction.create with the order and list prices. Ino Loyalty evaluates campaigns, earns or spends points and returns the result.

import crypto from 'node:crypto';

const body = JSON.stringify({
  requestId: crypto.randomUUID(),
  orderNumber: 'ORD-10482',
  customerId: '6f0c2b1e-8a54-4d2e-9b9e-2f1d7c3a4b10',
  cart: {
    currentTotal: '119.00', originalTotal: '119.00', totalDiscount: '0',
    lineItems: [{lineNumber: 1, sku: 'MR-DJ-001', qty: 1, unitPrice: '89.00', originalUnitPrice: '89.00'}],
  },
  payments: [{type: 'CREDIT_CARD', amount: '119.00'}],
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
  .createHmac('sha256', process.env.INO_HMAC_SECRET)
  .update(`${timestamp}.${body}`)
  .digest('hex');

const res = await fetch(`https://${host}/api/v1/MODERN_ECOM/transaction.create`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.INO_API_KEY,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
  },
  body,
});

Good to know

Channels and operations

One endpoint per channel: POST /api/v1/{channelCode}/{operation}, for example transaction.create, campaign.inquiry or voucher.redeem.

Authentication

API key with HMAC signature, or an OAuth2 client-credentials bearer token. Chosen per channel.

Idempotency

requestId is your idempotency key: the same request within 24 hours returns the original response; a duplicate never earns points twice.

Errors

A standard error catalog with HTTP status, a machine-readable code and whether it is safe to retry.

Rate limits

600 requests a minute per channel by default. Over the limit you get 429 with Retry-After.

Versioning

v1 only changes in additive ways. A breaking change ships as v2, with a sunset timeline for v1.

Correlation

Send X-Correlation-Id to tie a call to your logs; it is echoed back on every response.

Checkout safety

Use a short timeout for inquiries and never block checkout: on failure, show list prices and retry the transaction later with the same requestId.

Webhooks

Receive loyalty events as signed HTTP POSTs. Ino Loyalty follows the Standard Webhooks specification, so the official libraries verify our requests in one call.

webhook-id
Delivery id, the same on every retry. Use it to deduplicate.
webhook-timestamp
Unix seconds the request was signed at. Reject anything older than 5 minutes.
webhook-signature
v1,<base64 HMAC-SHA256> over "{id}.{timestamp}.{raw body}". During a secret rotation two signatures are sent; accept either.
  • At-least-once delivery, in order per subscription (best effort)
  • 10-second timeout; only 2xx counts as success
  • Retries for about six and a half days, up to 20 attempts
  • Paused or suspended subscriptions keep queuing events
  • Suspended only after 24 hours of failures, never for a short outage
verify-webhook.js
import {Webhook} from 'standardwebhooks'; // npm i standardwebhooks (also for Python, .NET, Go, Java…)

const wh = new Webhook(process.env.INO_WEBHOOK_SECRET); // whsec_…

app.post('/ino-webhook', express.raw({type: 'application/json'}), (req, res) => {
  // Throws if the signature does not match or the timestamp is older than 5 minutes.
  const event = wh.verify(req.body, req.headers);
  // event.type, e.g. "points.earned"; deduplicate with req.headers['webhook-id']
  res.sendStatus(204);
});

Event catalog

Every event is a CloudEvents 1.0 envelope with a versioned data payload. Example payloads are in the API reference.

transaction

  • transaction.completedPurchase completed
  • transaction.returnedReturn processed

points

  • points.earnedPoints earned
  • points.adjustedPoints adjusted
  • points.expiringPoints about to expire
  • points.releasedPending points released
  • points.expiredPoints expired

voucher

  • voucher.issuedVoucher issued
  • voucher.redeemedVoucher used
  • voucher.redemption_reversedVoucher use reversed
  • voucher.cancelledVoucher cancelled
  • voucher.expiredVoucher expired

member

  • member.registeredMember joined
  • member.updatedMember profile updated

consent

  • consent.changedContact permission changed

tier

  • tier.changedTier changed
  • tier.downgrade_scheduledTier downgrade scheduled
  • tier.downgrade_cancelledTier downgrade cancelled

segment

  • segment.membership_changedSegment membership changed

campaign

  • campaign.changedCampaign changed

job

  • job.completedBulk job completed
  • job.failedBulk job failed

More ways to integrate

Event stream

Pull events with a cursor when polling suits you better than webhooks.

Async jobs

Bulk operations run as jobs; poll status and download results with per-row errors.

Batch file import

Drop CSV files of transactions or point corrections for legacy POS and end-of-day runs.

Offline POS

Store and forward: send the real sale time and reconcile when the connection returns.

Apache Kafka

Receive events on your own broker or push transactions through it.

Data exports

Incremental Parquet exports of transactions and customer facts for your data lake.

Need a sandbox or a hand?

We set up a test channel for your team and answer integration questions.

Write to [email protected]

Anonymous visitor

Visitor

Explore the site, play a short game and earn XP and points. Reach Explorer to unlock a free 30-day Ino Loyalty POC for your company.

Play Points Rush

Your profile is stored in this browser. If you allow analytics cookies, we count what happens here anonymously, without your name.