Docs
Start: Signing requests
Start

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

HeaderValue
X-Laayo-KeyYour key ID, for example lpk_8f2c41a9e07b5d13c6a2f904. Not secret.
X-Laayo-TimestampUnix time in seconds, when you signed.
X-Laayo-SignatureLowercase 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"}
Sign the bytes you sendBuild the body string once, sign it, and send that same string. If your HTTP library encodes the object again after you signed, the spacing or key order changes and the signature no longer matches. The same goes for the query: sign it in the order you send it.

#Examples

Mark an item sold out
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.
Was this page clear?Updated 1 Nov 2026