Skip to content

Patterns & recipes

Common integration shapes, written as cookbook entries. Pick the one closest to what you're building, adapt as needed.

"I want to keep my data in sync"

The most common pattern. Your integration mirrors event data into your own storage so you can use it when Revento is unavailable, query it without rate-limit pressure, or join it against your own data.

Strategy: webhook-driven updates + occasional full reconcile

flowchart TB
    A[Connect to event] -->|once| B[Initial full sync<br/>via API pagination]
    B --> C[Listen on webhooks]
    C -->|application.approved| D[Update local row<br/>directly from payload]
    C -->|participant.registered| E[Insert participant<br/>directly from payload]
    C -->|activity.capacity_state_changed| F[Update signup state<br/>from payload]
    C -->|data.deletion_required| G[Stop, schedule deletion]

    H[Daily reconcile cron] -->|optional| I[List participants<br/>diff against local]

Initial full sync paginates /events/{id}/participants and /events/{id}/activities using the cursor. After that, react to webhooks for changes — usually application.approved, participant.registered, participant.profile_updated, activity.updated. Each webhook payload contains the full resource snapshot, so you can update your local state directly without a follow-up API fetch. Optionally run a daily reconcile that fetches the full list and diffs against local state, in case any webhooks were missed.

What to subscribe to

  • application.submitted — new applications coming in
  • application.approved / application.rejected / application.revision_requested — status changes
  • participant.registered — a participant transitioned to "registered" status
  • activity.capacity_state_changed — debounced snapshot of activity signups
  • data.deletion_required — connection ended, schedule deletion

Note: there is no participant.cancelled event. Application cancellation surfaces via the next API fetch — see the event catalog for the canonical list.

What NOT to subscribe to

If you don't actually use a particular event type, don't subscribe. Each subscribed event type adds load; subscribe only to types you actually use.

Deduplication

Use X-Revento-Delivery-Id as the dedup key. If your handler crashes mid-processing and Revento retries, you'll see the same delivery id again — process idempotently.

If a webhook handler does need to call the API for related data not in the snapshot (e.g. you got application.approved and want the parent event's metadata that wasn't in the payload), dedupe those follow-up fetches by resource id over a short window. The application snapshot itself comes in the payload — no fetch needed for the application's own data.

"I want to know who's attending right now"

A live attendee count, a "who's checked in" view, etc.

Strategy: combine participant.registered + your own check-in event

participant.registered tells you a participant transitioned to "registered" status. If you have your own check-in concept (e.g. a badge scanned on arrival), you combine that with the registered list.

If you don't have your own check-in:

  • "Registered" ≈ "they're committed" (paid + approved). Close enough to "attending" for most purposes.
  • Filter participant.registered events by event-id and timestamp, count them.

What about cancellations?

A cancellation isn't a webhook event. Detect it via reconcile pass: list /events/{id}/participants periodically and diff against your local state. Mark missing participants as cancelled.

"I want to render personalized content for a participant"

Your integration is a participant-facing app. Show "Hi {name}" and "You're at {event name}, here are the activities you signed up for."

Strategy: Login with Revento + cached participant context

def render_participant_view(user_token):
    profile = api.get("/me/profile", token=user_token)
    apps    = api.get("/me/application", token=user_token)
    program = api.get(f"/events/{user_token.event_id}/program",
                      token=installation_token_for(user_token.event_id))
    return render(name=profile.display_name,
                  event_id=user_token.event_id,
                  application=apps.data[0],
                  program=program)

Note the dual-token pattern: user-side endpoints use the user token, but the program (which is event-wide, not user-specific) is fetched with your installation token for that event. You hold both tokens because both flows ran.

Caching

The participant's profile (profile.read) is stable; cache for an hour, refetch on demand.

The event program is stable enough; cache for hours, invalidate on activity.* / thread.* / location.* webhooks for fine-grained updates or event.unpublished to deactivate.

The OAuth flow can return with error=access_denied if the participant clicks "Cancel" on the consent screen.

