Authentication and Scopes
- Base path
/api/v1/physical-pos- Identity
- Ed25519 request signature on every request —
X-Key-Id,X-Date,X-Nonce,X-Signature, plusDigeston requests with a body. ±5 min clock window, single-use nonce. A Bearer token or a panel session is not accepted here. - Authorization
- The key must carry the Physical POS product (
PHYSICAL_POS) and the scope for the endpoints on this page:physical_pos:view·physical_pos:manage. Without the product the call gets403 PRODUCT_NOT_ENABLED; without the scope403 API_INSUFFICIENT_SCOPE. See Authentication and Scopes. - Cross-surface
- These endpoints are the s2s surface — call them from your own server with a signed request. The same records have screens in the panel, but panel endpoints cannot be called with an API key, and these endpoints cannot be called with a panel session.
You call the Physical POS management endpoints from your own server with requests signed by your API key. Every request is checked in order for three things: signature, product and scope.
Request signature
Every request is signed with your API key. An Authorization: Bearer token or a panel session does not authenticate these endpoints; a request without signature headers gets 401.
| Header | Value |
|---|---|
X-Key-Id | Your key ID: mk_test_… for test, mk_live_… for live |
X-Date | ISO 8601 timestamp, within ±5 minutes of server time |
X-Nonce | A unique value per request; single use |
Digest | Only on requests with a body (POST, PATCH): the SHA-256 digest of the body as SHA-256=<base64> |
X-Signature | Base64 Ed25519 signature of the canonical string |
The canonical string joins the lines METHOD, the full path (including the query string), X-Date:…, X-Nonce:… and, if there is a body, Digest:… with \n. Requests without a body have no Digest line at all. Full rules and key management: Authentication › Canonical string · Digest header format.
The signed path includes the query string and must match the path you send exactly: when signing GET /api/v1/physical-pos/stores?status=ACTIVE&limit=50, the second line is /api/v1/physical-pos/stores?status=ACTIVE&limit=50.
Signing helper
The cURL examples in this section compute the signature with the shell function below (requires OpenSSL 3.0 or later). If you pass a body, Digest is computed too:
# lunixi-sign.sh — Usage:
# source lunixi-sign.sh
# lunixi_sign GET "/api/v1/physical-pos/stores"
# lunixi_sign POST "/api/v1/physical-pos/stores" "$BODY"
LUNIXI_KEY_ID="mk_test_..."
LUNIXI_KEY_FILE="lunixi-signing.key"
lunixi_sign() {
local method="$1" req_path="$2" body="$3"
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
NONCE=$(openssl rand -hex 16)
if [ -n "$body" ]; then
DIGEST="SHA-256=$(printf '%s' "$body" | openssl dgst -sha256 -binary | base64)"
printf '%s\n%s\nX-Date:%s\nX-Nonce:%s\nDigest:%s' "$method" "$req_path" "$DATE" "$NONCE" "$DIGEST" > canonical.txt
else
DIGEST=""
printf '%s\n%s\nX-Date:%s\nX-Nonce:%s' "$method" "$req_path" "$DATE" "$NONCE" > canonical.txt
fi
SIGNATURE=$(openssl pkeyutl -sign -inkey "$LUNIXI_KEY_FILE" -rawin -in canonical.txt | base64 | tr -d '\n')
}The digest is computed from the compact JSON body. The server parses the body, re-serializes it in compact form and computes the digest from that text. Send the body without whitespace, with non-ASCII characters unescaped ("Kadıköy", not "Kad\u0131k\u00f6y") and with / unescaped; compute the digest from the UTF-8 bytes of the same text. A body in any other form gets 401 INVALID_DIGEST. Details: Authentication › The Digest header format.
When you build the body in your own code:
// Node.js
const body = JSON.stringify(payload);# Python
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)// PHP
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);A read request:
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/overview"
lunixi_sign GET "$REQ_PATH"
curl "https://api-gateway.lunixi.com$REQ_PATH" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"A request with a body:
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores"
BODY='{"code":"IST-KADIKOY","name":"Kadıköy Store"}'
lunixi_sign POST "$REQ_PATH" "$BODY"
curl -X POST "https://api-gateway.lunixi.com$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "Digest: $DIGEST" \
-H "X-Signature: $SIGNATURE" \
-H "Idempotency-Key: $(uuidgen)" \
--data-raw "$BODY"Product
After the signature is verified, the key is checked for the Physical POS product (PHYSICAL_POS). Without it the request gets 403 PRODUCT_NOT_ENABLED. → Error Handling › PRODUCT_NOT_ENABLED
Scopes
After the product check, the key must carry the scope the endpoint requires. Otherwise the request gets 403 API_INSUFFICIENT_SCOPE.
| Scope | Grants | Endpoints |
|---|---|---|
physical_pos:view | Read | Every GET endpoint: catalog, store and register lists and details, change histories, terminals, bank rows, devices, assignment and link histories, overview counts |
physical_pos:manage | Write | Every POST and PATCH endpoint: create, update, archive and restore stores and registers; create, update and close terminals; add and update bank rows; register, update, assign, move to storage and retire devices; replace and unlink the device |
physical_pos:identity | Device identity operations | None of the endpoints in this section require this scope. |
physical_pos:manage does not include read access. An integration that both reads and writes needs both scopes on its key. For example, you must read the current rowVersion before updating a terminal, which requires physical_pos:view.
The scopes are listed in the panel under your API key's permission settings, in the Physical POS group; you can add and remove them per key. Give a key only the scopes its job needs: physical_pos:view is enough for an integration that only reports.
Your key determines the environment (test or live); scopes mean the same in both. A test key can only read records; write requests get 403 physical_pos.environment.test_key_read_only. → Environments
Identity and tenant
The organization a request is made for, and its environment, are resolved only from your signed key. Do not send organizationId, merchantId or environment in the body or the query: organizationId and merchantId get 400 VALIDATION_ERROR, environment gets 400 physical_pos.input.environment_invalid. A record that belongs to another organization gets the same 404 as a record that does not exist.
Next steps
- Differences between test and live: Environments
- Pagination,
Idempotency-Key,rowVersionand the response format: Common Rules