Plynth Sandbox

Errors and reason codes

A robot hears "no" in two ways. An HTTP error means the request couldn't be answered as sent: a signing problem, a bad body, too many requests. A refusal is a normal answer, HTTP 200, that says the building won't let your robot through now, and why. Handle them separately: fix an HTTP error in your software, and act on a refusal's code.

The error body

Every HTTP error has a JSON body:

{"code": "signature_mismatch", "error": "signature mismatch", "time": 1792562594.0, "request_id": "ece7c2db7072b481"}

New codes may be added. Handle one you don't know by its status.

Status codes

StatuscodeWhenWhat to do
400bad_requestBad JSON, a body that isn't a JSON object, a missing or malformed field, or a passage request over 4,096 bytes of JSON. error starts "bad request:" and says what's wrongFix the request: sent again unchanged, it gets the same answer
401unsigned, signature_mismatch, timestamp_skewThe request isn't signed, isn't signed right, or its timestamp is off (below)Fix the signing, or the clock
403not_for_this_keyA sandbox visitor key tried something other than its trips and its heartbeatUse your company's own keys
403read_only_keyA read-only key made a call other than GETUse the robot's secret for that call
403forbiddenThe stream, for a suspended robot or a stopped company; or a ROS 2 file the owner doesn't share with your company, with reason_code trip_onlyAsk the building; don't reconnect in a loop
404not_foundAn unknown path; a passage, booking, charger slot, help request, handoff, emergency or trip file that doesn't exist or isn't your robot's (the same answer for both, on purpose); or a sandbox-only call in a buildingCheck the path and the ID
405method_not_allowedPUT, PATCH or DELETE, or HEAD on anything but the public pages. The Allow header says what's acceptedUse GET or POST
409conflictA passage ID used before with other contents or by another robot; a report out of order; a cancel once aboard; ready before your turn; a trip file not released yet, or expired; an emergency that's over; a handoff already collected; a booking past the 20 a robot may hold openRead error: the same call won't succeed as it is
411length_requiredA chunked body. The connection closesSend the body with Content-Length
413too_largeA body over 64 KiB (the connection closes), or over 2 KB from a visitor keySend less
429rate_limitedPast a limit (below)Wait Retry-After seconds
503unavailableSomething unexpected on the gatewayTreat it as no; ask again later with the same passage_id
502, 503, 504noneNot the gateway's answer: something in between, such as a proxy or the site's network, answering for a gateway it couldn't reach. The body isn't the gateway's JSON and has no request_idTreat it as no answer, as a timeout (below)

Asking for a door or elevator that doesn't exist isn't a 404: it's a refusal, unknown_resource.

When you get 401

codeerrorWhat it meansWhat to do
unsignedsend X-Robot-Id, X-Timestamp and X-SignatureOne of the three headers is missing or emptySend all three on every call but health and the API spec
signature_mismatchsignature mismatchThe secret is wrong or was replaced, the robot ID isn't known here, or what you signed isn't what arrived. The answer is the same for each, on purpose, so it never says whether a robot ID existsCheck the robot ID and the secret, then work through Signing requests
timestamp_skewtimestamp outside ±30 s of gateway timeThe signature is right, since the gateway checks it first, but X-Timestamp is more than 30 seconds from the gateway's clockCompare your robot's clock with time in the answer, and fix it with NTP

In the raw answer, the ± arrives escaped, as \u00b1: compare code, or compare error after parsing the JSON. After Rotate on your company's page, the old secret works for 24 hours more; after Revoke Old, or when the building replaces a secret, it gets signature_mismatch at once. The gateway logs every refused call, so fix the cause before you send again.

429 and Retry-After

Requests are limited per robot and per caller address. The sandbox's limits are on the sandbox page; a building may set its own. A 429 carries Retry-After, in whole seconds, at least 1, and the same number as retry_after in its body: wait that long, then send again (the kit's client gives it as GatewayError.retry_after). Handle 429 by its status, and back off further if they keep coming.

