Physical POS
API Reference
Stores

Stores

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.

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

MethodPathScope
GET/api/v1/physical-pos/storesphysical_pos:view
POST/api/v1/physical-pos/storesphysical_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}/archivephysical_pos:manage
POST/api/v1/physical-pos/stores/{storeId}/restorephysical_pos:manage
GET/api/v1/physical-pos/stores/{storeId}/auditphysical_pos:view

The cURL examples compute the signature with lunixi-sign.sh: Authentication › Signing helper.

Store object

FieldTypeDescription
idstring (UUID)Store ID.
codestringStore code; upper-case, unique in your organization. Does not change after creation.
namestringStore name.
citystring | nullCity.
districtstring | nullDistrict.
addressLinestring | nullAddress line.
timezonestringIANA time zone, e.g. Europe/Istanbul.
storeStatusstringACTIVE or ARCHIVED.
externalRefstring | nullThe store ID in your own system; unique in your organization.
rowVersionintegerConcurrency version. → rowVersion
createdAtstringCreation time.
updatedAtstringLast change time.

List stores

GET /api/v1/physical-pos/stores

Query parameterTypeDescription
statusstringACTIVE or ARCHIVED. If omitted, both are returned. Any other value gets 400 physical_pos.input.filter_invalid.
qstringRecords whose store code or name starts with this value, at most 64 characters.
limitintegerRecords per page, 1–100. Default 25.
cursorstringpageInfo.nextCursor from the previous response.
includeTotalbooleanWhen 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

FieldTypeRequiredDescription
codestringYesStore code: A-Z, 0-9, ., _, -; at most 64 characters. Upper-cased. → Code rules
namestringYesStore name, at most 200 characters.
citystringNoCity, at most 100 characters.
districtstringNoDistrict, at most 100 characters.
addressLinestringNoAddress line, at most 300 characters.
timezonestringNoIANA time zone. Default Europe/Istanbul. → Time zone
externalRefstringNoThe 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"
}
FieldDescription
storeThe store object.
registerSummary.activeCountNumber of active registers.
registerSummary.archivedCountNumber of archived registers.
assignedDeviceCountNumber 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}

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
namestringNoNew name.
citystring | nullNonull clears the field.
districtstring | nullNonull clears the field.
addressLinestring | nullNonull clears the field.
timezonestringNoNew IANA time zone.
externalRefstring | nullNonull clears the field.
  • Fields you do not send are not changed. code cannot be changed; sending code in the body returns 400 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, rowVersion does 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

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
reasonCodestringYesSTORE_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.

BlockerError
A device is assigned to the store or one of its registers409 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 registers409 physical_pos.store.too_many_active_registers (details.limit: 200) — archive the registers one by one first.
The store is already archived409 physical_pos.store.invalid_transition

Restore a store

POST /api/v1/physical-pos/stores/{storeId}/restore

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
reasonCodestringYesREOPENED, 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 parameterTypeDescription
limitintegerEvents per page, 1–100. Default 25.
cursorstringpageInfo.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

HTTPcodeWhen
400physical_pos.idempotency.key_required / physical_pos.idempotency.key_invalidIdempotency-Key missing or malformed on create.
400physical_pos.input.code_invalidcode is malformed.
400physical_pos.input.text_invalidA free-text field breaks the rules.
400physical_pos.input.ref_invalidexternalRef is malformed.
400physical_pos.input.reason_code_invalidreasonCode is not on the list or does not fit the operation.
400physical_pos.validation.invalidUnknown time zone.
400physical_pos.input.empty_patchEmpty update.
400physical_pos.cursor.invalidBroken cursor.
400physical_pos.input.filter_invalidInvalid list filter.
403physical_pos.environment.test_key_read_onlyWrite request made with a test key.
404physical_pos.store.not_foundThe store does not exist or is not yours.
409physical_pos.store.code_takenThe code is in use (archived stores included).
409physical_pos.store.external_ref_takenexternalRef is in use (archived stores included).
409physical_pos.store.archivedTried to update an archived store.
409physical_pos.store.has_devicesTried to archive a store with assigned devices.
409physical_pos.store.too_many_active_registersToo many active registers to archive at once.
409physical_pos.store.invalid_transitionArchiving an archived store or restoring an active one.
409physical_pos.version_conflictrowVersion is not current.
409physical_pos.idempotency_mismatchThe same Idempotency-Key was used with a different request.
429RATE_LIMITEDStore and register write limit exceeded. → Rate limits

All codes: Error Codes.