> ## Documentation Index
> Fetch the complete documentation index at: https://developers.perkstar.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Your first safe POS test

> Record one test stamp, prove retries do not duplicate it, and check a signed webhook receipt.

Use this small workflow when connecting a till: **one completed sale earns one
stamp on an existing stamp-card enrolment**. The downloadable example uses the
real API and is checked against isolated database fixtures in our release tests.
It refuses live keys. It does not create customers or send wallet notifications.

<Warning>
  A test key can still read the business's real records. Use a dedicated test
  customer/enrolment agreed with the business, not a customer chosen at random.
  Test transactions can also trigger webhooks: use a test receiver, and make
  sure any endpoint set to receive both live and test traffic handles
  `data.testMode` correctly. The example does not create or alter webhooks.
</Warning>

## 1. Agree the business, owner and minimum access

The business needs API access through its POS Connect entitlement (Scale,
Enterprise or Franchise, including an eligible active trial). If key creation
is unavailable, ask the owner to check the plan and your permissions; do not
borrow another business's key.

The business owner creates the key in **Settings → API keys → Create API key**.
Select **Test** and only **Enrolments: read** (`ENROLLMENTS_READ`) and
**Transactions: write** (`TRANSACTIONS_WRITE`). No customer-write, enrolment-write,
webhook-write or broad read-only preset is needed.

Name it for the system and responsible maintainer, such as `Till setup — Alex`.
Set an expiry covering the agreed test period; revoke it when testing ends.
The business owns the key and can revoke it. The developer owns its secure
storage and the receiving server. Perkstar cannot show the full key again.

Keep the secret in a private file named `.perkstar-test.env`, excluded from
source control. Set restrictive file permissions (for example `chmod 600
.perkstar-test.env` on macOS/Linux). Do not put it in browser code, screenshots,
support messages or a shell command that will remain in history.

```dotenv theme={null}
PERKSTAR_API_KEY=pk_test_replace_me
PERKSTAR_API_URL=https://dashboard.perkstar.co.uk/api/v1
PERKSTAR_ORG_ID=replace_with_business_id
PERKSTAR_CARD_ID=replace_with_active_stamp_card_id
PERKSTAR_ENROLLMENT_ID=replace_with_existing_test_enrollment_id
PERKSTAR_RECEIPT_ID=test:my-till:store-1:receipt-0001
```

The business ID here is the API's `organization.id`, not the six-digit account
number in the dashboard URL. Read `GET /marketplace/ping` with the test key to
check the business and its active cards. Read `GET /enrollments?cardId=<id>` to
find the agreed enrolment; follow `next_cursor` if necessary. Inspect these
responses privately. Never select the first business or enrolment automatically.

The script verifies the business, test mode, scopes, active stamp card and
matching active enrolment **before writing**.

## 2. Download, inspect and run the example

Use **Node.js 24 or later**. There are no packages to install.

