This guide runs an event from nothing to a rewarded attendee: create the activity and a session, decide how entry is gated, admit people at the door, and have Davi issue a reward on the fifth check-in. Everything here needs an organization-scoped token.
1. Create the activity and a session
An activity is the event. A session is one occurrence of it, and people check in to 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": "Friday Meetup" }'
curl -X POST "https://api.davi.social/api/v1/activities/ACTIVITY_SLUG/sessions" \
-H "Authorization: Bearer ORG_SCOPED_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Week 1" }'
Model a recurring event as one activity with many sessions. That is what lets a trigger count "attended five times".
2. Decide how entry is gated
There are three gates. Each is checked separately, and passing one does not satisfy another.
| Gate | Set by | Effect |
|---|---|---|
| Ticket | requires_ticket on the activity or session | The attendee must already hold a valid ticket |
| Entitlement | required_entitlement on the activity or session | The attendee's membership tier must grant that key |
| Prior session | An earlier session the attendee must have attended | Check-in is refused, naming the session that is missing |
- A session inherits from its activity and can override it, so a members-only activity can still have one open session.
- The prior-session gate is answered by the person, not the card: someone who attended on one card and turns up with another has still attended.
- Registering means holding a ticket, and a ticket is a
reward. There is no separate register call. The activity
names a reward template in
ticket_template_uuid, and you issue tickets by redeeming that template to the attendee. - An anonymous walk-in cannot pass an entitlement gate. Check-in accepts an identifier that resolves to no Davi user, and that attendee holds no entitlements.
For open attendance, leave requires_ticket false and required_entitlement
unset.
3. Admit people at the door
curl -X POST "https://api.davi.social/api/v1/sessions/SESSION_SLUG/admit" \
-H "Authorization: Bearer ORG_SCOPED_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "identifier": { "type": "card", "value": "CARD_UID" } }'
identifier is the same tagged union used everywhere a person is named (card,
wallet, username, user_id or link), so a door scanner can send whatever
the tap gave it without resolving the person first.
One call decides and acts. decision is one of:
admitted: open the door.needs_confirm: no ticket, but one can be issued. Redeem it and admit again.blocked:blocked_reasonsays why.
A repeat admit answers admitted again rather than conflicting, so a double tap
and a retried request are the same admission.
Who may admit somebody depends on the token:
- Self-service: the resolved identity must match the token's subject. A person checks themselves in.
- On behalf of: needs
session:attendees:manageon an org-scoped token for the session's organization. This is the door-scanner case.
Admitting is in the expensive rate-limit bucket: 20 per minute, roughly one every three seconds. A busy entrance needs more than one credential or a queue.
Show the right refusal
A refusal at the door is a 200, not an error. /admit answers
decision: "blocked" with a blocked_reason for the door to render:
blocked_reason | Show |
|---|---|
card_frozen | "This card is frozen" |
already_admitted | "Already here" |
checkin_closed | "Check-in isn't open" |
too_early / too_late | "Doors open at…" / "Doors have closed" |
membership_required | "Members only": offer to join or upgrade |
prior_session_required | "You need to have attended the earlier session" |
at_capacity | "Full" |
registration_closed | "No ticket, and none can be issued" |
The reasons are checked in table order, so you get the most useful one to tell the person. "Already here" wins over "doors are shut". Opening hours are checked only for a first entry, so somebody re-entering a session that allows it is not turned away for arriving late. Membership and prior attendance are checked before the ticket, because no ticket fixes either.
Three responses are errors, about the request rather than the person:
| Status | code | Show |
|---|---|---|
404 | identifier_unresolved | "Card not recognized" |
403 | forbidden_not_authorized | A staff problem, not an attendee one |
409 | idempotency_conflict | The key was reused for a different person |
4. Reward attendance automatically
Register a reward trigger instead of watching check-ins and redeeming yourself:
curl -X POST \
"https://api.davi.social/api/v1/organizations/ORG_SLUG/reward-triggers" \
-H "Authorization: Bearer ORG_SCOPED_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Reward 5 check-ins",
"reward_template_uuid": "REWARD_UUID",
"trigger_scopes": ["activity.checked_in:count:5"],
"deduplication_window_seconds": 300
}'
The fifth check-in now issues the reward with no call from you.
5. Update your own system
Check-ins are not delivered as webhooks. Your door already holds each decision
in the admit response; to keep another system in step, read
GET /sessions/{session_slug}/attendees (scope session:attendees:read) or
GET /activities/{activity_slug}/attendees (scope activity:attendees:read)
on a schedule and apply the difference.
For rewards, subscribe your webhook endpoint to
reward.redeemed. It fires for rewards that land in a wallet belonging to a
Davi user, so a reward issued to a card-only attendee does not send one.
Deliveries retry, so the same event can arrive twice; use X-Webhook-Delivery
to recognize a repeat.
Next
- Org Activities: activities and sessions in full.
- Rewards: reward templates, redemption and triggers.
- Feature Gating: the entitlement keys a gate reads.