Documentation

Org Activities

Create activities and sessions, gate them with tickets, and admit attendees at the door.

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
  }'
FieldMeaning
max_attendeesCapacity; null is unlimited
start_time, end_timeThe check-in window
requires_ticket, ticket_template_uuidGate 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

MethodPathScope
POST/organizations/{organization_slug}/activitiesactivity:create
GET/activities/{activity_slug}activity:read
POST/activities/{activity_slug}/sessionsactivity:create
POST/activities/{activity_slug}/sessions/batchactivity:create
GET/activities/{activity_slug}/attendeesactivity:attendees:read
GET, PATCH/sessions/{session_slug}session:read, session:update
GET/sessions/{session_slug}/attendeessession:attendees:read
PATCH/sessions/{session_slug}/attendees/{wallet_address}session:attendees:manage
POST/sessions/{session_slug}/admitsession:attend
POST, DELETE/sessions/{session_slug}/cancellationsession: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" }
  }'
FieldMeaning
identifierAn IdentifierRef. type is card, wallet, username, user_id, or link
idempotency_keyKept per session, for offline replay
proof_metadataOptional 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:

decisionWhat it meansWhat to do
admittedThe identity is inOpen the door
needs_confirmNo ticket, but one can be issuedRedeem the session's ticket, then call /admit again
blockedCannot be admitted, and registering would not helpShow 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 admitted again, not a 409. 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 an idempotency_key for a different resolved identity returns 409 idempotency_conflict. That is the only 409 admitting 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 sets admits_frozen_cards. If it does, the card is judged like any other and the attendance is recorded with disputed_reason: card_frozen, for the organizer to keep or void.
  • The ledger proof is written after the response. An admitted response carries proof_status: "pending" and no attendance_proof_tx_id yet. 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}/eligibility takes an identifier and reports what this person can do. recommended_action is checkin, register or none, and blocked_reason says what stands in the way, if anything. It reaches the same verdict /admit acts on, by the same rules.
  • GET /sessions/{session_slug}/ticket-requirement takes no identifier and reports the session's setup: no ticket needed (not_required), the reward that issues it (available, with ticket_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.