Sending again safely

Methods

The API takes GET and POST. HEAD answers the public pages (/v1/health, /v1/openapi.yaml, /docs and its pages, /status) with their headers and no body; anywhere else, it's 405. OPTIONS answers 204 with Allow: GET, POST, OPTIONS, and HEAD too on the public pages. PUT, PATCH and DELETE answer 405, with the same Allow.

GET /v1/health needs no signature and answers {"ok": true}, with the gateway's release in the X-Gateway-Release header and the API's version in X-Api-Version. Watch it from your own monitoring.

Refusals

A refusal is a normal answer, HTTP 200, with state DENIED, or FAILED if something stops the passage later:

{"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 same codes appear elsewhere: in GET /v1/places, for why a place isn't open to your robot now; in notices (booking conflict, directions changed, stop); and in directions that are refused. The guide has more. On the sandbox, the scenario places give closed, outside_hours, approval_declined, device_refused and fire_recall whenever you ask them.

Reason codes

A few codes come from the refusal's own words, which say more: closed (the reason gives the closure's), timeout (it says in which state), gateway_restarted, emergency (which one, and where), fire_recall, and device_refused (what the door or elevator answered). Each has retry true. no_route comes from directions only: every way there is closed to your robot now. other is anything else: read the reason. The rest are in this table, from the gateway's own list.

reason_codeWaiting can helpThe refusal says, for example
access_endednocompany's access has ended
access_not_startedyescompany's access hasn't started
approval_declinednoapproval declined
approval_timeoutyesno approval in time
badge_inactivenosite credential not active
busy_can_waityeselevator busy: this group can wait
busy_group_shareyeselevator busy: group's hourly share used
busy_robot_limityeselevator busy: robot's hourly limit reached
cancellednocancelled by robot
cant_crossnocan't cross this threshold or slope
city_pausednopaused by the city: a parade, return to your hub
city_ruleit dependscity rule: Pier Plaza, 4 robots there now
closedyesclosed: the floor is being waxed
company_stoppednocompany stopped
curb_ruleit dependscurb rule: no stopping, 7 to 10 am
device_refusedyesopen refused: the controller refused to open it
do_not_disturbnodo not disturb: Room 412, take it to the front desk
earthquakeyesearthquake
egress_onlynoemergency exit: egress only
elevator_budgetyeselevator robot budget reached
emergencyyesemergency: fire alarm on 2
evacuationyesevacuation
fire_alarmyesfire alarm
fire_recallyesfire service recall
floors_requirednoride must name its floors
gateway_restartedyesgateway restarted while CAR_CALLED
insurance_expirednoinsurance expired
insurance_missingnono insurance on file
layout_missingnodoor's floor isn't in the site layout
line_expiredyeswaited too long in line
line_waitingyesrobots are waiting in line
lostnolost: stop and ask for help
mantrap_people_onlynomantrap is for people: robots use the dock
missed_turnyesmissed its turn
no_robot_windowyesno-robot window
no_routeit dependsno way there: Lab Door in the way
no_visitor_passnono visitor pass
not_on_routenonot on its route
not_permittednonot permitted
otherit dependsanything else: read reason
outside_floorsnooutside this company's floors
outside_hoursyesoutside route hours
outside_passnooutside its visitor pass
over_rated_loadnoover the elevator's rated load
pausedyesall robots paused
payload_refusednonot for clean loads
ride_alone_unavailablenoriding alone isn't available here
robot_suspendednorobot suspended
room_vacantnovacant: Room 412 has checked out, take it to the front desk
rule_offnono route switched on
tenant_approvalnono tenant approval
tenant_pendingyestenant approval pending
tenant_revokednotenant revoked its approval
timeoutyestimeout while CAR_CALLED
too_bignotoo big for this door or car
trip_onlynothe owner shares this robot's trips, not whole floors
unknown_floornofloor not served here
unknown_outcomeyesunknown outcome after restart
unknown_resourcenounknown resource
work_order_requirednowork order required