Physical POS
Authentication and Scopes

Authentication and Scopes

Who calls these endpoints?External API (s2s)
Base path
/api/v1/physical-pos
Identity
Ed25519 request signature on every request — X-Key-Id, X-Date, X-Nonce, X-Signature, plus Digest on 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 gets 403 PRODUCT_NOT_ENABLED; without the scope 403 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.

HeaderValue
X-Key-IdYour key ID: mk_test_… for test, mk_live_… for live
X-DateISO 8601 timestamp, within ±5 minutes of server time
X-NonceA unique value per request; single use
DigestOnly on requests with a body (POST, PATCH): the SHA-256 digest of the body as SHA-256=<base64>
X-SignatureBase64 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.

ScopeGrantsEndpoints
physical_pos:viewReadEvery GET endpoint: catalog, store and register lists and details, change histories, terminals, bank rows, devices, assignment and link histories, overview counts
physical_pos:manageWriteEvery 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:identityDevice identity operationsNone 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, rowVersion and the response format: Common Rules