Plynth Sandbox

Connecting your robots

Each building that works with Plynth runs a small gateway on site. Your robot asks it to go through a door or ride an elevator. The gateway checks the building owner's rules, commands the door or elevator, and tells your robot each step. You connect once, the same way in every building. Your routes, tasks and navigation stay yours.

Your robots also take part in the building's life safety. In a fire alarm or another emergency, the gateway tells each robot what to do, where to go and by when to confirm. The building's own equipment never waits for the gateway or a robot: fire recall, fire doors and the way out do their own work.

Fail closed. If the gateway doesn't answer within your timeout, treat it as "no" and use your own fallback.

This page covers any building. A data center adds its own rules: see Data centers. Every call, field and answer is in the API reference. You can try all of it on the sandbox, at https://sandbox.plynth.build.

In this guide

Credentials and verification

Each robot has its own robot ID and secret. In a building, the building gives you one for each robot, or your company's people take each secret on your company's page there. On the sandbox, see keys.

Sign every call except GET /v1/health and GET /v1/openapi.yaml with three headers:

Signing requests has the exact recipe, two test vectors to check your code against, and the same code in shell, Python, JavaScript and C++.

A request more than 30 seconds off the gateway's clock is refused as a replay, so keep every robot on NTP. GET /v1/me answers with the gateway's clock, time, to check yours against. Errors explains each 401.

A body is one JSON object, sent with Content-Length (not chunked), and at most 64 KiB. A passage request is at most 4,096 bytes of JSON.

Keep each secret like a password: out of logs, command lines and shell history. New secrets start plynth_live_ in a building and plynth_sbx_ on the sandbox, so a secret scanner can spot one that leaked; older secrets without the prefix keep working. Sign with the whole string, prefix included.

To replace a secret on your company's page, Rotate issues a new one and keeps the old one working for 24 hours, so you can install the new one first. Revoke Old issues a new one and stops the old one at once, for a secret that may have leaked. When the building replaces a secret, the old one stops at once.

Keep each robot's connection open and reuse it (HTTP/1.1). A check-in every second or two at a car door then costs no new TLS handshake. The gateway closes a connection that has been idle for 10 seconds, so check that it's still open before you reuse it, and reconnect when it isn't.

The test kit's reference client, plynth.client, does all of this for you, in standard-library Python 3.9 or later. Get it from the sandbox:

import os
from plynth.client import RobotClient

robot = RobotClient(os.environ["GATEWAY_URL"], os.environ["GATEWAY_ROBOT_ID"], os.environ["GATEWAY_ROBOT_SECRET"])
print(robot.me())  # who this robot is here, and the gateway's clock

Verifying the gateway

In a building, the gateway runs on the building's own network, and your robots reach it there. It accepts no connections from the internet. Its answers aren't signed, so HTTPS is your robot's only proof that OPEN_COMMANDED or CAR_ARRIVED came from the building and not from someone else on the network. Verify the gateway's certificate on every connection, and never turn verification off.

Ask the building for the gateway's address (a host name or IP address, and the port) and one of these:

To pin, take the SHA-256 of the gateway's certificate (its DER bytes) right after the TLS handshake, and compare it with the fingerprint before you send anything. Close the connection on a mismatch. The fingerprint is the whole check: the certificate's issuer, name and dates aren't looked at. Colons and case don't matter, so the value openssl x509 -fingerprint -sha256 prints, with colons, works as it is.

With the kit's client, pass pin="sha256:..." to RobotClient, or cafile="site-ca.pem" for a site's CA, or context= an ssl.SSLContext of your own. The client refuses plain http:// anywhere but loopback.

What failure looks like. If the certificate doesn't match, the call fails before a byte of the request is sent. In the kit's client it's a urllib.error.URLError whose reason is an ssl.SSLCertVerificationError that reads "the gateway's certificate is sha256:..., not the pinned sha256:...". Treat it like no answer: use your fallback, don't retry with verification off, and call the building. Either it changed its certificate and you missed the new fingerprint, or something on the network is pretending to be the gateway.

