Skip to content

Endpoints

All six live under /api/v1/open-network/v1. Every response is JSON and carries a success flag; reads return data, writes also return a message.

GET/pingNo scope required

Check your credentials

Answers which key you just used, which environment it belongs to and what it is allowed to reach. It needs a valid key and no scope, so it is the one call that can tell a misconfigured integration apart from a wrong one — start here before debugging a 403 anywhere else.

Response
{
  "success": true,
  "data": {
    "ok": true,
    "environment": "SANDBOX",
    "scopes": ["REGISTRATION", "UPDATE", "INVENTORY"],
    "keyPrefix": "lok_sbx_k7m2xqbd"
  }
}
Status codes
  • 200The key is valid, and this is what it can do
  • 401Missing, malformed, unknown, revoked or expired
POST/catalog/itemsScope: REGISTRATION

Create or replay a catalog item

Sends one item from your system to Localoy, addressed by your own id. The write is idempotent on externalId: the first call creates the row and answers 201, and a repeat converges the stored row onto what you sent and answers 200. A retried sync cannot know which of the two it is performing, so neither is treated as an error.

Request body
{
  "externalId": "SKU-1042",
  "name": "Chicken Shawarma Platter",
  "description": "Served with garlic sauce and pita.",
  "priceCents": 45000,
  "currency": "BDT",
  "available": true,
  "stock": 24,
  "metadata": { "posCategory": "mains", "posId": "8812" }
}
Response
{
  "success": true,
  "message": "Catalog item created",
  "data": {
    "id": "clx8m2p9k0001qz7f3d8a1b2c",
    "externalId": "SKU-1042",
    "name": "Chicken Shawarma Platter",
    "description": "Served with garlic sauce and pita.",
    "priceCents": 45000,
    "currency": "BDT",
    "available": true,
    "stock": 24,
    "metadata": { "posCategory": "mains", "posId": "8812" },
    "localoyRef": null,
    "createdAt": "2026-08-12T09:14:02.113Z",
    "updatedAt": "2026-08-12T09:14:02.113Z"
  }
}
Status codes
  • 201Created — the item did not exist
  • 200Replayed — it existed and was updated
  • 400A field failed validation
  • 401Key missing or not valid
  • 403The key has no REGISTRATION scope
Worth knowing
  • externalId and name are the only required fields. Everything else takes the column default: priceCents 0, currency BDT, available true, stock null, description and metadata null.
  • Because a repeat replaces the row, this endpoint is safe to call on a schedule for your whole catalogue — you do not have to track what Localoy already has.
GET/catalog/itemsScope: INVENTORY

List your catalog items

Pages over everything the key's business has synced, most recently created first. Use it to reconcile — to answer 'did all 400 of my items actually arrive?' without re-sending them.

Response
{
  "success": true,
  "data": {
    "items": [ {
    "id": "clx8m2p9k0001qz7f3d8a1b2c",
    "externalId": "SKU-1042",
    "name": "Chicken Shawarma Platter",
    "description": "Served with garlic sauce and pita.",
    "priceCents": 45000,
    "currency": "BDT",
    "available": true,
    "stock": 24,
    "metadata": { "posCategory": "mains", "posId": "8812" },
    "localoyRef": null,
    "createdAt": "2026-08-12T09:14:02.113Z",
    "updatedAt": "2026-08-12T09:14:02.113Z"
  } ],
    "total": 128,
    "limit": 50,
    "offset": 0
  }
}
Status codes
  • 200Page returned
  • 400limit, offset or available was not well formed
  • 403The key has no INVENTORY scope
Worth knowing
  • Query parameters: limit (default 50, capped at 200), offset (default 0), and available=true|false to narrow to one side. Any other value of available is a 400.
  • A limit above 200 is silently clamped rather than refused — it is a reasonable request for more, not a mistake.
  • Items are ordered by creation time, newest first. Because updating an item does not move it, an offset-based walk stays stable while a sync is running; only a create can shift later pages.
GET/catalog/items/{externalId}Scope: INVENTORY

Read one catalog item

Addressed by your own id, not by the Localoy id. An id belonging to another business is an ordinary 404: the lookup is scoped to your business before it happens, so someone else's row is not reachable to be refused.

Response
{
  "success": true,
  "data": {
    "id": "clx8m2p9k0001qz7f3d8a1b2c",
    "externalId": "SKU-1042",
    "name": "Chicken Shawarma Platter",
    "description": "Served with garlic sauce and pita.",
    "priceCents": 45000,
    "currency": "BDT",
    "available": true,
    "stock": 24,
    "metadata": { "posCategory": "mains", "posId": "8812" },
    "localoyRef": null,
    "createdAt": "2026-08-12T09:14:02.113Z",
    "updatedAt": "2026-08-12T09:14:02.113Z"
  }
}
Status codes
  • 200Item returned
  • 403The key has no INVENTORY scope
  • 404No item with that externalId for this business
