{"openapi":"3.1.0","info":{"title":"iBOX Terminal API","version":"1.0.0","description":"The API a parcel-locker terminal calls. Everything a kiosk does — showing its doors,\nverifying a pickup code, taking a courier's deposit, accepting a customer's return —\nhappens through these endpoints and nothing else.\n\n**Authentication.** Send the device's own token as `Authorization: Bearer <token>`.\nA token identifies one physical terminal and binds it to exactly one station: every\nrequest is scoped to that station's doors and parcels, and the station is never taken\nfrom a request body. Tokens are stored only as a hash. A terminal whose status is not\n`online` is refused exactly like an unknown token.\n\n**Two-step operations.** Opening a door and recording what happened are always two\ncalls, and the second one matters:\n\n- Pickup: `POST /pickup` verifies the code and tells you which door to open. It does\n  NOT mark the parcel collected. `POST /door/confirm` does, once the door closes.\n- Return: `POST /return` assigns a door for the customer to put the parcel in. It\n  does NOT record the return. `POST /return/confirm` does.\n\nImplementing either as a single step records parcels as collected or returned that\nare still sitting in a locker, or still in the customer's hand. If your firmware\ncannot reach us for the second call, retry it — both are safe to repeat, and a repeat\non an already-finished operation returns `404 not_found` rather than doing it twice.\n\n**Do not distinguish pickup failures.** `POST /pickup` answers a wrong code, a code\nbelonging to another station, and an already-collected code with byte-identical `401`\nresponses, deliberately. Someone standing at a keypad must not be able to learn which\nof those happened, or the keypad becomes an oracle for guessing at other people's\nparcels. Show one neutral message for all of them. `429` and `5xx` are different —\nthose are our fault, not the customer's, and telling them their code is wrong when\nour service is down sends them home without their parcel.\n\n**Doors are vendor slots.** `position` is the manufacturer's own slot number and IS\nthe physical door. iBOX's internal compartment model is not exposed here and the two\nare allowed to disagree; trust the slot number.\n\n**Errors** follow RFC 9457. Branch on `code`, which is stable, never on `title`,\nwhich is prose and may be reworded. Every error carries a `correlation_id` worth\nquoting in a support request.\n\n**Rate limits** are per terminal, as a sliding window: 10/minute on `/pickup`,\n20/minute on `/return`. Over the limit you get `429` with a `Retry-After` header in\nseconds. Back off by that value rather than retrying immediately — a refused request\nstill counts against the window."},"servers":[{"url":"https://api.iboxdelivery.com/terminal/v1"}],"tags":[{"name":"Cabinet","description":"What the terminal shows on screen."},{"name":"Pickup","description":"A customer collecting a parcel."},{"name":"Courier","description":"A courier depositing parcels."},{"name":"Return","description":"A customer sending a parcel back."},{"name":"Health","description":"Proving the terminal is still there."},{"name":"Offline","description":"Finishing what the network interrupted."}],"paths":{"/cabinet":{"get":{"tags":["Cabinet"],"summary":"The doors of this terminal's own station","description":"Poll this to render the door grid. Occupancy is derived from what is actually in\neach door, so a door reported `available` here is one `POST /deposit` will\nwillingly assign — the two never disagree.\n\nThe response deliberately carries no surname, phone number, email or address: a\nkiosk screen is read by whoever is standing behind the customer.","responses":{"200":{"description":"The station and its doors.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"station":{"type":"object","properties":{"code":{"type":"string","description":"Station code, e.g. `PRN-01`."},"name":{"type":"string"}},"required":["code","name"],"additionalProperties":false},"doors":{"type":"array","items":{"type":"object","properties":{"position":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The vendor slot number. This IS the physical door."},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Our own label for the compartment, if we have one."},"size":{"anyOf":[{"type":"string","enum":["S","M","L","XL"]},{"type":"null"}]},"status":{"type":"string","enum":["available","occupied","out_of_service"],"description":"Derived from what is actually in the door, not from our inventory records. A door holding a delivery awaiting pickup OR a return awaiting collection is `occupied`."},"parcel":{"anyOf":[{"type":"object","properties":{"trackingNumber":{"type":"string"},"recipientFirstName":{"type":"string","description":"First name only. A kiosk screen is read by whoever is standing behind the customer, so no surname, phone, email or address is ever returned."},"kind":{"type":"string","enum":["delivery","return"],"description":"`delivery` is waiting for the person named on it; `return` is waiting for the merchant's courier. Different people come to open the same door."}},"required":["trackingNumber","recipientFirstName","kind"],"additionalProperties":false},{"type":"null"}]}},"required":["position","code","size","status","parcel"],"additionalProperties":false}}},"required":["station","doors"],"additionalProperties":false}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"This terminal's station could not be resolved. Contact support.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/pickup":{"post":{"tags":["Pickup"],"summary":"Verify a pickup code and get the door to open","description":"Does not transition the parcel — call `/door/confirm` after the door closes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":10,"description":"The pickup code the customer received by SMS. Digits, as typed on the keypad."}},"required":["code"],"additionalProperties":false}}}},"responses":{"200":{"description":"Open this door.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","const":true},"door":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Open this door."},"trackingNumber":{"type":"string"},"shipmentId":{"type":"string","description":"Pass this to POST /door/confirm once the door closes. Not shown on screen."}},"required":["ok","door","trackingNumber","shipmentId"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or `code` is missing or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The code is not valid for a parcel awaiting collection at this station. Identical for a wrong code, another station's code, and an already-collected code — do not attempt to tell them apart, and show one neutral message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many attempts from this terminal. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/door/confirm":{"post":{"tags":["Pickup"],"summary":"The customer closed the door","description":"`collected: true` marks the parcel collected and frees the door. `collected:\nfalse` records that they closed it without taking anything and changes nothing\nelse.\n\nRetry this if it fails. A second confirm for the same parcel returns `404`.\n\n**If the network dropped after the door opened**, queue this and send it when you\nare back, with `occurred_at` set to when the door actually closed. The parcel left\nwhether or not we heard about it, and the collection time decides whether it was\ninside its free-storage window.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"shipmentId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The `shipmentId` returned by POST /pickup."},"collected":{"type":"boolean","description":"True if the customer took the parcel. False if they closed the door without taking it — the parcel stays where it is and the door stays occupied."},"occurred_at":{"description":"ISO 8601 with offset. Send this only when reporting late — e.g. a completion queued while the terminal was offline. Omit it and iBOX records the time it received the request. Must not be in the future, or more than 7 days ago.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["shipmentId","collected"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or fields are missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No parcel awaiting collection with that id at this station — including the case where a previous confirm already succeeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/courier/verify":{"post":{"tags":["Courier"],"summary":"Check a courier's staff number","description":"A `200` with `ok: false` is the normal answer for an unknown or deactivated courier. It is not an error and carries no reason.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"courierNo":{"type":"string","minLength":1,"description":"The courier's staff number, e.g. `C10024`."}},"required":["courierNo"],"additionalProperties":false}}}},"responses":{"200":{"description":"Whether this courier may deposit here.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","description":"False for an unknown or deactivated courier. Not an error."},"courierName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Present only when `ok` is true."}},"required":["ok","courierName"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or `courierNo` is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/deposit":{"post":{"tags":["Courier"],"summary":"A courier deposits a parcel","description":"iBOX picks the door — the smallest free one the parcel fits — and generates the\ncustomer's pickup code. Do not pick a door yourself: concurrent deposits are\nserialized per station so two couriers are never sent to the same one.\n\nOne call, no confirm step: the parcel is in the courier's hand and goes in\nimmediately. A repeated call for the same parcel returns `404`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"courierNo":{"type":"string","minLength":1,"description":"The staff number of the courier making the deposit."},"trackingNumber":{"type":"string","minLength":1,"description":"The parcel's iBOX tracking number, e.g. `IBX5256667520`."},"occurred_at":{"description":"ISO 8601 with offset. Send this only when reporting late — e.g. a completion queued while the terminal was offline. Omit it and iBOX records the time it received the request. Must not be in the future, or more than 7 days ago.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["courierNo","trackingNumber"],"additionalProperties":false}}}},"responses":{"200":{"description":"Open this door and let the courier deposit.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","const":true},"door":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The door iBOX assigned. Open it."},"trackingNumber":{"type":"string"}},"required":["ok","door","trackingNumber"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or fields are missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`courier_not_verified` — unknown or deactivated courier.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — no parcel awaiting deposit with that tracking number at this station, including one already deposited by a previous call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`cabinet_full` — every door large enough for this parcel is occupied. `detail` names the size needed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/return":{"post":{"tags":["Return"],"summary":"A customer returns a parcel — assign a door","description":"Assigns and holds a door. Records nothing: the customer has not put anything in\nit yet. Call `/return/confirm` once the door closes.\n\nA customer whose first attempt met `cabinet_full` may simply try again later —\nthe started return is resumed rather than refused.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"trackingNumber":{"type":"string","minLength":1,"description":"The tracking number of the parcel the customer is bringing back."}},"required":["trackingNumber"],"additionalProperties":false}}}},"responses":{"200":{"description":"Open this door for the customer.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","const":true},"door":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The door iBOX assigned. Open it."},"trackingNumber":{"type":"string"},"returnId":{"type":"string","description":"Pass this to POST /return/confirm once the door closes."}},"required":["ok","door","trackingNumber","returnId"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or `trackingNumber` is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — no parcel eligible for return at this station.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`cabinet_full` — no door large enough is free.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many return attempts from this terminal. Honour `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/heartbeat":{"post":{"tags":["Health"],"summary":"Report that this terminal is alive","description":"Call this every 60 seconds, whether or not anyone is using the terminal. A\nterminal that has lost its network is indistinguishable from an idle one, so\nsilence is the only signal iBOX has: after three minutes without a heartbeat the\nterminal is marked offline and operations are alerted.\n\nThe body is optional — an empty POST is a valid heartbeat.\n\nKeep sending these while offline-degraded. The first one that gets through clears\nthe offline mark by itself; nothing has to be reset by hand.\n\nThe response carries this terminal's current configuration. Apply what you receive\nrather than caching it at install time — this is how a support number or idle\ntimeout is changed without visiting the site.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"appVersion":{"description":"The build running on this terminal, so a fault can be traced to a release.","type":"string","maxLength":64}},"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded, with your current configuration.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"station_code":{"type":"string","description":"The station iBOX believes this terminal stands at."},"support_phone":{"type":"string","description":"Show this on the help screen. May change; re-read it."},"idle_timeout_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Return to the idle screen after this long without a touch."},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Echo of what you sent, or null."}},"required":["station_code","support_phone","idle_timeout_seconds","app_version"],"additionalProperties":false}}}},"400":{"description":"Body was sent but is not valid JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/return/confirm":{"post":{"tags":["Return"],"summary":"The customer closed the door on a return","description":"`deposited: true` records the return, which is what notifies the merchant.\n`deposited: false` releases the door and leaves the return open, so a customer\nwho changed their mind can try again — offer them this, or they will either lie\nto the kiosk or walk away holding a door against everyone else.\n\nUntil this is called the door stays held, whichever way they decide.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"returnId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The `returnId` returned by POST /return."},"deposited":{"type":"boolean","description":"True if the parcel is physically inside the door. False if the customer changed their mind — the door is released and the return stays open so they may retry."},"occurred_at":{"description":"ISO 8601 with offset. Send this only when reporting late — e.g. a completion queued while the terminal was offline. Omit it and iBOX records the time it received the request. Must not be in the future, or more than 7 days ago.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},"required":["returnId","deposited"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"Body is not valid JSON, or fields are missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown, or revoked device token — or the terminal is not `online`. Every authentication failure returns an identical body: a caller cannot learn whether a token exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No return awaiting deposit with that id at this station — including the case where a previous confirm already succeeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"type":{"type":"string","description":"A URI identifying the error class."},"title":{"type":"string","description":"Human-readable prose. Do not branch on this; it may change."},"status":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"code":{"type":"string","description":"The stable machine-readable code. Branch on this."},"detail":{"type":"string"},"correlation_id":{"type":"string","description":"Quote this in a support request."}},"required":["type","title","status","code","correlation_id"],"additionalProperties":false,"description":"RFC 9457 problem details."}},"securitySchemes":{"deviceToken":{"type":"http","scheme":"bearer","description":"The terminal's own device token."}}},"security":[{"deviceToken":[]}]}