Getting started

Authenticate with an API key, then call the Perkss Partner API over HTTPS.
View as Markdown

The Perkss Partner API lives under /api/v1. All requests use HTTPS, JSON bodies (unless noted), and a Bearer token obtained with your API key.

Base URL: https://api.perkss.io/api/v1

Every API key belongs to exactly one company. Everything you read or write is scoped to that company.

Authentication

1

Create an API key

In the Perkss CMS, go to API Keys in your “Admin” panel and create a client_secret (prefixed perkss_sk_live_).The secret is shown exactly once — store it in a secret manager. Only a display prefix is visible afterwards.

If you lose it, you will have to create a new one. Keep it safe!

2

Exchange it for an access token

POST /api/v1/auth/token implements the OAuth 2.0 client-credentials grant (RFC 6749 §4.4). Send the credentials form-encoded (or as HTTP Basic auth):

$curl -X POST https://api.perkss.io/api/v1/auth/token \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d "grant_type=client_credentials" \
> -d "client_id=YOUR_CLIENT_ID" \
> -d "client_secret=YOUR_CLIENT_SECRET"
1{ "access_token": "eyJ…", "token_type": "Bearer",

Tokens last 24 hours by default. Pass ttl_seconds (60–86400) to shorten or cap it.

3

Call the API

Send the token on every request:

$curl https://api.perkss.io/api/v1/customers \
> -H "Authorization: Bearer eyJ…"

Re-exchange for a new token before expires_in elapses. Exchanges are cheap; don’t cache tokens past their lifetime.

client_secret is a server-side credential. Never embed it in a browser, mobile app, or public repository. If a secret leaks, revoke the key in the CMS — revocation immediatelyery token minted from it**.

POST /api/v1/auth/logout revokes the calling tok

Rate limits

Limits are enforced per minute, in separate buckets:

ScopeLimit
Token endpoint (per IP)10 / min
Reads, per API key600 / min
Writes, per API key120 / min

Reads and writes never share a bucket, so heavy re(and vice versa).

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets). When you exceed a limit you get 429 with code RATE_LIMIT.EXCEEDED and a Retry-After header (seconds). Honor Retry-After — retrying earlier only consumes your next window.

Idempotency

Transaction writes move money-like state, so they are protected against accidental double-execution.

Send an Idempotency-Key header — any unique stri

$curl -X POST https://api.perkss.io/api/v1/customers/{customerId}/purchase \
> -H "Authorization: Bearer eyJ…" \
> -H "Idempotency-Key: 4f9d36c1-6c2e-4b7e-9d1a-0a4f5b3c2e1d" \
> -H "Content-Type: application/json" \
> -d '{ "amountCents": 1000 }'
EndpointIdempotency-Key
POST /customers/{id}/purchase, /adjustment, /redeem-rewardRequired — missing key returns 400
POST /customers and POST /customers/{id}/wallet/pass-linkOptional — honoured when present

How it behaves:

  • Retry with the same key and same body → the original response is replayed (stored for 24 hours). The write executes once.
  • Same key, different body409 IDEMPOTENCY.PAYLOAD_MISMATCH. Keys identify one operation; never reuse them.
  • Duplicate while the first attempt is still running409 IDEMPOTENCY.REQUEST_IN_PROGRESS with Retry-After. Wait and retry
    with the same key.
  • Failed requests are not stored — retrying a rejected write with the same key re-executes it once the blocking condition is resolved.

Keys are scoped per company, method, and path, so a key used on one endpoint never replays another endpoint’s response.

Errors

Two shapes, by design:

The token endpoint returns standard OAuth error bodies (RFC 6749 §5.2):

1{ "error": "invalid_client", "error_description": "Client authentication failed", "request_id": "01J…" }

Everything else returns the Perkss error envelope:

1{ "error": { "code": "API_CUSTOMERS.NOT_FOUND", "message": "Customer not found", "requestId": "01J…", "httpStatus": 404 } }

Codes worth handling explicitly:

CodeStatusMeaning
API_CUSTOMERS.ALREADY_EXISTS409A customer with this phone already exists in your company
CUSTOMERS.DUPLICATE_EXTERNAL_ID409Your `ed to another customer
API_CUSTOMERS.NOT_FOUND404Unknown customer id, or it belongs to another company
API_TRANSACTIONS.WRITE_REJECTED422The loyalty engine rejected the write — see context.failureReason (e.g. INSUFFICIENT_BALANCE, NO_ACTIVE_PROGRAM)
RATE_LIMIT.EXCEEDED429Over a rate limit — honor Retry-After
VALIDATION.REQUEST_INVALID400Body/query ields are rejected

Include the requestId when contacting support — es.

Customer identity

  • Phone is the identity. It’s required at creation, accepted in any parseable format, and stored canonicalized to E.164. Creating a customer with an already-registered phone returns 409 — one phone, one customer per company.
  • externalId is your alias. Optional, uniqueterable (GET /customers?externalId=… returns 0 or1 rows). Use it to link Perkss customers to your own system without storing our ids.
  • Email is not an identity. Multiple customers may share one; the email filter returns all of them.

Pagination

List endpoints take page (1-based, default 1) and pageSize (default 20, max 100) and always return the same shape:

1{ "items": [], "total": 137, "page": 1, "pageSi

The API is browser-callable (CORS allows any origin, Bearer-only, no cookies), but because the client_secret must stay server-side, the intended integration is from your backend.