PATCH/catalog/items/{externalId}Scope: UPDATE

Change part of a catalog item

A partial write: only the keys you send are touched. This is the endpoint for the small, frequent changes — a price move, a sold-out flag, a stock count — where re-sending the whole item would be wasteful.

Request body
{
  "priceCents": 42000,
  "stock": 0,
  "available": false
}
Response
{
  "success": true,
  "message": "Catalog item updated",
  "data": {
    "id": "clx8m2p9k0001qz7f3d8a1b2c",
    "externalId": "SKU-1042",
    "name": "Chicken Shawarma Platter",
    "description": "Served with garlic sauce and pita.",
    "priceCents": 45000,
    "currency": "BDT",
    "available": true,
    "stock": 24,
    "metadata": { "posCategory": "mains", "posId": "8812" },
    "localoyRef": null,
    "createdAt": "2026-08-12T09:14:02.113Z",
    "updatedAt": "2026-08-12T09:14:02.113Z"
  }
}
Status codes
  • 200Item updated
  • 400Validation failed, or the body was empty
  • 403The key has no UPDATE scope
  • 404No item with that externalId for this business
Worth knowing
  • An empty body is a 400, not a no-op — a PATCH that changes nothing is always a bug in the caller.
  • Sending externalId is a 400 (external_id_immutable). It is what every call addresses the row by, so renaming it would orphan whatever you already have pointing at it. Delete the item and create it under the new id instead.
  • For nullable fields, absent and null differ: leaving description out keeps it, sending "description": null clears it.
DELETE/catalog/items/{externalId}Scope: UPDATE

Delete a catalog item

Removes the row outright. Prefer available: false for something temporarily off the menu — a delete loses the history and the item has to be created again.

Response
{
  "success": true,
  "message": "Catalog item deleted"
}
Status codes
  • 200Item deleted
  • 403The key has no UPDATE scope
  • 404No item with that externalId for this business
GET/bookablesScope: INVENTORY

List what your items can be linked to

Every Localoy bookable your business has — event ticket types, activity items and, where table booking is set up, your restaurant — with the id to put in an item's localoyRef. Linking an item is what makes Localoy ask your inventory endpoint before booking it; see Inventory checker.

Response
{
  "success": true,
  "data": {
    "items": [
      {
        "type": "event_ticket",
        "id": "cmtf2kq9p0004xfe1abcd1234",
        "name": "VIP",
        "experienceId": "cmtf2kq3m0001xfe1abcd0000",
        "experienceName": "Rooftop Jazz Night"
      },
      {
        "type": "dining",
        "id": "cmtf2m1rb0007xfe1abcd5678",
        "name": "Table reservation",
        "experienceId": "cmtf2m1rb0007xfe1abcd5678",
        "experienceName": "Your restaurant"
      }
    ]
  }
}
Status codes
  • 200The list, possibly empty
  • 403The key has no INVENTORY scope
Worth knowing
  • type is event_ticket, activity_item or dining — the same words localoyRef takes.
  • Only your own business's bookables are listed, and only those can be linked: an id from anywhere else is a 404 on the item write.
GET/healthNo scope required

Is the API up?

Answers whether the Open Network API can serve an authenticated request right now — its database and its rate-limit store. It takes NO key and is not rate limited per key, so point an uptime monitor at it; Localoy's own monitor does, once a minute.

Response
{
  "success": true,
  "data": {
    "status": "ok",
    "services": { "database": "healthy", "redis": "healthy" }
  }
}
Status codes
  • 200Up — authenticated calls can be served
  • 503Down — status is unavailable and services says which part
Worth knowing
  • No key is needed and none is read: a monitor must not hold a credential to ask whether the door is open.
GET/payments/sessions/{id}Scope: PAYMENT

Read what a customer owes

Your checkout page calls this to learn the amount, the currency and who is paying. THE AMOUNT COMES FROM HERE — never from the query string the customer arrived with, which is a number they can edit in their own address bar. This response is authenticated by your key, so the browser that carried them cannot alter it.