[Download pos-test-quickstart.mjs](https://dashboard.perkstar.co.uk/developer-downloads/pos-test-quickstart.mjs)

Save the file, inspect it, then run:

```bash theme={null}
node --env-file=.perkstar-test.env pos-test-quickstart.mjs
```

The one-stamp request is:

```json theme={null}
{
  "enrollment_id": "your_existing_test_enrollment_id",
  "type": "STAMP",
  "delta": 1,
  "external_transaction_id": "test:my-till:store-1:receipt-0001"
}
```

It uses `POST /transactions` and the receipt ID as `Idempotency-Key`. This
example is for a completed sale earning **one stamp**, not a spend-to-stamp or
points calculation. Use the [POS guide](/guides/pos) for those workflows.
Amounts named `amount_pence` use the business currency's minor units; never
assume every currency uses two decimal places.

## 3. Check the result, repeat and exercise an error

| Check | Expected result |
| - | - |
| First request | `201` for a new row; `test_mode: true` and a transaction ID |
| Repeat identical request and key | Same transaction ID and original status, including `201`; `Idempotent-Replayed: true` |
| Same receipt with a different request-cache key | `200` and the same transaction ID: permanent receipt deduplication |
| `ADJUST` without a `delta` | `400`, `error.code: missing_field`, `error.param: delta`; no transaction |
| Read enrolment again | Balance, redemptions, spend and bonus balance unchanged |

The script runs all five checks and prints a compact `PASS` summary without
customer records or secrets. Run it again with the **same receipt ID**: it
should identify the same transaction. Each genuinely new sale needs its own
namespaced permanent reference. Do not generate a new reference on every retry.

A real customer being served during the check may change their balance through
another channel. That is why an isolated test enrolment is useful. A successful
script proves this small API workflow, not every card type or POS feature.

## 4. Receive a signed test event

[Download test-webhook-receiver.mjs](https://dashboard.perkstar.co.uk/developer-downloads/test-webhook-receiver.mjs)

The receiver accepts `transaction.created` only when `data.testMode` is `true`,
plus the dashboard's synthetic `webhook.test`. It validates the signature,
five-minute signing timestamp and business ID, then commits a minimal receipt
to a local SQLite file before returning `200`. It stores the delivery ID, event
type, transaction ID, time and a content hash; it does not store the full customer
payload, award another stamp or claim to fulfil an order.

Add the separate webhook signing secret to the private environment file:

```dotenv theme={null}
PERKSTAR_WEBHOOK_SECRET=replace_with_endpoint_signing_secret
PERKSTAR_RECEIPTS_DB=perkstar-test-receipts.sqlite
```

```bash theme={null}
node --env-file=.perkstar-test.env test-webhook-receiver.mjs
```

It listens on `127.0.0.1:8787/webhooks/perkstar`. Perkstar cannot deliver to your
localhost. Expose that exact path through an HTTPS ingress you control, then ask
the business owner to create a **separate test endpoint** in **Settings →
Webhooks**, subscribed to `transaction.created`, filtered to the selected card
and **Test** mode. The API test key cannot change webhook configuration. Keep
the existing production endpoints intact. Store the one-time signing secret in
the receiver's private configuration before testing delivery.

Use **Send test** to check connectivity. Then run the POS script with a new test
receipt to check a real test-transaction event. Check the endpoint's delivery
history and your receiver's receipt; webhook dispatch is asynchronous.

| Receiver check | Expected result |
| - | - |
| Valid signed test transaction | `200` and `receipt: stored` |
| Retry/manual resend after restarting the receiver | `200` and `receipt: duplicate`; one saved receipt |
| Missing, altered, old or future-dated signature | `400`; nothing saved |
| Different business | `403`; nothing saved |
| Live transaction | `422 live_event_refused`; nothing saved |
| Storage failure | `503`; no success acknowledgement, so Perkstar can retry |

The database file must survive restarts. Do not deploy this teaching receiver on
an ephemeral filesystem. Before production, use a durable inbox/queue shared by
all instances, process real business work asynchronously and make downstream
operations idempotent too. Do not simply remove the live-event rejection.
Node's built-in SQLite module may show an experimental warning on earlier Node
24 releases; see the [Node SQLite documentation](https://nodejs.org/download/release/latest-v24.x/docs/api/sqlite.html).

## If a check fails

* **401:** the key may be wrong or expired. Ask its owner to replace it;
  do not print the secret while diagnosing it.
* **410:** the key has been revoked. Stop using it and ask its owner for a replacement.
* **403:** check the selected scopes and business access. Add only the missing
  permission needed for the intended operation.
* **404:** verify the enrolment belongs to the chosen business/card.
* **409:** check reused request keys and receipt references. Changing a payload
  is not a retry of the original operation.
* **429:** wait for `Retry-After` before rerunning with the same receipt ID.
* **Timeout/5xx:** the write may have succeeded. Keep the receipt ID unchanged,
  inspect the outcome and use a bounded retry policy; never loop indefinitely.
* **Webhook 307/308:** enter the final HTTPS destination directly. Perkstar does
  not follow redirects; confirm the exact route accepts POST.
* **Webhook timeout/5xx:** fix storage/availability and acknowledge promptly after
  persistence. Resend an exhausted delivery only after correcting the receiver.

See [webhook recovery](/guides/webhooks) and [idempotency](/fundamentals/idempotency).

## Finish testing and move deliberately to production

Record the expected business, card, connection owner, expiry and successful
transaction/delivery IDs. Revoke temporary test keys and disable temporary test
endpoints. Keep or remove the local receipt database according to your test-data
retention policy. Do not run the all-test-transactions cleanup endpoint on a
shared business without agreement: it removes other developers' test rows too.

The owner then creates a **separate**, narrowly scoped live key for the finished
integration. Store it server-side, use distinct live receipt references, verify
one agreed real transaction, and monitor failures. This downloadable script
continues to refuse live keys. For future rotation, create a replacement with
the required overlap, check last-use evidence and revoke the old key when the
consumer has switched; an already expired key is not revived by rotation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.