Signing requests
Every call carries your key ID, a timestamp and an HMAC signature over the method, the path and the body. We refuse anything unsigned, anything more than 5 minutes off, and the same signature twice.
#The three headers
| Header | Value |
|---|---|
X-Laayo-Key | Your key ID, for example lpk_8f2c41a9e07b5d13c6a2f904. Not secret. |
X-Laayo-Timestamp | Unix time in seconds, when you signed. |
X-Laayo-Signature | Lowercase hex HMAC-SHA256 of the signing base, keyed with your secret (lps_...). |
Send all three on every call, GET included. Paths start with /v1 on https://posconnect.laayo.com.np. A body needs Content-Type: application/json and can be up to 512 KB.
#What you sign
Join the timestamp, the method in upper case, the path with its query exactly as you send it, and the raw body, with a dot between each. For GET the body is empty, so the base ends with a dot.
base = timestamp + "." + METHOD + "." + path_with_query + "." + raw_body
signature = hex(hmac_sha256(secret, base))
1762019400.GET./v1/orders/123.
1762019400.GET./v1/events?after=5521&wait=25.
1762019400.PUT./v1/stores/5/status.{"state":"open"}#Examples
const crypto = require('crypto');
const method = 'PUT';
const path = '/v1/stores/27/items/availability';
const body = JSON.stringify({ items: [ { external_item_id: 'MOMO-C-10', available: false } ] });
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', process.env.LAAYO_SECRET)
.update(ts + '.' + method + '.' + path + '.' + body).digest('hex');
await fetch('https://posconnect.laayo.com.np' + path, {
method,
headers: {
'Content-Type': 'application/json',
'X-Laayo-Key': process.env.LAAYO_KEY_ID,
'X-Laayo-Timestamp': ts,
'X-Laayo-Signature': sig,
},
body,
});#Clock and replays
- We accept a timestamp up to 5 minutes away from our clock. Anything further returns 401 stale_timestamp. Keep the machine clock synced (NTP, or Windows time sync on a desktop POS).
- Each signature is accepted once. Identical requests in the same second (same timestamp, method, path and body) sign the same, so the second returns 401 replayed. Retry after 1 second with a new timestamp.
#Rotating the secret
POST /v1/partner/secret/rotate, or the button in the console, gives you a new secret, shown once. The old one keeps working for 24 hours, so you can roll the new one out without downtime. GET /v1/partner/me tells you until when.
Webhooks Laayo sends are signed with your current secret, see Verify a signature.
#When it does not match
401 unauthorized covers a missing header, an unknown key and a signature that does not match. The console request log shows which one, and how old the timestamp was. Common causes:
- The body was encoded again after signing (spaces after colons, a different key order).
- The path was signed without its query, or with the query in another order, or with the host in front.
- The method was signed in lower case.
- The signature was sent uppercase or base64. We expect lowercase hex.
- The secret was rotated more than 24 hours ago and this machine still has the old one.