Response
{
  "success": true,
  "data": {
    "id": "cmsrirjpr0013xfwtsrurkqnj",
    "status": "PENDING",
    "environment": "PRODUCTION",
    "amountMinor": 61600,
    "currency": "BDT",
    "description": "2 tickets — Salsa Night Extravaganza",
    "subject": {
      "type": "EVENT_TICKET_ORDER",
      "id": "cmsrirb2i000xxfwt1bsl05t5",
      "experienceId": "cmruwm61z1mb6xfblrgr3i4kf"
    },
    "customer": { "name": "Shagato Chowdhury", "phone": "01712345678", "email": null },
    "returnUrl": "https://partner.localoy.app/payments/return?session_id=…&token=…",
    "expiresAt": "2026-08-13T13:23:40.613Z",
    "isExpired": false,
    "isTest": false,
    "providerReference": null,
    "settledAt": null,
    "createdAt": "2026-08-13T12:53:40.624Z"
  }
}
Status codes
  • 200The session, as Localoy holds it
  • 403The key has no PAYMENT scope
  • 404No such session for this business — or it belongs to the other environment
Worth knowing
  • amountMinor is in MINOR UNITS: 61600 is 616.00 BDT. The order itself stores whole taka; the conversion happens once, when the session is opened.
  • A sandbox key cannot read a live session and a live key cannot read a sandbox one — both answer 404. That separation is what stops a test integration from seeing a real customer's details.
  • Send the customer to returnUrl when you are done, whatever the outcome. It is a navigation, not a report — the result call below is what actually settles the order.
  • subject.type is EVENT_TICKET_ORDER, ACTIVITY_BOOKING or DINING_RESERVATION. A session with a null subject is a sandbox rehearsal with no real order behind it.
POST/payments/sessions/{id}/resultScope: PAYMENT

Report how the payment ended

THE CALL THAT MARKS AN ORDER PAID. Nothing else does — not the customer returning to your return URL, not your checkout page having been opened. Make it from your SERVER, after your gateway has confirmed the money, and send an Idempotency-Key so a retried callback does not settle the same order twice.

Request body
{
  "status": "SUCCEEDED",
  "amountMinor": 61600,
  "currency": "BDT",
  "providerReference": "SSLCZ-99812"
}
Response
{
  "success": true,
  "message": "Payment result recorded.",
  "replayed": false,
  "data": {
    "id": "cmsrirjpr0013xfwtsrurkqnj",
    "status": "SUCCEEDED",
    "paidAmountMinor": 61600,
    "providerReference": "SSLCZ-99812",
    "settledAt": "2026-08-13T12:54:04.416Z",
    "settledAfterExpiry": false
  }
}
Status codes
  • 200Recorded — or replayed, see `replayed`
  • 400Validation failed, or SUCCEEDED was sent without amountMinor
  • 403The key has no PAYMENT scope
  • 404No such session for this business
  • 409The move is not allowed, the amount did not match, or the order was already settled another way
Worth knowing
  • status is SUCCEEDED, FAILED, CANCELLED or PROCESSING. EXPIRED is Localoy's own clock and a refund has its own endpoint, so neither is reportable here.
  • amountMinor is REQUIRED on SUCCEEDED and is checked against what the session quoted. A mismatch is a 409 and the order is NOT marked paid — the attempt is still recorded and shows in your Payment Log, because it means money was taken that Localoy will not acknowledge.
  • An EXPIRED session still accepts SUCCEEDED. Our window lapsing is not a reason to deny a payment that happened; the session comes back with settledAfterExpiry: true.
  • A 409 payment_subject_not_chargeable means the order was settled outside this session — someone marked it paid at the counter, or it was refunded. Nothing was applied; reconcile before retrying.
  • CANCELLED is terminal, FAILED is not: a customer whose card was declined can try again on the same session, and FAILED → SUCCEEDED is allowed.
POST/payments/sessions/{id}/refundScope: PAYMENT

Record a refund you have already made

Reverses a succeeded payment and its order. Localoy moves no money — this records a refund your own gateway has already processed.

Request body
{
  "providerReference": "SSLCZ-RF-4",
  "reason": "customer cancelled"
}
Response
{
  "success": true,
  "message": "Refund recorded.",
  "replayed": false,
  "data": { "id": "cmsrirjpr0013xfwtsrurkqnj", "status": "REFUNDED", "refundedAt": "2026-08-13T12:54:33.400Z" }
}
Status codes
  • 200Refund recorded, or already recorded
  • 403The key has no PAYMENT scope
  • 404No such session for this business
  • 409Only a succeeded payment can be refunded
Worth knowing
  • What a refund does to the order depends on WHAT was bought. A refunded event ticket order loses its check-ins — entry is what the ticket bought. A refunded activity booking keeps them: the activity still happened, and the partner reconciles attendance against it.
  • paidAt is never cleared. The sales trend buckets on it, so clearing it would rewrite history that did happen.