This page shows you how to set up an event and check people in. Managing an
organization's activities needs an org-scoped token.
All paths are under https://api.davi.social/api/v1.
- An activity is an event with a capacity and a check-in window.
- A session is a timeslot within it.
- Attendees check in by presenting an identifier (a card tap, QR scan, wallet, or username). Each check-in writes an immutable proof and can feed reward triggers.
Create an activity and sessions
curl -X POST \
"https://api.davi.social/api/v1/organizations/ORG_SLUG/activities" \
-H "Authorization: Bearer ORG_SCOPED_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Launch Night",
"description": "Doors at 7pm",
"max_attendees": 200,
"start_time": "2026-02-01T19:00:00Z",
"end_time": "2026-02-01T23:00:00Z",
"requires_ticket": false
}'
| Field | Meaning |
|---|---|
max_attendees | Capacity; null is unlimited |
start_time, end_time | The check-in window |
requires_ticket, ticket_template_uuid | Gate entry on a ticket (see Tickets and registration) |
Add timeslots with POST /activities/{activity_slug}/sessions. Each session can
override capacity and ticketing, or inherit them from the activity.
For a recurring schedule, such as a course that meets daily or a conference with
parallel tracks, use POST /activities/{activity_slug}/sessions/batch. Expand the
recurrence yourself and send one entry per occurrence, each with its own name and
times. Every other setting in the request applies to all of them, so capacity and
gate settings stay identical across the batch. Each occurrence is its own
session, so one meeting is cancelled or removed on its own, with no list of
exceptions to maintain. Create a session singly when it needs its own capacity or gate.
Endpoints and scopes
| Method | Path | Scope |
|---|---|---|
POST | /organizations/{organization_slug}/activities | activity:create |
GET | /activities/{activity_slug} | activity:read |
POST | /activities/{activity_slug}/sessions | activity:create |
POST | /activities/{activity_slug}/sessions/batch | activity:create |
GET | /activities/{activity_slug}/attendees | activity:attendees:read |
GET, PATCH | /sessions/{session_slug} | session:read, session:update |
GET | /sessions/{session_slug}/attendees | session:attendees:read |
PATCH | /sessions/{session_slug}/attendees/{wallet_address} | session:attendees:manage |
POST | /sessions/{session_slug}/admit | session:attend |
POST, DELETE | /sessions/{session_slug}/cancellation | session:update |
DELETE | /sessions/{session_slug} | session:delete |
Register for the two scope families separately:
activity:*covers the event as a record: creating it, reading it, listing everyone who ever attended.session:*covers one occurrence and what happens at it: reading its state, checking somebody in, correcting an attendee.
A door app needs only session:*, so the credential at the entrance cannot delete
the event.
Tickets and registration
There is no separate "register" call. Registration means holding a valid
ticket. When an activity or session sets requires_ticket, an attendee must
already hold the ticket, which is a reward issued from the
ticket_template_uuid. Open events (requires_ticket: false) let anyone check
in.
Admit somebody
POST /sessions/{session_slug}/admit (scope session:attend) handles the door in
one call. It resolves the identifier, evaluates the same verdict /eligibility
reads, and acts on it.
curl -X POST \
"https://api.davi.social/api/v1/sessions/SESSION_SLUG/admit" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": { "type": "card", "value": "CARD_IDENTIFIER" },
"idempotency_key": "unique-per-tap",
"proof_metadata": { "method": "nfc_tap" }
}'
| Field | Meaning |
|---|---|
identifier | An IdentifierRef. type is card, wallet, username, user_id, or link |
idempotency_key | Kept per session, for offline replay |
proof_metadata | Optional context, such as method (nfc_tap, qr_scan), location, device |
Admitting yourself (the resolved identity matches your token) is self-service.
Admitting somebody else, as a kiosk or door app does, needs
session:attendees:manage and an org-scoped token.
Read the decision
Every verdict returns 200. Act on decision:
decision | What it means | What to do |
|---|---|---|
admitted | The identity is in | Open the door |
needs_confirm | No ticket, but one can be issued | Redeem the session's ticket, then call /admit again |
blocked | Cannot be admitted, and registering would not help | Show blocked_reason |
On admitted, action_taken says what kind of admission it was:
checkin: this call recorded the attendance.reentry: it re-admitted somebody already inside a session that allows re-entry.null: it was a retry of an admit that already succeeded.
On needs_confirm, issue the ticket with
POST /rewards/{reward_slug}/redeem, redeeming the
ticket_reward_uuid from /ticket-requirement. Then call /admit again; the
second call finds the ticket.
Admission rules
- A repeat admit returns
admittedagain, not a409. A double tap or a retried request is the same admission, so the door does not need to tell a retry from a second person. Reusing anidempotency_keyfor a different resolved identity returns409 idempotency_conflict. That is the only409admitting returns. - Held seats are never refused for capacity. A seat is held by a ticket for
the session or by an earlier admission. When two admits race for the last seat,
exactly one wins and the other gets
at_capacity. - A frozen card is refused first, as
card_frozen, unless the session setsadmits_frozen_cards. If it does, the card is judged like any other and the attendance is recorded withdisputed_reason: card_frozen, for the organizer to keep or void. - The ledger proof is written after the response. An
admittedresponse carriesproof_status: "pending"and noattendance_proof_tx_idyet. The door waits only on the attendance record, so a slow ledger delays the proof, not the queue. Read the proof back later. - Attendance is recorded on the identity's canonical wallet for the organization, the one every card they hold resolves to. A person who taps one card today and another tomorrow is the same attendee.
If a request fails with no response, call /admit again; the repeat is the same
admission. To read the outcome without acting, use
GET /sessions/{session_slug}/attendee-status.
Check before the tap
Two reads, both session:read, answer a door's questions before anyone taps:
GET /sessions/{session_slug}/eligibilitytakes an identifier and reports what this person can do.recommended_actionischeckin,registerornone, andblocked_reasonsays what stands in the way, if anything. It reaches the same verdict/admitacts on, by the same rules.GET /sessions/{session_slug}/ticket-requirementtakes no identifier and reports the session's setup: no ticket needed (not_required), the reward that issues it (available, withticket_reward_uuid), or why it cannot be issued right now (not_configured,unavailable).
Use these instead of deriving either answer yourself. A session's ticket falls back to its activity's, and the reward behind it can be withdrawn or sold out. Logic that reimplements this can disagree with what check-in actually does.
Cancel a session
POST /sessions/{session_slug}/cancellation calls a session off. It stops
admitting anyone and stops taking new seats, at online and offline doors alike.
Its attendance, the tickets people hold, and its reports all stay.
Cancel rather than delete a session that has attendance or issued tickets;
deleting one of those is refused with a 409.
DELETE /sessions/{session_slug}/cancellation reinstates the session, and its
door goes back to whatever its check-in switch says. An activity takes the same
pair at /activities/{activity_slug}/cancellation.
What check-ins power
Each check-in writes an immutable proof and emits an activity.checked_in event.
That event drives reward triggers (for example
activity.checked_in:count:5) and the user's feed. It is
not delivered to webhooks; read the attendee lists above to follow check-ins
from another system.
Next
- Rewards: reward attendance automatically.