Passes and pass rules
Set when and where a visit's pass admits with pass_rules, check visitors in despite a rule with override_pass_rules, and let them back in on the same pass with re-enter.
This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.
Every expected visit has a pass: the link and QR code the visitor shows at reception, the kiosk or the gate. The visit's pass object says where it was sent, when and where it admits, and, for an entry on a pass that admits again, which visit holds the pass.
{
"id": "visit_01m4k5z4j0fxbte6sn6e8tpgza",
"status": "expected",
"expected_at": "2026-10-12T07:00:00Z",
"expected_until": "2026-10-16T17:00:00Z",
"pass": {
"channels": ["sms"],
"last_sent_at": "2026-10-09T10:14:03Z",
"rules": {
"daily": { "from": "07:00", "until": "17:00" },
"single_use": true,
"gate_ids": ["gate_01kjpt7m20fpf9vcsaexsxgqr3"]
},
"for_visit_id": null
}
}Days and pass rules
A pass admits on the visit's days, from expected_at to expected_until, counted in the site's time zone: one day for most visits, up to 92 days for a multi-day pass. pass_rules narrows that:
| Field | Means | Plan |
|---|---|---|
daily | { "from": "HH:MM", "until": "HH:MM" }: the hours it admits on each of its days, at the site. from is before until. | Starter and above |
window | { "before": minutes, "after": minutes } (0–720 each): around expected_at on a one-day pass. With daily, before opens each day early. | Growth and above |
single_use | true: one entry (one a day, on a multi-day pass). | Growth and above |
gate_ids | Up to 20 gates at the visit's site. | Growth and above |
An empty object, {}, means no rules: the pass admits on its days, at any gate, any number of times. Several days without daily need Starter and above too. A rule your plan doesn't include returns plan_required; a gate at another site returns validation_failed at body.pass_rules.gate_ids. Plans are checked when the visit is made or its pass changes, so moving to a lower plan never changes passes already sent.
Setting rules
- On create (POST/visits): send
pass_rules. Leave it out, and the visit takes its visit type's default rules, as your plan allows them, with only the gates at its site. Publishable keys can't set rules: a pre-registration withpass_rulesreturnsvalidation_failedatbody.pass_rules. - On update (PATCH/visits/{visit_id}):
pass_rulesreplaces them;{}clears them. Only while the visit isexpected(invalid_stateotherwise).
Visit types' default rules are set by admins in the Console; they aren't in the API.
Checking in against the rules
POST/visits/{visit_id}/check-in checks a pass that has rules before anything else.
When they don't admit the visitor now, the call fails with invalid_state and an error whose location names the reason:
location | Means |
|---|---|
pass_rule.not_yet | Before its window or its first day. |
pass_rule.expired | After its window or its last day. |
pass_rule.outside_hours | Outside its daily hours. |
pass_rule.wrong_gate | gate_id isn't one of its gates. |
pass_rule.used | Single use, and the visitor has already come in (that day). |
pass_rule.ended | The visit was cancelled, declined or missed. |
{
"type": "https://docs.agoo.ardent.africa/developers/concepts/errors#invalid_state",
"title": "Invalid state",
"status": 409,
"detail": "This pass admits only during its daily hours. Let them in anyway to override it; the audit trail records it.",
"code": "invalid_state",
"request_id": "req_01m4k6c2r0e9q7w3t5y8u1i2o4",
"errors": [{ "location": "pass_rule.outside_hours", "message": "This pass admits only during its daily hours." }]
}To let the visitor in anyway, send the request again with "override_pass_rules": true. The override is recorded on the visit's timeline and in the audit trail, as your key or app. ended can never be overridden. Branch on the location, not on detail.
A pass without rules is checked in as before, early or late: its days only bound the signed code that kiosks check offline.
Back in on the same pass
When a visitor on a multi-day pass comes back the next day, or anyone steps out and returns while their pass admits, call POST/visits/{visit_id}/re-enter with the pass's visit (or its latest entry):
curl -X POST https://api.agoo.ardent.africa/v1/visits/visit_01m4k5z4j0fxbte6sn6e8tpgza/re-enter \
-H "Authorization: Bearer $AGOO_API_KEY" \
-H "Idempotency-Key: 6f1c2a9e-1b3d-4c5e-8f7a-9b0c1d2e3f4a" \
-H "Content-Type: application/json" \
-d '{ "gate_id": "gate_01kjpt7m20fpf9vcsaexsxgqr3" }'- The pass's visit must be
checked_out, with no entry already expected, waiting or on site (invalid_stateotherwise). - The rules must admit now, or
override_pass_rulesmust betrue, with the samepass_rule.<reason>errors as check-in. - The response is a new visit for this entry:
pass.for_visit_idis the visit that holds the pass, and it's approved as the pass was. It's then checked in as usual: the watchlist runs again and the host is told, so it may come backawaiting_approvalif your check-in rules need the host. visit.createdandvisit.checked_infire for the entry, so each day on site is its own visit in your data.
Scope: visits:write.
The pass's QR code
The QR code on a pass carries a code signed with the organisation's pass key, so Agoo's kiosks and guard apps can check it with no network. Treat it as opaque: its format is for those apps, and there's no endpoint that reads it. Use the visit's pass object instead. See Signed passes for what it means for an organisation.