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: signing, and checking that you're talking to the building.
- A passage, step by step: a door, an elevator ride, and what to send.
- Refusals: what a "no" says, and when to ask again.
- The stream: every step and notice, pushed as it happens.
- Picking up after a restart.
- Check-ins and cancel, and timeouts.
- Notices: what to plan around.
- Emergencies and positions.
- Everything else: waiting in line, size and capability, riding alone, keep-out zones, booking ahead, directions and trip packages, chargers, asking for help, insurance, visitor robots, handoffs and your company page.
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:
X-Robot-Id: the robot ID.X-Timestamp: unix time now, in whole seconds.X-Signature: HMAC-SHA256, keyed with the robot's secret, over the method, a newline, the path with its query string, a newline, the timestamp, a newline, and then the exact body bytes. Send it as 64 hex digits, lowercase. Uppercase is accepted too.
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:
- The certificate's fingerprint:
sha256:and 64 hex digits. The owner finds it on the gateway's screen under Settings, with a copy button. This is the usual case, since the gateway has a certificate of its own. Pin it. - The site's certificate authority, if the site issues its own certificates. Trust that CA. The name you connect to must be in the certificate.
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
- Ask for the ride, naming both floors:
{"passage_id": "...", "resource_id": "lift", "from_floor": "1", "to_floor": "2"}. The answer isCAR_CALLED, andcarnames the car to board where the bank has more than one, such as"A". - Wait for
CAR_ARRIVED, on the stream or by polling the status. - Board, then report
{"event": "boarded"}. The state becomesDEST_SENT. - Wait for
ARRIVED. Get out, then report{"event": "exited"}. The state becomesRELEASED, withdone: 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
| State | What it means | What your robot does |
|---|---|---|
AWAITING_APPROVAL | A person approves each robot at this door | Wait, and show approval_code on its screen |
IN_LINE | Waiting its turn for the elevator | Wait clear of the lobby, at line.wait_at |
YOUR_TURN | Its turn in line has come | Come to the lobby within 90 seconds and report ready |
OPEN_COMMANDED | The door was told to open | Go through when the way is clear, then report passed |
CAR_CALLED | The car is called; car names it | Wait at the landing |
CAR_ARRIVED | The car is at its floor | Board when the doors are open and there's room, then report boarded |
DEST_SENT | The destination is sent | Ride |
ARRIVED | At the destination floor | Get out, then report exited |
PASSED, RELEASED | Done: done is true | Nothing more |
DENIED, FAILED, CANCELLED | Ended, not done | Read reason_code, retry and retry_after |
REQUESTED, AUTHORIZED, SENDING, BOARDED, EXITED, UNKNOWN_OUTCOME | Passing steps | Keep 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
| Field | What it is |
|---|---|
passage_id | Your ID for this trip: 1 to 128 letters, digits, ., - or _. A UUID is best |
resource_id | The door or elevator, by the ID GET /v1/places gives |
from_floor, to_floor | Every elevator ride names both, as the building's floor labels |
wait_in_line | Elevators only: true to wait in line rather than be refused (waiting in line) |
ride_alone | Elevators only: true to ask for a car to itself (riding alone) |
payload_class | What the trip carries, as a class only: none, clean, soiled, hazardous or controlled |
deadline | Unix time the trip must be done by, within the next day |
purpose | What it's bringing: meds, specimens, supplies, linen, meals, waste, parcel, equipment, maintenance or other |
from_area, to_area, for_area | Where 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_company | The company it's working for, by that company's ID in this building |
ref | Your own delivery number, up to 40 characters. Shown to a person who approves robots at a door; never stored |
work_order, carrying | Data 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:
reason: plain words, for people and logs.reason_code: a stable code for your software, such asclosed,outside_hours,fire_alarm,busy_can_waitornot_permitted.retry:trueif waiting can help;falseif it needs the owner or your company.nullwhere the gateway can't say.retry_after: seconds until waiting should help, where the gateway knows; otherwisenull.
{"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: each step of your robot's passages, shaped like a status, with the event'sidand its time,ts.event: notices: your robot's whole list of notices, at once when you connect and again whenever it changes. Replace your list with it each time.
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:
GET /v1/me: who this robot is here (name, model, maker, its group and the group's priority, your company, its role here and its access:since,untiland floors), whether the owner has suspended it (active), whether your company's insurance is in force (company.insurance), what the site approved of its cameras and radios (sensors),time, the gateway's clock, and on the sandbox,sandbox: true.GET /v1/passages: its passages not yet ended, as statuses, oldest first. Carry on with each one: report what the robot did, cancel a call it no longer needs, or follow it on the stream. Add?since=<unix time>for those that ended since then too: a day back at most, the newest 200, withcappedtrue when older ones were left out. Read it withsincefrom before the restart: a ride that ended while your robot was down, withaboardtrue, means it may still be in the car. Get out when the doors open, and reportexited; it's accepted late.GET /v1/visits: its bookings not cancelled and not over.
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 doing | Limit | Then |
|---|---|---|
Waiting for a person at a door (AWAITING_APPROVAL) | 2 minutes | DENIED, approval_timeout |
At an open door (OPEN_COMMANDED), until it reports passed | The passage timeout | FAILED, timeout |
Waiting for the car (CAR_CALLED) | The passage timeout | FAILED, timeout, and the call is released |
The car waiting at its floor (CAR_ARRIVED), until it reports boarded | The passage timeout | FAILED, timeout, and the call is released |
Aboard (DEST_SENT, then ARRIVED until it reports exited) | The passage timeout | FAILED, timeout, but the ride goes on, the building's people are told, and a late exited is still accepted |
In line (IN_LINE) | 20 minutes | DENIED, line_expired, retry_after 0 |
Its turn (YOUR_TURN), until it reports ready at the lobby | 90 seconds | DENIED, missed_turn, retry_after 0 |
| Silent in a car or doorway, after a check-in | 10 seconds, or half the passage timeout if that's shorter | The building's people are told it may be stuck |
| Confirming an emergency | deadline_s, 5 to 60 seconds by kind | The owner's screen shows it late, if its profile says it confirms |
| A visitor pass held for the front desk | 10 minutes | It lapses |
A request's X-Timestamp | 30 seconds either side of the gateway's clock | 401 |
| A kept-open connection, idle | 10 seconds | The gateway closes it |
| Online | 60 seconds after its last signed call | Shown offline |
| A position | 15 seconds | No longer live |
| Directions and their trip files | 30 minutes, or until the trip ends | Files 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.
kind | What it tells your robot |
|---|---|
closure | A door, elevator or floor is closed to it: resource_id, floors, reason, until |
fire alarm | The building's fire alarm is on. Every door and elevator holds it |
emergency | What to do, where to go and by when to confirm; then all clear (emergencies) |
paused | The owner has paused all robots |
no-robot window | One of an elevator's no-robot windows is in effect now |
out of service | A door or elevator is in fire recall or isn't answering |
robot car | One car of a bank is kept for robots now: robot calls go to it |
keep out | An area to keep out of: floor, name, x, z, w, d, until |
stop | It's lost: stop where it is. Nothing opens until its help request is closed |
no-stop | It has stopped too long where robots may only pass |
directions changed | Something now blocks the directions it was given: ask again |
company stopped, access starts, access ends | Your company's access here |
visitor pass, pass pending, no visitor pass | A visitor robot's pass |
booking conflict | A booking that won't get through as things stand, with reason_code |
handoff waiting | Goods left at a handoff spot for your company |
permit | A tenant's answer to a cage permit (data centers) |
charger power goes, charger power cut | The 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"}
emergency: what's happening. Fire alarm, evacuation, lockdown, make way, power loss or earthquake; a data center adds its own (Data centers).scope: where. The building, a floor, or an area of a floor, such as a route kept clear for a medical team.action: what to do, below.target: where to go, forgo to zone. A holding zone on its floor and in its own smoke compartment, so it never crosses a closing smoke door: its middle (x,z), itsbox, andentry, the way in from the corridor.deadline_sandconfirm_by: how long it has to confirm, and by when.also, on some kinds: what else this kind asks.never: what no robot does in any emergency.
action | What your robot does |
|---|---|
go to zone | Goes to target, then confirms arrived |
clear and hold | Finishes 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 |
hold | Stops where it is, to the side |
make way | Gets out of the area in scope, to the side |
stay put | Doesn'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:
{"action": "going", "eta_s": 40}as it sets off, then{"action": "arrived"}at its target;- or
{"action": "holding"}; - or
{"action": "cannot", "text": "battery too low to move"}.
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:
- Ask as usual, adding
"wait_in_line": true. If the robot can't go now, the answer isIN_LINE, withline: itsposition(1 is next), how many areahead,wait(away),wait_at,whyanduntil. - 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. - When it's your turn, the state becomes
YOUR_TURN(waitislobby). 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 fromCAR_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.
| Field | What it is | Values |
|---|---|---|
length_m, width_m, height_m | Its size as it travels | Meters, more than 0, up to 5 |
weight_kg | Its weight, empty | More than 0, up to 3,000 |
payload_kg | The most it carries | 0 to 3,000 |
turning_m | The narrowest corridor it turns in | Meters, up to 10 |
threshold_mm | The highest threshold it crosses | 0 to 200 |
slope_deg | The steepest slope it climbs | 0 to 30 |
speed_mps | Its top speed indoors | Meters a second, up to 5 |
doors | How it gets through a door | automatic (it needs them), pushes or arm |
elevators | How it uses an elevator | connected (it needs the elevator connected) or buttons (it presses them) |
finding | How it finds its way in a building | own_map, floor_plan, markers or escort |
cameras | Whether it carries cameras | true or false |
certifications | Such as ["UL 3300", "ISO 13482"] | Up to 8, each up to 40 characters |
by_hand | How to move it if it stops, shown to staff on every request for help | Up to 200 characters |
emergency | It confirms each emergency notice and goes where it's sent | true or false |
grounded, cameras_off_in_cages, containment_doors, radios, max_temp_c, clear_door_s | Data 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:
passages: the doors and elevator rides to ask for, in order, each withwait_at(where to stop and ask) andcomes_out(where it comes out). Here,liftfrom 1 to 2, thensecure-door.to.at: where the place is.avoid: the areas to keep out of on the way.words, for a robot that reads them: "From the Door, go north, then turn right into Corridor 1-1, then turn left into Elevator Bank Lobby, then ride the Elevator Bank from 1 to 2, ..."
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}:
route-<floor>.geojson: your trip's own way, as a graph in the GeoJSON that Nav2's Route Server reads, with doors, elevator landings, and each hallway's name, width and rules inmetadata.trip-<floor>.yamlandtrip-<floor>.png: a corridor-only Nav2 map, free along your way at the hallway's width and unknown everywhere else.
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"}
kind:blocked path,door won't open,elevator problem,lost,low battery,payload problem,faultorother.where:{"floor", "x", "z"}on the plate, or{"place": <a door or elevator ID>}. Left out, the gateway uses the position your robot last shared.text: a few words for whoever comes, up to 200 characters. Never a personal detail.urgency:normalorurgent.
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.
- Before putting the goods down:
POST /v1/handoffs {"spot": "<the spot's ID>", "for_company": "<their company ID>", "kind": "specimens"}.kindcomes from the same list aspurpose. 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. - Once they're down:
POST /v1/handoffs/{id}/left. The other company's robots get ahandoff waitingnotice. - 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.
- your robots, and where the gateway last saw each;
- today's trips and refusals, with the plain reason and the same
reason_codeyour software gets; - your bookings and their conflicts, and your notices;
- your access (role, dates, floors, and the doors and elevators each robot may use), and your go-live, with its measures and tests (going live);
- requests for runs from the building's staff, to accept or decline;
- how your robots look on the building's screen, and each model's spec sheet;
- your certificate of insurance.
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).