When the building changes its certificate (a new gateway computer, a key that may have leaked, or the site's own policy), it makes the new one alongside the old, sends you the new fingerprint, and agrees a time to switch. Until then, pin both, separated by a comma: pin="sha256:<old>,sha256:<new>". After the switch, drop the old one. A pinned certificate doesn't stop working when its dates run out (the gateway's own last 825 days), so a change is always a planned one.

The hosted sandbox has a public certificate, so there's nothing to pin there. Verify it the normal way, against your computer's trusted authorities.

A passage, step by step

A passage is one robot through one door, or one elevator ride. You pick its passage_id, new for each trip. A UUID is best: IDs are shared across the whole gateway, and one that another robot has used gets 409. Sending exactly the same request again is safe: you get the current status, and no second command is sent. The same ID with different contents gets 409.

A door

POST /v1/passages
{"passage_id": "3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14", "resource_id": "door"}

The answer is the passage's status:

{"passage_id": "3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14", "resource_id": "door", "state": "OPEN_COMMANDED",
 "reason": null, "reason_code": null, "retry": null, "retry_after": null, "car": null, "done": false,
 "approval_code": null, "line": null, "alone": false, "arrived": null}

OPEN_COMMANDED means the door was told to open. Go through when your robot's own sensors say the way is clear, then report it:

POST /v1/passages/3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14/report
{"event": "passed"}

The state becomes PASSED, with done: true.

An elevator ride

  1. Ask for the ride, naming both floors: {"passage_id": "...", "resource_id": "lift", "from_floor": "1", "to_floor": "2"}. The answer is CAR_CALLED, and car names the car to board where the bank has more than one, such as "A".
  2. Wait for CAR_ARRIVED, on the stream or by polling the status.
  3. Board, then report {"event": "boarded"}. The state becomes DEST_SENT.
  4. Wait for ARRIVED. Get out, then report {"event": "exited"}. The state becomes RELEASED, with done: true.

Every ride names both floors, with the labels GET /v1/places gives ("1", "2"). A ride without them is refused floors_required, and one that names a floor the bank doesn't serve is refused unknown_floor. Neither helps to retry.

At the destination, the status's arrived names the floor, and map_id, your company's own name for its map of that floor, where the building recorded one. Load that map, so a look-alike floor never fools your robot.

States

StateWhat it meansWhat your robot does
AWAITING_APPROVALA person approves each robot at this doorWait, and show approval_code on its screen
IN_LINEWaiting its turn for the elevatorWait clear of the lobby, at line.wait_at
YOUR_TURNIts turn in line has comeCome to the lobby within 90 seconds and report ready
OPEN_COMMANDEDThe door was told to openGo through when the way is clear, then report passed
CAR_CALLEDThe car is called; car names itWait at the landing
CAR_ARRIVEDThe car is at its floorBoard when the doors are open and there's room, then report boarded
DEST_SENTThe destination is sentRide
ARRIVEDAt the destination floorGet out, then report exited
PASSED, RELEASEDDone: done is trueNothing more
DENIED, FAILED, CANCELLEDEnded, not doneRead reason_code, retry and retry_after
REQUESTED, AUTHORIZED, SENDING, BOARDED, EXITED, UNKNOWN_OUTCOMEPassing stepsKeep following

Treat a state you don't know as not final, and keep following the passage until your own timeout.

What only your robot can say

Only your robot's own report completes a passage. A command acknowledgement never counts. CAR_ARRIVED means the elevator system says the car is at your floor: your robot still checks with its own sensors that the doors are open and there's room before it boards.

If your robot never reports, the gateway times the passage out (see timeouts) and releases the car. Once your robot has boarded, nothing the gateway does cancels its ride. If the passage times out while your robot is aboard, it ends FAILED, but the ride goes on, the building's people are told to check on your robot, and your late exited is still accepted.

Once the car has come, an owner's stop doesn't strand your robot either: it boards, rides and gets out. Of the building's rules, only the fire alarm turns a robot back at the car door. The elevator's own fire recall can end any ride, even with your robot aboard (fire_recall): get out when the doors open, and report exited.

If a person helped your robot through (held a door, walked it out of a car), also report {"event": "assisted"}. The building counts it as an intervention.

What to send

FieldWhat it is
passage_idYour ID for this trip: 1 to 128 letters, digits, ., - or _. A UUID is best
resource_idThe door or elevator, by the ID GET /v1/places gives
from_floor, to_floorEvery elevator ride names both, as the building's floor labels
wait_in_lineElevators only: true to wait in line rather than be refused (waiting in line)
ride_aloneElevators only: true to ask for a car to itself (riding alone)
payload_classWhat the trip carries, as a class only: none, clean, soiled, hazardous or controlled
deadlineUnix time the trip must be done by, within the next day
purposeWhat it's bringing: meds, specimens, supplies, linen, meals, waste, parcel, equipment, maintenance or other
from_area, to_area, for_areaWhere it comes from, where it's going, and the area that asked for it, each one of the building's named areas (areas in GET /v1/places)
for_companyThe company it's working for, by that company's ID in this building
refYour own delivery number, up to 40 characters. Shown to a person who approves robots at a door; never stored
work_order, carryingData centers: see Data centers

Use the building's IDs for doors and elevators and its labels for floors. Anything you send is kept in the building's record for its retention period, so never send personal details: no patients', residents' or customers' names, no room numbers, no order contents. ref is the one exception the gateway never stores.

Payload class and deadline. The building may keep a payload class off a door or elevator: soiled carts off a clean supply car, say, or hazardous loads off the visitor elevators. A trip that says it carries one is refused there, payload_refused, and retrying doesn't help: ask for another way. A trip that says nothing isn't refused. In an elevator line, a trip with a deadline goes ahead of others of the same priority without one, the soonest first. A deadline never jumps a priority.

Doors where a person lets robots through

Some doors, such as a locked unit's, need a person's approval for each robot. Your passage then waits in AWAITING_APPROVAL for up to two minutes, and its status carries approval_code. Show those four digits on your robot's screen: the person approving checks them.

Help them decide: send purpose, to_area and from_area with the request, and ref, your own delivery number, which they see and the gateway never stores. If they decline, or nobody answers in time, the passage ends DENIED, with approval_declined or approval_timeout. You may cancel while it waits.

Refusals

A refusal is not an HTTP error. It's a normal answer, HTTP 200, with state DENIED (or FAILED, if something stops the passage later), and four fields:

{"passage_id": "3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14", "resource_id": "lift", "state": "DENIED",
 "reason": "closed: Car being serviced", "reason_code": "closed", "retry": true, "retry_after": 1200,
 "car": null, "done": false, "approval_code": null, "line": null, "alone": false, "arrived": null}

The gateway knows retry_after for these: outside_hours, until a rule's next window opens; no_robot_window, until the window ends; closed, until the closure's set end (null when no end is set); access_not_started, until your company's access starts; and elevator_budget, until the elevator's hourly robot limit has room again. After missed_turn or line_expired it's 0: ask again now. Otherwise it's null. The gateway doesn't guess, so back off as you see fit.

To ask again, send a new request with a new passage_id: the same request again returns the same answer. Act on reason_code, never on the words in reason. New codes may be added, and existing ones never change meaning. Treat a code you don't know as a refusal, and a state you don't know as not final. Every code is in Errors and reason codes.

Your robot can know before it asks: GET /v1/places says, for each door and elevator it may use, whether it's open to it now (open_now), and if not, why (why_not, reason_code).

The stream

GET /v1/stream keeps a connection open and pushes server-sent events:

event: passage
id: 4211
data: {"id": 4211, "ts": 1792562561.1, "passage_id": "3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14", "resource_id": "lift", "state": "CAR_ARRIVED", "reason": null, "reason_code": null, "retry": null, "retry_after": null, "car": "A", "done": false, "approval_code": null}

: keep-alive

A keep-alive comment comes every 15 seconds, so 30 seconds of silence means the connection is gone. Every event carries an id:. When you reconnect, send the standard Last-Event-ID header (or ?after=<id>) with the last one you saw. The steps of your robot's passages that you missed come first, then the stream goes live. That replays at most the last 500 steps of the last day; after a longer gap, read GET /v1/passages. Without either, only steps after you connect are sent.

A robot may hold two streams at once. A third closes the oldest, so a reconnect always gets through, even if an old connection hasn't died yet. The gateway ends a stream within a second when the robot's secret is replaced, the robot is suspended or its company is stopped; reconnecting then gets 401 or 403. Don't reconnect in a tight loop on either.

Polling a passage's status about once a second works too, and the status stays the source of truth.

Picking up after a restart

When your robot or fleet manager restarts, three reads put it back where it was:

Check-ins and cancel

Check-ins

In a car or a doorway, check in every second or two: POST /v1/passages/{id}/heartbeat. The answer is the passage's status. Once your robot has checked in for a passage, if it goes quiet there for 10 seconds, the building's people are told it may be stuck. Its next word about the passage (a check-in, a status poll or a report) clears that. A robot that never checks in is never flagged.

At the car door, boarding or getting out, each check-in also asks the elevator to keep its doors open 5 seconds longer, where the elevator can (KONE's door hold is one). Keep checking in until your robot is through, then report boarded or exited, which lets the doors close. There's no door hold during the fire alarm.

Cancel

Plans changed before boarding? POST /v1/passages/{id}/cancel, with no body. A waiting car call is released at once, so no car waits for nobody, and the passage ends CANCELLED (reason_code cancelled). Cancelling twice is harmless, and cancelling a passage that has already ended returns its status. Once aboard, your robot can't cancel (409): it gets out and reports exited.

Timeouts

Each step of a passage has its own limit, counted from when that step began. The building sets its passage timeout; it's 60 seconds unless the building sets another, and 60 seconds on the sandbox.

What your robot is doingLimitThen
Waiting for a person at a door (AWAITING_APPROVAL)2 minutesDENIED, approval_timeout
At an open door (OPEN_COMMANDED), until it reports passedThe passage timeoutFAILED, timeout
Waiting for the car (CAR_CALLED)The passage timeoutFAILED, timeout, and the call is released
The car waiting at its floor (CAR_ARRIVED), until it reports boardedThe passage timeoutFAILED, timeout, and the call is released
Aboard (DEST_SENT, then ARRIVED until it reports exited)The passage timeoutFAILED, timeout, but the ride goes on, the building's people are told, and a late exited is still accepted
In line (IN_LINE)20 minutesDENIED, line_expired, retry_after 0
Its turn (YOUR_TURN), until it reports ready at the lobby90 secondsDENIED, missed_turn, retry_after 0
Silent in a car or doorway, after a check-in10 seconds, or half the passage timeout if that's shorterThe building's people are told it may be stuck
Confirming an emergencydeadline_s, 5 to 60 seconds by kindThe owner's screen shows it late, if its profile says it confirms
A visitor pass held for the front desk10 minutesIt lapses
A request's X-Timestamp30 seconds either side of the gateway's clock401
A kept-open connection, idle10 secondsThe gateway closes it
Online60 seconds after its last signed callShown offline
A position15 secondsNo longer live
Directions and their trip files30 minutes, or until the trip endsFiles answer 409: ask for directions again

After a gateway restart, a passage left part-way gets a fresh timeout. If it then times out, the reason says the gateway restarted (gateway_restarted). A command that may or may not have reached a device when the gateway stopped is never sent again: the passage ends FAILED, unknown_outcome, unless the device confirms it.

Notices

GET /v1/notices, or the stream, says what to plan around now. Read it before planning a route.

kindWhat it tells your robot
closureA door, elevator or floor is closed to it: resource_id, floors, reason, until
fire alarmThe building's fire alarm is on. Every door and elevator holds it
emergencyWhat to do, where to go and by when to confirm; then all clear (emergencies)
pausedThe owner has paused all robots
no-robot windowOne of an elevator's no-robot windows is in effect now
out of serviceA door or elevator is in fire recall or isn't answering
robot carOne car of a bank is kept for robots now: robot calls go to it
keep outAn area to keep out of: floor, name, x, z, w, d, until
stopIt's lost: stop where it is. Nothing opens until its help request is closed
no-stopIt has stopped too long where robots may only pass
directions changedSomething now blocks the directions it was given: ask again
company stopped, access starts, access endsYour company's access here
visitor pass, pass pending, no visitor passA visitor robot's pass
booking conflictA booking that won't get through as things stand, with reason_code
handoff waitingGoods left at a handoff spot for your company
permitA tenant's answer to a cage permit (data centers)
charger power goes, charger power cutThe charger it's on is about to lose power, or has (data centers)

New kinds may be added: ignore one you don't know.

GET /v1/closures lists just the closures that apply to your robot: which doors, elevators and floors, why, and until when. A closure that spares your robot isn't listed, and none says who else it spares.

GET /v1/places says which doors and elevators your robot may use, on which floors, who lets it through (check: automatic, badge, human, badge + human or badge or human), the days and hours of the rules that cover it, its no-robot windows and closures, and whether each is open to it now, and if not, why. It also gives each place's size and whether your robot fits, and, on the floors your robot may reach, the building's named areas, holding zones, keep-out zones, no-stop areas, chargers and handoff spots. Read it when your robot starts its shift, and use the IDs it gives.

The fire alarm. While the building's fire alarm is on, every robot is held at every door and elevator (fire_alarm, with retry true), calls not yet boarded end FAILED with fire_alarm, and every robot has a fire alarm notice beside its emergency notice. Stay clear of doors and elevators until it clears.

Emergencies and positions

In an emergency, your robot gets one emergency notice, on the stream and in GET /v1/notices:

{"kind": "emergency", "id": 12, "emergency": "fire alarm", "scope": {"building": true}, "action": "go to zone",
 "target": {"name": "Robot Holding", "floor": "1", "x": 0.125, "z": 0.175, "box": [0.05, 0.1, 0.15, 0.15],
            "capacity": 4, "compartment": null, "entry": [], "kind": "holding"},
 "deadline_s": 30, "confirm_by": 1792562591.0,
 "never": ["press or block a hold button, EPO station or manual release", "hold any door during an alarm",
           "use an elevator after recall", "treat a door unlocked for egress as permission"],
 "answer": null, "eta_s": null, "reason": "fire alarm", "reason_code": "emergency"}
actionWhat your robot does
go to zoneGoes to target, then confirms arrived
clear and holdFinishes clearing any doorway, car or area it's in, then stops to the side. Sent where the floor has no holding zone, or where the gateway can't place your robot
holdStops where it is, to the side
make wayGets out of the area in scope, to the side
stay putDoesn't move (a lockdown, an earthquake)

Who is told

A building-wide emergency goes to every robot that's online (heard from in the last minute) and every robot the gateway can place now. One for a floor or an area goes to each robot placed there now, and to each online robot the gateway can't place now whose last known floor is that floor, or unknown. The gateway can place a robot that shared a position in the last 15 seconds.

A robot the gateway can't place is told clear and hold (or make way, where the emergency makes way) rather than sent to a zone. It's sent to a zone as soon as it shares a position. A robot that comes online during an emergency is told then.

Confirming

Answer each notice with POST /v1/emergency/{id}/ack:

eta_s is whole seconds, 0 to 3,600, and text is up to 200 characters. confirm_by is for the first answer, not for arriving: your software should answer within a second or two. The answer comes back as the notice, with your answer in it. An ack for an emergency your robot wasn't told of is 404; for one that's over, 409.

If your robot rides out of a car onto another floor, its notice changes to that floor's zone. When the emergency is over, the notice becomes all clear, still kind emergency with the same id, with cleared (the kind that ended, such as fire alarm) and action ask again, for two minutes. Nothing resumes on its own: ask again for each door and ride, and the rules decide afresh.

Say that your robot confirms: send "emergency": true in its profile. Then a robot that hasn't confirmed by confirm_by, or says cannot, goes to the top of the owner's screen with where it was last seen. That's how the building knows your robots take part in its response. A robot that hasn't built this still gets the notice and is held at every door and elevator; the owner's screen says it doesn't confirm.

While an emergency covers a door or ride, a request there is refused with reason_code emergency (retry once it's over). Nothing your robot does or fails to do can delay fire recall, fire doors or the way out. The equipment does its own work, and the gateway only tells robots. GET /v1/places lists the holding zones on your robot's floors (holding_zones), to plan with.

Positions

POST /v1/position {"floor": "1", "x": 0.42, "z": 0.61} shows your robot live on the owner's screen, and lets the building send it to the right holding zone in an emergency. Positions are kept in memory only and never logged. A position is live for 15 seconds. A robot that doesn't send positions is shown at the last door or elevator the gateway saw it use.

Send a position every 1 to 5 seconds while your robot is moving, and again when it stops. Without a live one, the building can't send your robot to a zone in an emergency, and it's told to clear and hold instead.

The floor plate. x and z run from 0 to 1 across the floor plate, as the building's plan is drawn: x from the plate's west edge (0) to its east edge (1), and z from its north edge (0) to its south edge (1). North and east are the plan's, the same ones the words in directions use. A position off the floor's plate is refused (400).

Where a building's plans are to scale, the plate has a size in meters, which GET /v1/places gives as plate_m: [width, depth], or null where they aren't. The sandbox's is [40, 30]: 40 m from west to east and 30 m from north to south. Trip packages use meters in the building's frame: x east and y north from the plate's south-west corner. A point on the plate is x times the plate's width east of that corner, and 1 minus z, times its depth, north of it. On the sandbox, the door at x 0.2 and z 0.6 is 8 m east and 12 m north of the corner.

In your own map. If your fleet navigates in its own map, in meters, send {"floor": "1", "map_x": 12.3, "map_y": 4.5} instead (map_position() in the kit's client). The building places your map on its floor plate once, from two spots you both know (a doorway, a column), for one floor or for every floor your map covers alike, and the gateway turns each position into the plate's x and z. Your map should be right-handed with z up, as in VDA 5050 and ROS. Until a frame is set for your company, map positions get 400.

A position may also carry confidence, from 0 (no idea) to 1 (sure): see trip packages.

Online, battery and software

Every signed call tells the building your robot is there. It shows Online on the owner's screen for 60 seconds after any call, then Offline, with when it was last heard. Between other calls, send POST /v1/heartbeat about every 30 seconds, with the battery and what the robot is doing, if you have them:

{"battery": 0.62, "state": "working", "software": "2.4.0"}

battery runs from 0 to 1, and state is idle, working, charging or error. Each is optional, and the last one sent stands. Like positions, they're kept in memory only.

Send the robot's software version, software (up to 40 characters), too. Unlike the rest, it's kept and logged, and when it changes, the owner is asked to rerun your robot's conformance test (going live).

Software that only reads on a robot's behalf, such as a dashboard or monitoring, should use the robot's read-only key, from Read-Only Key on your company's page. It starts plynth_read_, and you sign with it as the robot, exactly as with its secret. It makes GET calls only (anything else is 403, read_only_key), and its calls don't count as the robot being online. Software that has to use the robot's own secret sends the header X-Robot-Observer: 1 instead, so its calls don't count either; in the kit's client, that's observer=True.

Waiting in line for an elevator

When an elevator is out of service or busy, robots that keep retrying all rush it when it's back. Ask with "wait_in_line": true instead, and the gateway keeps your robot's place:

  1. Ask as usual, adding "wait_in_line": true. If the robot can't go now, the answer is IN_LINE, with line: its position (1 is next), how many are ahead, wait (away), wait_at, why and until.
  2. Wait clear of the elevator lobby and of doorways: at wait_at, the building's holding spot for that elevator, where it names one. The lobby is for people, and for the robots whose turn it is.
  3. When it's your turn, the state becomes YOUR_TURN (wait is lobby). Come to the lobby within 90 seconds and report {"event": "ready"}. The car is called then, never before, so no car waits for a robot that isn't there. The ride goes on from CAR_CALLED, as above.

The line goes by the building's priority for your robot's group (urgent, rush, normal or later), then by who asked first. Within a priority, a trip with a deadline goes ahead of one without, the soonest first. Urgent robots wait only for an elevator that's out of service. At most two robots are at a lobby at once.

A turn not taken in 90 seconds is lost (missed_turn), and a place lasts 20 minutes (line_expired): ask again. Anything that refuses the ride (the fire alarm, the elevator's fire recall, an owner's stop or a closure) ends a place in line at once, with that reason. Reporting ready before your turn is 409. Without wait_in_line, a robot is refused line_waiting while robots of its priority or higher are waiting, so it can't cut in.

Size and capability

Tell the building how big and heavy your robot is, once when it connects and again if anything changes: POST /v1/profile, describing the robot as it travels, with any cart it tows. GET /v1/profile reads back what the building goes by: what you sent, and for the rest, its model's spec sheet (below).

{"length_m": 1.2, "width_m": 0.6, "height_m": 1.3, "weight_kg": 120, "payload_kg": 200,
 "threshold_mm": 12, "slope_deg": 7, "speed_mps": 1.0, "doors": "automatic", "elevators": "connected",
 "cameras": true, "finding": "floor_plan", "by_hand": "Release the brake under the rear panel, then push",
 "emergency": true}

Each POST replaces the whole profile. Send every field you know each time: a field you leave out, or send as null, is unknown, unless its model's confirmed spec sheet gives it (below); unknown never refuses. A field the gateway doesn't know is 400.

FieldWhat it isValues
length_m, width_m, height_mIts size as it travelsMeters, more than 0, up to 5
weight_kgIts weight, emptyMore than 0, up to 3,000
payload_kgThe most it carries0 to 3,000
turning_mThe narrowest corridor it turns inMeters, up to 10
threshold_mmThe highest threshold it crosses0 to 200
slope_degThe steepest slope it climbs0 to 30
speed_mpsIts top speed indoorsMeters a second, up to 5
doorsHow it gets through a doorautomatic (it needs them), pushes or arm
elevatorsHow it uses an elevatorconnected (it needs the elevator connected) or buttons (it presses them)
findingHow it finds its way in a buildingown_map, floor_plan, markers or escort
camerasWhether it carries camerastrue or false
certificationsSuch as ["UL 3300", "ISO 13482"]Up to 8, each up to 40 characters
by_handHow to move it if it stops, shown to staff on every request for helpUp to 200 characters
emergencyIt confirms each emergency notice and goes where it's senttrue or false
grounded, cameras_off_in_cages, containment_doors, radios, max_temp_c, clear_door_sData centers: see Data centers

Where the building has measured a door (its clear width) or an elevator (the car's inside width and depth, its door's width and its rated load), GET /v1/places gives each place's size and says whether your robot fits. fits is true, false, or null when either side hasn't said, with a plain fit_note, such as "wider than the door's 0.86 m clear opening". A request your robot is known not to fit is refused too_big, and a ride over the car's rated load, with its weight and payload, over_rated_load. Where the building gives a door's or a car sill's threshold, or the slope of a door's approach, a robot that can't manage it is refused cant_cross. None of these helps to retry.

On the sandbox, the doors have a 0.86 m clear opening, and the cars are 1.6 m wide and 1.4 m deep inside, with a 1.07 m door and a 1,150 kg rated load.

How the building's screen draws your robots (each model's form and size, your color and your logo) is set on your company's page, and changes nothing the gateway checks. Which model each robot is, set there too under Which Model Each Robot Is, does: a robot goes by its model's spec sheet.

Spec sheets. On your company's page, file each model's spec sheet (a PDF). Its values are read for you, each beside the line it came from; you check them and confirm. From then on the building goes by that model's sheet for each of its robots wherever the robot hasn't said for itself: a field your robot sends in POST /v1/profile wins, and a field it leaves out comes from its model's confirmed sheet. What a robot's software does (emergency, cameras_off_in_cages) never comes from a sheet: only the robot says it. GET /v1/profile reads back what the building goes by, both together. A new sheet for a model waits for you to confirm it; until then the building goes by the one before. The owner's people can open each sheet on file. The building's admins can also set how your robots look, and file and confirm a sheet for you, from their view of your page; the building's record names who made each change.

Riding alone

Ask for a car to your robot alone with "ride_alone": true on an elevator ride. The building decides per elevator: never (the default), allowed, or for its urgent groups only, and the elevator's connection must be able to do it. GET /v1/places says, per elevator, whether your robot may (ride_alone). If not, the ride is refused ride_alone_unavailable, and you can ask again without it. If granted, the status says alone: true.

It's the elevator's own dispatcher that keeps the car to your robot: the gateway holds no car for it, and fire recall ends the ride like any other. Today only simulated elevators do it, such as the sandbox's lift. On KONE it isn't verified yet, so it's refused there.

Keep-out zones

The building can mark areas robots must keep out of: a nursery, a wet floor, a corridor being worked on. Each comes in your notices as keep out, with its floor, name, its rectangle on the plate (x, z, w, d) and until, so the stream tells you when one is marked or removed. GET /v1/places lists them by floor under keep_out. Plan routes around them.

Zones are advisory: no door or elevator is held for one, so they never stand in the way of getting out of the building. If a position your robot shares is inside a zone, the building's people are told.

Booking ahead

Book a trip the way a front desk pre-registers a visitor:

POST /v1/visits
{"since": 1792573200, "until": 1792580400, "floors": ["2"], "purpose": "parcel", "note": "Supplies for floor 2"}

The window is when your robot expects to arrive: up to 12 hours, starting within the next 14 days. A booking lets nothing through. On the day, your robot still needs every rule, and a pass if it's a visitor. What would stop it is checked at once and comes back as conflicts, so you hear before the robot leaves. If the building changes a rule later, each conflict arrives in your notices as booking conflict.

Your robot's first request in the window, or up to 15 minutes before it, records its arrival. GET /v1/visits lists your robot's bookings, and POST /v1/visits/{id}/cancel cancels one you no longer need. A robot holds at most 20 open bookings.

Directions and trip packages

Your robot needn't have the building mapped before it arrives. GET /v1/directions?to=<place> answers with what your robot's planner can't know about the building. to is a door, elevator or handoff spot's ID from GET /v1/places, or a zone's name, such as Robot Holding. Add floor= where the place is on several floors, and from=, with a door's ID or floor,x,z, if your robot hasn't shared its position.

On the sandbox, GET /v1/directions?to=secure-door&from=door answers with:

Each place comes on the floor plate (x, z), and as map, the same in your own map's meters, where your company has registered a frame for that floor. Your robot plans its own path between them: there's no corridor line to follow. The way goes only through doors and elevators your robot may use now, around keep-out zones and any way kept clear in an emergency, on floors your company may reach. Directions open nothing: ask for each passage as usual.

If there's no way, the answer is refused, with reason and reason_code (no_route when every way is closed). If something blocks the way later, your notices say directions changed: ask again. Say how your robot finds its way with finding in its profile.

Trip packages

Where the building's plans are to scale, directions also carry a package: per floor of the way, in order, where that stage starts and three files to fetch with GET /v1/directions/{id}/floors/{floor}/{file}:

Each stage's start is in meters in the building's frame (x east and y north from the plate's south-west corner, and yaw, anticlockwise from east), in words ("The Door, facing north"), and as map, the same in your own map, where your company's map of that floor is placed. The first floor's files are yours at once. Each later floor's come when your elevator ride arrives there; before that, they answer 409. When the trip ends (through its door, at its end, with new directions, or after 30 minutes), its files answer 409: ask again. The building keeps which floors it gave you, and when they expired.

The files are as designed: a reference, not for localization. Your robot always localizes with its own sensors. Start AMCL, or your own, from each stage's start: the elevator's landing, on each later floor. When a ride arrives at its floor, its status's arrived gives the building floor and your own map_id for it, where the building recorded one.

Say how sure it is. A position may carry confidence, from 0 (no idea) to 1 (sure): turn AMCL's covariance, or your own score, into it. Below the owner's threshold (0.5 unless they set another), or at a position outside your trip's corridor (its hallway's half-width plus a meter), your robot is lost. The building opens a lost help request, which tells its people; your notices say stop; and nothing opens for your robot (reason_code lost) until a person, or your robot, closes the request (POST /v1/help/{id}/resolved). A visiting robot on a trip may use only the doors and rides on its route (not_on_route): ask for new directions to go somewhere else.

ROS 2 files

GET /v1/ros/floors says which floors your robot may reach, and whether the owner gives your company whole floors. That's the owner's choice: for its own fleet, or for a robot company working there regularly. A visitor gets trip packages.

For every robot, GET /v1/ros/{floor}/keepout-{floor}.yaml and .png are a Nav2 Keepout Filter mask of the keep-out zones in force and the doors your robot may not use. The YAML's comment carries its version, new on every change. keepout-filter.yaml is its costmap filter info. With whole floors, there's also floor-{floor}.yaml and .png (walls occupied, door openings cut through) and route-{floor}.geojson; without, those answer 403. The kit's client fetches a file's bytes with fetch(path), signed like any call.

Chargers

GET /v1/places lists the charging rooms on your robot's floors under chargers: each one's name, floor and chargers, how many robots the building lets charge at once (slots), and when (hours, or null for always). Ask for a slot like a booking: POST /v1/charging {"zone": "<the charging room's name>", "minutes": 30}, or leave out zone for the first with a slot free. minutes is 5 to 240, and 30 if you leave it out.

Granted, the slot is your robot's until until, or until it says POST /v1/charging/{id}/done. A robot holds one at a time. Refused, the answer says why, and retry_after says when a slot frees. The gateway keeps the count; it never switches a charger. The sandbox has no chargers.

Asking for help

When your robot is stuck, blocked or lost, and a person should come:

POST /v1/help
{"kind": "door won't open", "where": {"place": "secure-door"}, "text": "badge refused twice", "urgency": "normal"}

It goes to the staff for that floor or door and to the owner's screen, with where in words, your company's support contact, and how to move your robot by hand (by_hand in its profile). Your robot hears each step in GET /v1/messages: that the building's people were told, then who handled it. If it sorts itself out, say so: POST /v1/help/{id}/resolved. Either way, if your robot was mid-passage, that passage counts as assisted.

The gateway also notices a robot that hasn't moved for 5 minutes in a corridor, outside any zone and off a passage, and tells the building's people, since it may be in someone's way out.

Insurance on file

Many buildings want a certificate of insurance from your company, as from any contractor. The owner sets minimums by role, stricter for visitors and contractors than for its own fleet. Your people upload the certificate on your company's page (the insurer, each policy's limit, whether the owner is named as additional insured, its dates, and the certificate itself), and the owner's people check it. Your robots send nothing.

Without a certificate in force that meets the minimums, every request is refused insurance_missing, or insurance_expired once it lapses, with retry false: send the renewed certificate, not another request. You're told 30 days before it ends, on your company's page. GET /v1/me says, under company.insurance, whether it's required, whether it's valid, and until when.

Visitor robots

If the building sets your company up as a visitor, ask for a pass before each trip:

POST /v1/passes
{"floors": ["1", "2"], "note": "Parcel for floor 2"}

Depending on the building, the pass is given at once, limited to the lobby, or held for the front desk (status pending; follow your notices). A pass held for the front desk lapses if no one answers in 10 minutes. Without a pass, requests are refused no_visitor_pass, and outside its floors outside_pass.

This is about the building's visitor role. The sandbox's visitor keys are something else: test keys for anyone (keys).

Handoffs

Some buildings name handoff spots, such as a shelf at the dock, where one company's robot leaves goods for another company's robots to collect. GET /v1/places lists the ones your robot may use (handoff_spots). Like a door, each needs the owner's permission, and a visitor robot needs a pass that covers its floor.

  1. Before putting the goods down: POST /v1/handoffs {"spot": "<the spot's ID>", "for_company": "<their company ID>", "kind": "specimens"}. kind comes from the same list as purpose. That's all a handoff says: never what's in it, who it's for, or an order number. Any other field is refused (400), and nothing of it is stored.
  2. Once they're down: POST /v1/handoffs/{id}/left. The other company's robots get a handoff waiting notice.
  3. A robot of that company, with permission for the spot, picks them up and says POST /v1/handoffs/{id}/collected. Only that company's robots can collect it.

GET /v1/handoffs lists those waiting for your company, and those your robot left in the last day. The building keeps robot IDs, companies, the spot, the kind and the times only. If nothing collects a handoff within the owner's limit (30 minutes unless the building sets another), a person is told. The sandbox has no handoff spots.

Your company page

Your people can see how your robots are doing in a building without asking the owner. The building's admin adds each of them with the role Robot Company and sends them a sign-in link, which works once, for 7 days; they choose their own password. Signed in, they see one page, for your company only, in parts: a tile for each says what's in it, and one part is open at a time.

Nothing of any other company is there, and nothing of the building beyond the places your robots may use. From the page, your people can also replace a robot's secret themselves (Rotate keeps the old one working for 24 hours; Revoke Old stops it at once) and take a read-only key for a dashboard. Each new key is shown once, and the building's record names who made it. The same sign-in works in each of the owner's buildings that connects your company. On the sandbox, it's how you get your keys (keys).