Channels and operations
One endpoint per channel: POST /api/v1/{channelCode}/{operation}, for example transaction.create, campaign.inquiry or voucher.redeem.
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.
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.
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.
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,
});import hashlib, hmac, json, os, time, uuid
import requests
body = json.dumps({
"requestId": str(uuid.uuid4()),
"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"}],
}, separators=(",", ":"))
timestamp = str(int(time.time()))
signature = hmac.new(os.environ["INO_HMAC_SECRET"].encode(),
f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()
res = requests.post(f"https://{host}/api/v1/MODERN_ECOM/transaction.create", data=body, headers={
"Content-Type": "application/json",
"X-Api-Key": os.environ["INO_API_KEY"],
"X-Timestamp": timestamp,
"X-Signature": signature,
})using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var body = JsonSerializer.Serialize(new {
requestId = Guid.NewGuid(),
orderNumber = "ORD-10482",
customerId = "6f0c2b1e-8a54-4d2e-9b9e-2f1d7c3a4b10",
cart = new {
currentTotal = "119.00", originalTotal = "119.00", totalDiscount = "0",
lineItems = new[] { new { lineNumber = 1, sku = "MR-DJ-001", qty = 1, unitPrice = "89.00", originalUnitPrice = "89.00" } },
},
payments = new[] { new { type = "CREDIT_CARD", amount = "119.00" } },
});
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant();
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{host}/api/v1/MODERN_ECOM/transaction.create") {
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
request.Headers.Add("X-Api-Key", apiKey);
request.Headers.Add("X-Timestamp", timestamp);
request.Headers.Add("X-Signature", signature);One endpoint per channel: POST /api/v1/{channelCode}/{operation}, for example transaction.create, campaign.inquiry or voucher.redeem.
API key with HMAC signature, or an OAuth2 client-credentials bearer token. Chosen per channel.
requestId is your idempotency key: the same request within 24 hours returns the original response; a duplicate never earns points twice.
A standard error catalog with HTTP status, a machine-readable code and whether it is safe to retry.
600 requests a minute per channel by default. Over the limit you get 429 with Retry-After.
v1 only changes in additive ways. A breaking change ships as v2, with a sunset timeline for v1.
Send X-Correlation-Id to tie a call to your logs; it is echoed back on every response.
Use a short timeout for inquiries and never block checkout: on failure, show list prices and retry the transaction later with the same requestId.
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-idwebhook-timestampwebhook-signatureimport {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);
});Every event is a CloudEvents 1.0 envelope with a versioned data payload. Example payloads are in the API reference.
transaction
transaction.completedPurchase completedtransaction.returnedReturn processedpoints
points.earnedPoints earnedpoints.adjustedPoints adjustedpoints.expiringPoints about to expirepoints.releasedPending points releasedpoints.expiredPoints expiredvoucher
voucher.issuedVoucher issuedvoucher.redeemedVoucher usedvoucher.redemption_reversedVoucher use reversedvoucher.cancelledVoucher cancelledvoucher.expiredVoucher expiredmember
member.registeredMember joinedmember.updatedMember profile updatedconsent
consent.changedContact permission changedtier
tier.changedTier changedtier.downgrade_scheduledTier downgrade scheduledtier.downgrade_cancelledTier downgrade cancelledsegment
segment.membership_changedSegment membership changedcampaign
campaign.changedCampaign changedjob
job.completedBulk job completedjob.failedBulk job failedPull events with a cursor when polling suits you better than webhooks.
Bulk operations run as jobs; poll status and download results with per-row errors.
Drop CSV files of transactions or point corrections for legacy POS and end-of-day runs.
Store and forward: send the real sale time and reconcile when the connection returns.
Receive events on your own broker or push transactions through it.
Incremental Parquet exports of transactions and customer facts for your data lake.
We set up a test channel for your team and answer integration questions.