Strategy: graceful retry path, no looping

@app.route("/callback")
def callback():
    if request.args.get("error") == "access_denied":
        return render("consent-declined.html",
                      message="You must grant consent to continue.",
                      retry_url=url_for("signin"))
    # ... normal token-exchange path

Your "consent declined" page should:

  • Explain why consent is needed (one sentence)
  • Offer a clear "Try again" button that re-runs the OAuth flow
  • Optionally explain what data your integration would access (mirrors what the consent screen showed)

Don't auto-retry. The participant declined deliberately; respect that. Provide an obvious path back if they change their mind.

"I want to support multi-event participants"

Same human attends 3 events, all with your integration. Each event is a separate context with a separate user token.

Strategy: per-(user, event) sessions, joined on user_id in your UI

Your storage is keyed by (user_id, event_id). Your "what events does this person have" query is a WHERE user_id = ? lookup that returns multiple rows.

SELECT event_id, signed_in_at FROM revento_user_sessions
WHERE user_id = $1
ORDER BY signed_in_at DESC;

Render a "Switch event" picker in your UI, or default to most-recent.

Don't try to merge tokens

Each user token has one event_id. Don't substitute one for another even if both are for the same human — event_not_authorized will fire.

"I want to react fast to a single event"

Single high-priority event type — e.g. "page someone's phone when an application gets flagged for review."

Strategy: subscribe minimally, alert immediately

  • Subscribe ONLY to application.revision_requested (or whatever your specific trigger is).
  • Webhook handler does the absolute minimum: read the relevant fields from the payload (which already includes the full resource snapshot), then fire off your alert (PagerDuty, Slack, SMS).
  • Heavy work (sending follow-up emails, syncing to a CRM) happens async on your side.
  • Respond 200 to Revento within 10 seconds even if the alert is still in-flight.

The webhook payload already contains the full application object — no API call is needed inside the handler unless you want related context (e.g. the parent event's metadata).

"I want a quick org-level dashboard"

You're not connected to one event — you're trying to give an organizer a view of "all my events with your integration."

Strategy: per-event tokens + your own aggregate

Iterate over the connections the organizer has made (each a separate (event_id, organization_id) tuple), fetch each, aggregate in your UI.

def org_dashboard(organization_id):
    connections = db.connections.where(organization_id=organization_id, status="active")
    summaries = []
    for conn in connections:
        event = api.get(f"/events/{conn.event_id}", token=conn.token)
        participants = api.get(f"/events/{conn.event_id}/participants?limit=50",
                                token=conn.token)
        summaries.append({
            "event_name": event.name,
            "participant_count": len(participants.data),
            "event_dates": (event.start_date, event.end_date),
        })
    return render(summaries=summaries)

This makes N API calls per dashboard render. Cache aggressively (5–15 minutes is usually fine for a dashboard).

"I want to detect when my connection is broken"

A health check, basically.

Strategy: probe /events/{id} periodically, surface failure

def health_check(connection):
    try:
        api.get(f"/events/{connection.event_id}", token=connection.token)
        return "healthy"
    except RevokedTokenError:
        return "revoked"
    except RateLimitError:
        return "throttled"
    except APIError:
        return "error"

Run on a low-frequency cron (every 15 min, every hour — not faster, no point). Surface to your operations dashboard.

Don't probe just to keep the token "fresh" — the lifetime curve is independent of usage; probing doesn't extend it.

What NOT to do

  • Don't store client_secret in client-side code. It's a confidential client secret. Lives only on your servers.
  • Don't share installation tokens across customers. They're per-(event, org)-tuple. Mixing them up is a security incident.
  • Don't retry 400 invalid_grant. It's permanent. Trigger re-consent flow.
  • Don't ignore data.deletion_required. Even if you don't have great deletion infrastructure on day one, at minimum log it and schedule manual cleanup.
  • Don't poll faster than your data actually changes. If applications are approved at most every few minutes, polling every 30 seconds wastes 95% of your budget.