Stores
- 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.
A store is a point of sale of your business. Registers belong to a store: → Registers. Stores are environment-less; a test key can only read stores, and the write endpoints get 403 physical_pos.environment.test_key_read_only with a test key. → Environments
| Method | Path | Scope |
|---|---|---|
GET | /api/v1/physical-pos/stores | physical_pos:view |
POST | /api/v1/physical-pos/stores | physical_pos:manage |
GET | /api/v1/physical-pos/stores/{storeId} | physical_pos:view |
PATCH | /api/v1/physical-pos/stores/{storeId} | physical_pos:manage |
POST | /api/v1/physical-pos/stores/{storeId}/archive | physical_pos:manage |
POST | /api/v1/physical-pos/stores/{storeId}/restore | physical_pos:manage |
GET | /api/v1/physical-pos/stores/{storeId}/audit | physical_pos:view |
The cURL examples compute the signature with lunixi-sign.sh: Authentication › Signing helper.
Store object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Store ID. |
code | string | Store code; upper-case, unique in your organization. Does not change after creation. |
name | string | Store name. |
city | string | null | City. |
district | string | null | District. |
addressLine | string | null | Address line. |
timezone | string | IANA time zone, e.g. Europe/Istanbul. |
storeStatus | string | ACTIVE or ARCHIVED. |
externalRef | string | null | The store ID in your own system; unique in your organization. |
rowVersion | integer | Concurrency version. → rowVersion |
createdAt | string | Creation time. |
updatedAt | string | Last change time. |
List stores
GET /api/v1/physical-pos/stores
| Query parameter | Type | Description |
|---|---|---|
status | string | ACTIVE or ARCHIVED. If omitted, both are returned. Any other value gets 400 physical_pos.input.filter_invalid. |
q | string | Records whose store code or name starts with this value, at most 64 characters. |
limit | integer | Records per page, 1–100. Default 25. |
cursor | string | pageInfo.nextCursor from the previous response. |
includeTotal | boolean | When true, pageInfo.totalCount is filled. |
List rows carry the store object's fields plus registerCount (the store's number of active registers).
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores?status=ACTIVE&q=IST&limit=50"
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"{
"status": "success",
"code": "SUCCESS",
"data": {
"items": [
{
"id": "00000000-0000-0000-0000-000000000101",
"code": "IST-KADIKOY",
"name": "Kadıköy Store",
"city": "İstanbul",
"district": "Kadıköy",
"addressLine": "Örnek Cad. No: 1",
"timezone": "Europe/Istanbul",
"storeStatus": "ACTIVE",
"externalRef": "store_00042",
"rowVersion": 2,
"createdAt": "2026-01-01T07:00:00.000Z",
"updatedAt": "2026-01-05T09:30:00.000Z",
"registerCount": 3
}
],
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null }
},
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null },
"statusCode": 200,
"requestId": "00000000-0000-0000-0000-000000000000"
}Create a store
POST /api/v1/physical-pos/stores · Idempotency-Key required
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Store code: A-Z, 0-9, ., _, -; at most 64 characters. Upper-cased. → Code rules |
name | string | Yes | Store name, at most 200 characters. |
city | string | No | City, at most 100 characters. |
district | string | No | District, at most 100 characters. |
addressLine | string | No | Address line, at most 300 characters. |
timezone | string | No | IANA time zone. Default Europe/Istanbul. → Time zone |
externalRef | string | No | The ID in your own system: printable ASCII without spaces, at most 128 characters. Stored as is. |
Free-text fields follow the text rules.
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores"
BODY='{"code":"ist-kadikoy","name":"Kadıköy Store","city":"İstanbul","district":"Kadıköy","addressLine":"Örnek Cad. No: 1","externalRef":"store_00042"}'
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: 4f9b2c1e-0000-4000-8000-000000000001" \
--data-raw "$BODY"A new store returns 201; a repeat with the same key and the same body returns 200 and replayed: true:
{
"status": "success",
"code": "SUCCESS",
"data": {
"store": {
"id": "00000000-0000-0000-0000-000000000101",
"code": "IST-KADIKOY",
"name": "Kadıköy Store",
"city": "İstanbul",
"district": "Kadıköy",
"addressLine": "Örnek Cad. No: 1",
"timezone": "Europe/Istanbul",
"storeStatus": "ACTIVE",
"externalRef": "store_00042",
"rowVersion": 1,
"createdAt": "2026-01-01T07:00:00.000Z",
"updatedAt": "2026-01-01T07:00:00.000Z"
},
"replayed": false
},
"statusCode": 201,
"requestId": "00000000-0000-0000-0000-000000000000"
}The store code and externalRef are unique including archived stores: you cannot use the code of an archived store for a new store. To use that store again, restore it.
Retrieve a store
GET /api/v1/physical-pos/stores/{storeId}
{
"status": "success",
"code": "SUCCESS",
"data": {
"store": {
"id": "00000000-0000-0000-0000-000000000101",
"code": "IST-KADIKOY",
"name": "Kadıköy Store",
"city": "İstanbul",
"district": "Kadıköy",
"addressLine": "Örnek Cad. No: 1",
"timezone": "Europe/Istanbul",
"storeStatus": "ACTIVE",
"externalRef": "store_00042",
"rowVersion": 2,
"createdAt": "2026-01-01T07:00:00.000Z",
"updatedAt": "2026-01-05T09:30:00.000Z"
},
"registerSummary": { "activeCount": 3, "archivedCount": 1 },
"assignedDeviceCount": 0
},
"statusCode": 200,
"requestId": "00000000-0000-0000-0000-000000000000"
}| Field | Description |
|---|---|
store | The store object. |
registerSummary.activeCount | Number of active registers. |
registerSummary.archivedCount | Number of archived registers. |
assignedDeviceCount | Number of devices assigned to the store (and its registers). |
A store that does not exist, belongs to another organization or has a malformed ID returns 404 physical_pos.store.not_found.
Update a store
PATCH /api/v1/physical-pos/stores/{storeId}
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
name | string | No | New name. |
city | string | null | No | null clears the field. |
district | string | null | No | null clears the field. |
addressLine | string | null | No | null clears the field. |
timezone | string | No | New IANA time zone. |
externalRef | string | null | No | null clears the field. |
- Fields you do not send are not changed.
codecannot be changed; sendingcodein the body returns400 VALIDATION_ERROR. - A request with no field to change gets
400 physical_pos.input.empty_patch. - If the values you send equal the current ones, the record is not changed,
rowVersiondoes not increase and no change-history event is written. - An archived store cannot be updated:
409 physical_pos.store.archived. Restore it first.
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores/00000000-0000-0000-0000-000000000101"
BODY='{"rowVersion":2,"name":"Kadıköy Moda Store","addressLine":null}'
lunixi_sign PATCH "$REQ_PATH" "$BODY"
curl -X PATCH "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" \
--data-raw "$BODY"Response: { "store": { … } } — the current store object with the new rowVersion.
Archive a store
POST /api/v1/physical-pos/stores/{storeId}/archive
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
reasonCode | string | Yes | STORE_CLOSED, TEMPORARILY_CLOSED, RELOCATED, MERGED, CREATED_IN_ERROR, DATA_CORRECTION, OTHER. → Reason codes |
When a store is archived, its active registers are archived in the same operation. The response also gives the number of archived registers:
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores/00000000-0000-0000-0000-000000000101/archive"
BODY='{"rowVersion":3,"reasonCode":"STORE_CLOSED"}'
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" \
--data-raw "$BODY"{
"status": "success",
"code": "SUCCESS",
"data": {
"store": {
"id": "00000000-0000-0000-0000-000000000101",
"code": "IST-KADIKOY",
"storeStatus": "ARCHIVED",
"rowVersion": 4
},
"archivedRegisterCount": 3
},
"statusCode": 200,
"requestId": "00000000-0000-0000-0000-000000000000"
}The store object is shortened in the example; the response contains every field.
| Blocker | Error |
|---|---|
| A device is assigned to the store or one of its registers | 409 physical_pos.store.has_devices — move the devices to another store, move them to storage or retire them. |
| The store has more than 200 active registers | 409 physical_pos.store.too_many_active_registers (details.limit: 200) — archive the registers one by one first. |
| The store is already archived | 409 physical_pos.store.invalid_transition |
Restore a store
POST /api/v1/physical-pos/stores/{storeId}/restore
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
reasonCode | string | Yes | REOPENED, ARCHIVED_IN_ERROR, DATA_CORRECTION, OTHER. |
The archived store becomes ACTIVE; its code and externalRef are kept. Registers stay archived; restore the ones you need one by one. If the store is already active: 409 physical_pos.store.invalid_transition.
Response: { "store": { … } }.
Change history
GET /api/v1/physical-pos/stores/{storeId}/audit
| Query parameter | Type | Description |
|---|---|---|
limit | integer | Events per page, 1–100. Default 25. |
cursor | string | pageInfo.nextCursor from the previous response. |
{
"status": "success",
"code": "SUCCESS",
"data": {
"items": [
{
"id": "00000000-0000-0000-0000-000000000901",
"action": "physical_pos.audit.store_updated",
"actorType": "API_KEY",
"actorId": "kid:mk_live_00000000",
"reasonCode": null,
"changes": {
"storeId": "00000000-0000-0000-0000-000000000101",
"storeNo": "IST-KADIKOY",
"nameChanged": true
},
"requestId": "00000000-0000-0000-0000-000000000000",
"createdAt": "2026-01-05T09:30:00.000Z"
}
],
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null }
},
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null },
"statusCode": 200,
"requestId": "00000000-0000-0000-0000-000000000000"
}Store events: physical_pos.audit.store_created, …store_updated, …store_archived, …store_restored. Field meanings: Common Rules › Change history.
Errors
| HTTP | code | When |
|---|---|---|
400 | physical_pos.idempotency.key_required / physical_pos.idempotency.key_invalid | Idempotency-Key missing or malformed on create. |
400 | physical_pos.input.code_invalid | code is malformed. |
400 | physical_pos.input.text_invalid | A free-text field breaks the rules. |
400 | physical_pos.input.ref_invalid | externalRef is malformed. |
400 | physical_pos.input.reason_code_invalid | reasonCode is not on the list or does not fit the operation. |
400 | physical_pos.validation.invalid | Unknown time zone. |
400 | physical_pos.input.empty_patch | Empty update. |
400 | physical_pos.cursor.invalid | Broken cursor. |
400 | physical_pos.input.filter_invalid | Invalid list filter. |
403 | physical_pos.environment.test_key_read_only | Write request made with a test key. |
404 | physical_pos.store.not_found | The store does not exist or is not yours. |
409 | physical_pos.store.code_taken | The code is in use (archived stores included). |
409 | physical_pos.store.external_ref_taken | externalRef is in use (archived stores included). |
409 | physical_pos.store.archived | Tried to update an archived store. |
409 | physical_pos.store.has_devices | Tried to archive a store with assigned devices. |
409 | physical_pos.store.too_many_active_registers | Too many active registers to archive at once. |
409 | physical_pos.store.invalid_transition | Archiving an archived store or restoring an active one. |
409 | physical_pos.version_conflict | rowVersion is not current. |
409 | physical_pos.idempotency_mismatch | The same Idempotency-Key was used with a different request. |
429 | RATE_LIMITED | Store and register write limit exceeded. → Rate limits |
All codes: Error Codes.