PerxDevelopers

Getting started

Quickstart

Send one signed sale to UAT and watch it earn points. About twenty minutes, assuming you have your vendor slug.

1. Check you can reach the gateway

Before debugging signatures, prove the network path and TLS work.

bash
curl https://uat-connect.perx.mv/health
# → ok

2. Get your credentials

  • Your vendor slug and a UAT merchant + store come from your Perx contact.
  • The connection secret is generated by the merchant: Perx dashboard → Settings → Integrations → Add connection. For UAT, your Perx contact will do this and send you the secret.

3. Send a sale

One POST, four headers, one signature. The customer phone below must belong to a Perx member on your UAT merchant — ask your contact for a test member.

const crypto = require('crypto');

const BASE   = 'https://uat-connect.perx.mv';
const VENDOR = 'acmepos';          // your slug, from Perx
const STORE  = '12345';            // the merchant's store id on your side
const SECRET = process.env.PERX_SECRET;   // pasted by the merchant

async function sendSale() {
  const payload = {
    eventId:    'evt-' + Date.now(),
    eventType:  'sale.completed',
    occurredAt: new Date().toISOString(),
    store:      { id: STORE },
    transaction: {
      id:       'INV-2026-0042',
      total:    45.0,
      currency: 'MVR',
      channel:  'pos',
      customer: { type: 'phone', value: '+9607123456' },
      items: [
        { name: 'Flat White', sku: 'SKU-COFFEE-001', quantity: 2, unitPrice: 5.5 },
        { name: 'Chicken Sandwich', sku: 'SKU-FOOD-014', quantity: 1, unitPrice: 34.0 },
      ],
    },
  };

  // Sign the EXACT bytes you send. Serialise once, reuse the buffer.
  const raw = Buffer.from(JSON.stringify(payload), 'utf8');
  const ts  = Math.floor(Date.now() / 1000).toString();
  const sig = crypto.createHmac('sha256', SECRET)
                    .update(ts + '.').update(raw)
                    .digest('base64');

  const res = await fetch(BASE + '/webhooks/generic', {
    method: 'POST',
    headers: {
      'Content-Type':     'application/json',
      'X-Perx-Vendor':    VENDOR,
      'X-Perx-Store':     STORE,
      'X-Perx-Timestamp': ts,
      'X-Perx-Signature': 'v1=' + sig,
    },
    body: raw,
  });

  console.log(res.status, await res.json());
}

sendSale();

4. Read the response

A 200 with status: "processed" and a non-zero pointsEarned means the whole chain worked.

json
{
  "status": "processed",
  "membershipId": "0f6a1c9e-4b7d-4c2a-9f31-8a2d5e6b0c31",
  "pointsEarned": 45,
  "stampsUpdated": [
    {
      "rewardId": "b21e77a0-3c14-4e77-9a02-1d55f0b9c8e2",
      "rewardTitle": "Coffee Club",
      "current": 5,
      "required": 5,
      "completed": true
    }
  ],
  "warning": null
}
Put this on the receipt

If you can print pointsEarned and a completed stamp card on the customer’s receipt, do it. It is the single highest-value thing you can build for the merchant’s customers, and it costs you one line.

5. Things that will happen next

  • You get a 401. Almost always the signature. Paste your body and secret into the signature playground — it shows the exact value Perx expects and the most common reasons they differ.
  • You get 200 but pointsEarned: 0 and a warning. The customer is not a Perx member. That is a normal outcome, not an error — do not retry.
  • You send the same sale twice and get status: "duplicate". Working as intended. Idempotency is on transaction.id.

Where to go next