# Smokesignal XRPC API Reference Base URL: `https://smokesignal.events` All XRPC endpoints are available at `/xrpc/{method-nsid}`. Responses are JSON (`application/json`). ## Table of Contents - [Public Query Endpoints](#public-query-endpoints) - [community.lexicon.calendar.searchEvents](#communitylexiconcalendarsearchevents) - [community.lexicon.calendar.getEvent](#communitylexiconcalendargetevent) - [community.lexicon.calendar.getRSVP](#communitylexiconcalendargetrsvp) - [community.lexicon.calendar.listRSVPs](#communitylexiconcalendarlistrsvps) - [Authenticated Procedure Endpoints](#authenticated-procedure-endpoints) - [events.smokesignal.rsvp.linkAttestation](#eventssmokesignalrsvplinkattestation) - [events.smokesignal.event.configure](#eventssmokesignaleventconfigure) - [Authentication](#authentication) - [Error Responses](#error-responses) --- ## Public Query Endpoints ### community.lexicon.calendar.searchEvents Search for events by text query, organizer DID, and/or location. Requires OpenSearch to be configured on the server. **Method:** `GET` **Path:** `/xrpc/community.lexicon.calendar.searchEvents` **Authentication:** None **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `query` | string | No | — | Free-text search query. Use `"upcoming"` to search for upcoming events. | | `repository` | string | No | — | DID of the event organizer to filter by (`did:plc:...` or `did:web:...`). | | `location` | string or string[] | No | — | Location CID(s) to filter by. Repeat the parameter for multiple values. | | `limit` | integer | No | `10` | Maximum number of results to return. | At least one of `query`, `repository`, or `location` must be provided. If none are provided, an empty result set is returned. **Response Body:** ```json { "results": [ { "$type": "community.lexicon.calendar.eventView", "name": "Rust Meetup", "description": "Monthly Rust meetup", "startsAt": "2026-03-01T18:00:00Z", "endsAt": "2026-03-01T20:00:00Z", "countGoing": 42, "countInterested": 15, "countNotGoing": 3, "url": "https://smokesignal.events/alice.bsky.social/3abc123" } ] } ``` Each result contains the event record fields (flattened), RSVP counts, and a web URL for the event. **curl Example:** ```bash # Search by text curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?query=rust+meetup&limit=5' # Search by organizer DID curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?repository=did:plc:1234abcd' # Search upcoming events curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?query=upcoming&limit=10' # Search by multiple locations curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?location=bafyreiabc&location=bafyreixyz' ``` **Example HTTP Request:** ```http GET /xrpc/community.lexicon.calendar.searchEvents?query=rust+meetup&limit=5 HTTP/1.1 Host: smokesignal.events Accept: application/json ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json { "results": [ { "$type": "community.lexicon.calendar.eventView", "name": "Rust Meetup", "description": "Monthly Rust meetup in Tallinn", "startsAt": "2026-03-01T18:00:00Z", "endsAt": "2026-03-01T20:00:00Z", "mode": "physical", "countGoing": 42, "countInterested": 15, "countNotGoing": 3, "url": "https://smokesignal.events/alice.bsky.social/3abc123" } ] } ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 404 | `invalid_repository` | `repository` parameter is not a valid `did:plc` or `did:web` value. | | 503 | `search_unavailable` | OpenSearch is not configured on the server. | | 500 | `search_error` | Failed to initialize or execute the search. | --- ### community.lexicon.calendar.getEvent Retrieve a single event by its organizer DID and record key. **Method:** `GET` **Path:** `/xrpc/community.lexicon.calendar.getEvent` **Authentication:** None **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `repository` | string | Yes | DID of the event organizer (`did:plc:...` or `did:web:...`). | | `record_key` | string | Yes | Record key (rkey) of the event. | **Response Body:** ```json { "$type": "community.lexicon.calendar.eventView", "name": "Rust Meetup", "description": "Monthly Rust meetup", "startsAt": "2026-03-01T18:00:00Z", "endsAt": "2026-03-01T20:00:00Z", "countGoing": 42, "countInterested": 15, "countNotGoing": 3, "url": "https://smokesignal.events/alice.bsky.social/3abc123" } ``` The response contains the event record fields (flattened at the top level), RSVP counts, and a web URL. **curl Example:** ```bash curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.getEvent?repository=did:plc:1234abcd&record_key=3abc123' ``` **Example HTTP Request:** ```http GET /xrpc/community.lexicon.calendar.getEvent?repository=did:plc:1234abcd&record_key=3abc123 HTTP/1.1 Host: smokesignal.events Accept: application/json ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json { "$type": "community.lexicon.calendar.eventView", "name": "Rust Meetup", "description": "Monthly Rust meetup in Tallinn", "startsAt": "2026-03-01T18:00:00Z", "endsAt": "2026-03-01T20:00:00Z", "mode": "physical", "countGoing": 42, "countInterested": 15, "countNotGoing": 3, "url": "https://smokesignal.events/alice.bsky.social/3abc123" } ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 404 | `not_found` | Invalid `repository` DID format or event record not found. | --- ### community.lexicon.calendar.getRSVP Retrieve a specific RSVP record for a given event and identity. **Method:** `GET` **Path:** `/xrpc/community.lexicon.calendar.getRSVP` **Authentication:** None **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `event` | string | Yes | AT-URI of the event (e.g., `at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123`). | | `identity` | string | Yes | DID or handle of the RSVP author. Handles are resolved to DIDs. | **Response Body:** ```json { "uri": "at://did:plc:5678efgh/community.lexicon.calendar.rsvp/3xyz789", "cid": "bafyreiabc123", "record": { "$type": "community.lexicon.calendar.rsvp", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going", "createdAt": "2026-02-10T12:00:00Z" } } ``` **curl Example:** ```bash curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.getRSVP?event=at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123&identity=did:plc:5678efgh' ``` **Example HTTP Request:** ```http GET /xrpc/community.lexicon.calendar.getRSVP?event=at%3A%2F%2Fdid%3Aplc%3A1234abcd%2Fcommunity.lexicon.calendar.event%2F3abc123&identity=did%3Aplc%3A5678efgh HTTP/1.1 Host: smokesignal.events Accept: application/json ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json { "uri": "at://did:plc:5678efgh/community.lexicon.calendar.rsvp/3xyz789", "cid": "bafyreiabc123", "record": { "$type": "community.lexicon.calendar.rsvp", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going", "createdAt": "2026-02-10T12:00:00Z" } } ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 400 | `InvalidRequest` | Invalid event AT-URI format or invalid identity DID. | | 404 | `NotFound` | RSVP not found for the given event and identity. | | 500 | `InternalServerError` | Database error while fetching the RSVP. | --- ### community.lexicon.calendar.listRSVPs List all RSVPs for a given identity with cursor-based pagination. **Method:** `GET` **Path:** `/xrpc/community.lexicon.calendar.listRSVPs` **Authentication:** None **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `identity` | string | Yes | — | DID or handle of the user to list RSVPs for. Handles are resolved to DIDs. | | `limit` | integer | No | `50` | Maximum number of results. Clamped to range `[1, 100]`. | | `cursor` | string | No | — | RFC 3339 datetime string from a previous response for pagination. | **Response Body:** ```json { "rsvps": [ { "uri": "at://did:plc:5678efgh/community.lexicon.calendar.rsvp/3xyz789", "cid": "bafyreiabc123", "record": { "$type": "community.lexicon.calendar.rsvp", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going", "createdAt": "2026-02-10T12:00:00Z" }, "eventUri": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going" } ], "cursor": "2026-02-10T12:00:00Z" } ``` The `cursor` field is only present when there are more results available. Pass it as the `cursor` query parameter to fetch the next page. **curl Example:** ```bash # First page curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.listRSVPs?identity=did:plc:5678efgh&limit=25' # Next page using cursor curl 'https://smokesignal.events/xrpc/community.lexicon.calendar.listRSVPs?identity=did:plc:5678efgh&limit=25&cursor=2026-02-10T12:00:00Z' ``` **Example HTTP Request:** ```http GET /xrpc/community.lexicon.calendar.listRSVPs?identity=did:plc:5678efgh&limit=25 HTTP/1.1 Host: smokesignal.events Accept: application/json ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json { "rsvps": [ { "uri": "at://did:plc:5678efgh/community.lexicon.calendar.rsvp/3xyz789", "cid": "bafyreiabc123", "record": { "$type": "community.lexicon.calendar.rsvp", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going", "createdAt": "2026-02-10T12:00:00Z" }, "eventUri": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "status": "going" }, { "uri": "at://did:plc:5678efgh/community.lexicon.calendar.rsvp/3def456", "cid": "bafyreidef456", "record": { "$type": "community.lexicon.calendar.rsvp", "event": "at://did:plc:9999zzzz/community.lexicon.calendar.event/3ghi789", "status": "interested", "createdAt": "2026-02-08T09:30:00Z" }, "eventUri": "at://did:plc:9999zzzz/community.lexicon.calendar.event/3ghi789", "status": "interested" } ], "cursor": "2026-02-08T09:30:00Z" } ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 400 | `InvalidRequest` | Invalid identity DID format or invalid cursor format (must be RFC 3339). | | 500 | `InternalServerError` | Database error while fetching RSVPs. | --- ## Authenticated Procedure Endpoints These endpoints require an AT Protocol JWT Bearer token. See [Authentication](#authentication) for details. ### events.smokesignal.rsvp.linkAttestation Link an external attestation record (e.g., from a ticket purchase on ti.to or Eventbrite) to an event. This creates an acceptance ticket that the target user can later confirm to complete their RSVP. **Method:** `POST` **Path:** `/xrpc/events.smokesignal.rsvp.linkAttestation` **Authentication:** Required (AT Protocol JWT Bearer token) **Authorization:** The authenticated caller must be both the event organizer and the attestation record owner. **Request Body (`application/json`):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `did` | string | Yes | DID of the target user the attestation is for. | | `event` | string | Yes | AT-URI of the event. | | `aturi` | string | Yes | AT-URI of the remote attestation record. | **Response Body (success):** ```json {} ``` **curl Example:** ```bash curl -X POST 'https://smokesignal.events/xrpc/events.smokesignal.rsvp.linkAttestation' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "did": "did:plc:5678efgh", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "aturi": "at://did:plc:1234abcd/com.example.attestation/3att456" }' ``` **Example HTTP Request:** ```http POST /xrpc/events.smokesignal.rsvp.linkAttestation HTTP/1.1 Host: smokesignal.events Authorization: Bearer eyJhbGciOi... Content-Type: application/json { "did": "did:plc:5678efgh", "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "aturi": "at://did:plc:1234abcd/com.example.attestation/3att456" } ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json {} ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 401 | `AuthRequired` | Missing, invalid, or expired authentication token, or no issuer in token. | | 400 | `InvalidRequest` | Invalid target DID format, invalid event AT-URI, or invalid attestation AT-URI. | | 403 | `Forbidden` | Caller is not the event organizer or not the attestation record owner. | | 404 | `NotFound` | Event not found. | | 500 | `InternalServerError` | Failed to store the attestation link. | --- ### events.smokesignal.event.configure Configure Smokesignal-specific settings for an event, such as email confirmation requirements and external ticketing redirects. **Method:** `POST` **Path:** `/xrpc/events.smokesignal.event.configure` **Authentication:** Required (AT Protocol JWT Bearer token) **Authorization:** The authenticated caller must be the event organizer (owner of the event's AT-URI). **Request Body (`application/json`):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `event` | string | Yes | AT-URI of the event to configure. Must belong to the authenticated user. | | `requireConfirmedEmail` | boolean | No | When `true`, RSVPs require a confirmed email address. | | `disableDirectRsvp` | boolean | No | When `true`, the RSVP button redirects to an external ticketing URL. | | `rsvpRedirectUrl` | string | No | HTTPS URL to redirect users to for external ticketing (max 2048 characters). Must use the `https` scheme. Pass an empty string to clear. | At least one of `requireConfirmedEmail`, `disableDirectRsvp`, or `rsvpRedirectUrl` must be provided. **Response Body (success):** ```json {} ``` **curl Example:** ```bash # Enable email confirmation requirement curl -X POST 'https://smokesignal.events/xrpc/events.smokesignal.event.configure' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "requireConfirmedEmail": true }' # Set up external ticketing redirect curl -X POST 'https://smokesignal.events/xrpc/events.smokesignal.event.configure' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "disableDirectRsvp": true, "rsvpRedirectUrl": "https://ti.to/myorg/my-event" }' ``` **Example HTTP Request:** ```http POST /xrpc/events.smokesignal.event.configure HTTP/1.1 Host: smokesignal.events Authorization: Bearer eyJhbGciOi... Content-Type: application/json { "event": "at://did:plc:1234abcd/community.lexicon.calendar.event/3abc123", "requireConfirmedEmail": true, "disableDirectRsvp": true, "rsvpRedirectUrl": "https://ti.to/myorg/my-event" } ``` **Example HTTP Response:** ```http HTTP/1.1 200 OK Content-Type: application/json {} ``` **Error Responses:** | Status | Error | Description | |--------|-------|-------------| | 401 | `AuthRequired` | Missing, invalid, or expired authentication token, or no issuer in token. | | 400 | `InvalidRequest` | Event AT-URI doesn't belong to the authenticated user, invalid AT-URI format, or no settings provided. | | 400 | `InvalidRedirectUrl` | Redirect URL is not valid or does not use HTTPS. | | 404 | `EventNotFound` | The specified event does not exist. | | 500 | `InternalServerError` | Failed to update event settings. | --- ## Authentication Authenticated endpoints require an AT Protocol JWT passed as a Bearer token in the `Authorization` header: ``` Authorization: Bearer ``` The JWT must: - Be a valid AT Protocol access token - Contain an `iss` (issuer) claim with the caller's DID - Not be expired Tokens are validated by the server using AT Protocol's JOSE/JWT verification with P-256 ECDSA keys. ## Error Responses All error responses follow a consistent JSON structure: ```json { "error": "ErrorCode", "message": "Human-readable description of the error" } ``` Some endpoints use `error_description` instead of `message`: ```json { "error": "error_code", "error_description": "Human-readable description of the error" } ``` ### Common HTTP Status Codes | Status | Meaning | |--------|---------| | 200 | Success | | 400 | Bad request (invalid parameters or input) | | 401 | Authentication required or token invalid | | 403 | Forbidden (insufficient permissions) | | 404 | Resource not found | | 500 | Internal server error | | 503 | Service unavailable (e.g., search not configured) |