# Gerany Integration API

<!-- Canonical copy. A public mirror is served at https://gerany.com/integration-api.md
     from apps/web/public/integration-api.md — regenerate it after editing here:
     sed -e 's|https://gerany.com/integration-api.md|https://gerany.com/integration-api.md|g' \
         -e 's|at https://gerany.com/integration-api.md|at https://gerany.com/integration-api.md|g' \
         docs/INTEGRATION-API.md > apps/web/public/integration-api.md
     The human-facing walkthrough with copy buttons is https://gerany.com/organizers
     (apps/web/src/app/organizers/page.tsx); https://gerany.com/llms.txt points AI
     assistants at both. Keep the three in step. -->

Server-to-server API for **people who host things on a schedule** — studios, run clubs, supper clubs, tours, workshops, venues. Put your calendar where your neighbors look, sell seats, run the door, and keep your own booking system in charge.

**Three ways to connect, from no code to full control:**

| | You need | What you get |
|---|---|---|
| **1. Paste a calendar link** | a public ICS URL (Google Calendar, Apple Calendar, Outlook, most booking tools export one) | every upcoming date becomes an experience neighbors can find, RSVP to and buy tickets for; re-synced every hour; nothing to maintain |
| **2. Ask your AI assistant** | an API key + one of the prompts at the bottom of this page | the assistant reads this document and builds the integration for you — a sync script, a "tickets" section on your website, a door kiosk, a box office |
| **3. Call the API yourself** | an API key | everything below: pricing and capacity, issuing barcodes for seats you sold elsewhere, validating and redeeming codes at the door, the live door list, your own listing |

- **Base URL:** `https://gerany.com/v1`
- **Format:** JSON over HTTPS. Responses are envelopes (`{"event": …}`, `{"attendees": …}`). Errors are `{"error": {"code": "...", "message": "..."}}` with a meaningful HTTP status. Branch on `code`, never on `message`.
- **Auth:** an API key sent as a bearer token:

```
Authorization: Bearer gk_<64 hex characters>
```

## Getting a key

1. On gerany.com, open **Settings → API keys** and create a key (about a minute; the iOS and Android apps link there).
2. The secret (`gk_…`) is shown **once** — store it like a password. Only a hash is kept server-side; a lost key must be revoked and re-created.
3. Keys carry the `events` scope and act **as your account**: your posting gates (verified phone/email + profile photo) and, for paid tickets, your Gerany+ plan + active payout account apply exactly as in the app.

Limits: 10 live keys per account. Revoking a key kills it immediately.

**What a key can NOT do — by design.** Keys work only on the endpoints below. Messages, calls, payments/wallet, profile, moderation, and every other surface reject keys with `401 api_key_not_allowed`. Buying tickets and RSVPing are buyer actions and stay app-only, so a leaked key can never spend anyone's money.

## Concepts

- **Experience (event):** has `starts_at`/`ends_at`, free text `location_text`, and a venue point (`lat`/`lng`, falls back to your stored location). RSVP-only by default.
- **Ticketed experience:** `ticket_price_cents` between `1000` ($10) and `250000` ($2,500); optional `capacity`. That price is what YOU receive: Gerany's fee of 8% + $0.30 is added on top and charged to the buyer, and the full ticket price reaches your Stripe payout account as soon as the payment clears — Gerany holds nothing. Tickets go on sale 60 days before the start. Because the money is already yours, refunding buyers if you cancel is your responsibility: Gerany voids the tickets and tells them to contact you.
- **Ticket:** one admission, an opaque single-use code. Two kinds, one door:
  - **bought** through Gerany's checkout by a neighbor (the app shows it as a QR);
  - **issued** by you through the API for a seat that did not go through Gerany — sold by your own booking system, a guest, a staff pass. Same code format, same scanner, same capacity count; no money moves through Gerany, so it is voided rather than refunded.
- **Barcode:** the payload is `gerany:ticket:<code>` (the code is 32 hex characters). Render it as a QR code; a Code-128 barcode works too, it is plain ASCII. Check-in accepts the payload or the bare code.
- **Calendar feed:** a public ICS URL you connect once; Gerany imports upcoming entries as your experiences and re-syncs hourly. Simple recurring rules (daily/weekly) are expanded ~5 weeks ahead. All-day entries are skipped (an experience needs a time). Cancelling an imported experience in Gerany sticks — the sync never resurrects it.

## Endpoints

### Create an experience

```bash
curl -X POST https://gerany.com/v1/events \
  -H "Authorization: Bearer $GERANY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Sunset yoga in the park",
    "description": "All levels. Bring a mat.",
    "location_text": "Prospect Park, Long Meadow",
    "starts_at": "2026-09-20T18:00:00-04:00",
    "ends_at":   "2026-09-20T19:00:00-04:00",
    "lat": 40.6631, "lng": -73.9708,
    "ticket_price_cents": 1500,
    "capacity": 20
  }'
```

→ `201 {"event": {...}}`. Omit `ticket_price_cents` (or send `0`) for a free, RSVP-only experience. Selling tickets requires a paid plan + active payout account, else `409 provider_not_payable`.

### List / read

- `GET /v1/events?organizer=me` — **your own experiences**, wherever they are, soonest first (`?cursor=` keyset pagination; add `&canceled=1` to include canceled ones for reconciliation). Each carries `ticket_price_cents`, `capacity`, `tickets_sold`, `going_count`. Use this, not the plain list: `GET /v1/events` without `organizer=me` is the neighbor's view, a circle around your account's stored location, and a studio whose owner signed up somewhere else would never see its own venue in it.
- `GET /v1/events/{id}` — one experience.
- `GET /v1/events/{id}/ics` — the experience as a `text/calendar` file.

### Update ticketing / cancel

- `PATCH /v1/events/{id}` with `{"ticket_price_cents": 2000}` and/or `{"capacity": 30}`.
  - Price is frozen once any ticket exists (`409 tickets_exist`).
  - Capacity can grow anytime; it can never drop below seats already taken (`422 invalid_capacity`). `"capacity": 0` clears the limit.
- `DELETE /v1/events/{id}` — cancel (soft; every live ticket is voided and each holder with an account is notified — refunding them is yours to do, since the money has already been paid out to you).

### Issue a ticket (a seat sold elsewhere, a guest, a staff pass)

```bash
curl -X POST https://gerany.com/v1/events/$EVENT/tickets/issue \
  -H "Authorization: Bearer $GERANY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"holder_name": "Jamie Ortiz", "note": "Booked through our website, order 4471"}'
```

→ `201 {"ticket": {"id", "status": "valid", "code", "holder_name", "issued": true, ...}, "qr": "gerany:ticket:<code>"}`

- `qr` is the exact string to encode. Print it, email it, show it in your own app — the Gerany door check-in (or a kiosk you build on it) redeems it exactly like a paid ticket.
- To issue to a neighbor's Gerany account instead, send `{"username": "jamieo"}`; the ticket then appears in their app's ticket wallet. `409 ticket_exists` if they already hold one for this experience.
- Capacity is enforced: `409 sold_out` when the experience is full (paid, pending and issued tickets all count).
- `422 holder_required` when neither `holder_name` nor `username` is given; `404 user_not_found` for an unknown username.

### Look up a code without redeeming it

```bash
curl https://gerany.com/v1/events/$EVENT/tickets/gerany:ticket:4f1c… \
  -H "Authorization: Bearer $GERANY_KEY"
```

→ `200 {"attendee": {...}, "admissible": true}` — `admissible` is true only for a valid, unredeemed ticket. Use it to show the holder's name before the door person taps admit, or to let a box office confirm a printed code. `404 ticket_not_found` for a code that is not a ticket for this experience.

### Check-in (redeem a ticket)

```bash
curl -X POST https://gerany.com/v1/events/$EVENT/checkin \
  -H "Authorization: Bearer $GERANY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "gerany:ticket:4f1c…"}'   # QR payload or bare code — both accepted
```

- `200 {"attendee": {...}}` — admitted, exactly once. `attendee.neighbor.display_name` is the holder (for an issued ticket, the name you gave; `neighbor.id` is then empty).
- `409 ticket_used` — already checked in (turn them away or verify identity).
- `409 ticket_unpaid` / `409 ticket_invalid` — not admitted (payment in flight / voided or refunded).
- `404 ticket_not_found` — not a ticket for this experience.

### Void an issued ticket

- `DELETE /v1/events/{id}/tickets/{code}` → `204`. Only a **valid, unredeemed, issued** ticket can be voided. A ticket bought through Gerany answers `409 ticket_paid` — that one is money and is refunded through the order in the app, never voided here.

### Attendees (door list + summary)

```bash
curl https://gerany.com/v1/events/$EVENT/attendees -H "Authorization: Bearer $GERANY_KEY"
```

→

```json
{
  "attendees": [
    {"ticket": {"id": "...", "status": "valid", "holder_name": "Jamie Ortiz", "issued": true}, "neighbor": {"display_name": "Jamie Ortiz"}, "amount_cents": 0},
    {"ticket": {"id": "...", "status": "checked_in", "checked_in_at": "..."}, "neighbor": {"id": "...", "username": "sam", "display_name": "Sam"}, "amount_cents": 1500}
  ],
  "summary": {"capacity": 20, "sold": 2, "checked_in": 1, "pending": 0, "issued": 1, "revenue_cents": 1500}
}
```

Statuses: `pending` (payment in flight — not admitted), `valid`, `checked_in`. `summary` is the door headline: seats sold (valid + in), how many are in, payments still in flight, how many you issued yourself, and what the sold seats brought in (ticket prices, before Gerany's buyer-paid fee). Attendees carry public-profile fields only — never a phone number or email.

### Calendar feeds

- `POST /v1/me/calendar-feeds` with `{"url": "https://…/basic.ics", "lat": …, "lng": …, "location_text": "My studio"}` — connects and imports immediately (`201 {"feed": {...}, "imported": N}`). `lat`/`lng` set the default venue for imported entries; omitted → your stored location.
- `GET /v1/me/calendar-feeds` — list with `last_synced_at` / `last_error`.
- `DELETE /v1/me/calendar-feeds/{id}` — disconnect (already-imported experiences stay).

Up to 5 feeds. The fetcher only accepts public URLs (private/internal addresses are refused). Imported experiences are ordinary experiences: set a price and capacity on them with `PATCH`, issue tickets, run the door — the sync updates title/time/description in place and never touches your ticketing.

## Errors you should handle

| Status | Code | Meaning |
|---|---|---|
| 401 | `invalid_api_key` | Unknown or revoked key |
| 401 | `api_key_not_allowed` | This endpoint doesn't accept keys |
| 403 | `insufficient_scope` | Key lacks the needed scope |
| 403 | `verify_required` / `photo_required` | Your account hasn't met the posting gate (fix in the app) |
| 403 | `forbidden` | Not your experience |
| 409 | `provider_not_payable` | Paid plan or payout account missing for ticket sales |
| 409 | `tickets_exist` | Price change after tickets sold |
| 409 | `sold_out` | Issuing past capacity |
| 409 | `ticket_exists` | That account already holds a ticket |
| 409 | `ticket_paid` | Voiding a bought ticket (refund the order instead) |
| 409 | `ticket_used` / `ticket_unpaid` / `ticket_invalid` | Check-in refused, see above |
| 409 | `event_canceled` | Issuing on a canceled experience |
| 404 | `ticket_not_found` / `user_not_found` | Lookup misses |
| 422 | `invalid_price` / `invalid_capacity` / `invalid_start` / `holder_required` / `invalid_code` | Validation |
| 429 | `rate_limited` | Slow down; retry with backoff |

Idempotency: `POST /v1/events` and `POST …/tickets/issue` are **not** idempotent — calling twice makes two. If you sync from an external system, prefer a connected calendar feed for the schedule (Gerany dedupes by ICS UID) and store the returned `event.id` / `ticket.code` on your side for everything else.

---

## For AI assistants (LLM integration)

Give your assistant this page. Both of these URLs are public and stable:

- `https://gerany.com/integration-api.md` — this document
- `https://gerany.com/llms.txt` — a one-page index written for assistants

Then paste one of the prompts below, fill in the brackets, and put your key in the `GERANY_KEY` environment variable where the code will run. The prompts are written for ChatGPT, Claude, Cursor, Claude Code, Copilot and similar tools; they work as-is in a chat window and as the first message of a coding session.

**Facts an assistant needs (and what these prompts rely on):**

1. Base URL `https://gerany.com/v1`; auth header `Authorization: Bearer gk_…`. The human gets the key from Gerany → Settings → API keys and supplies it as an environment variable — never hard-code it, never print it, never ship it to a browser.
2. All requests/responses are JSON envelopes; error shape `{"error":{"code","message"}}` — branch on `code`.
3. Money is integer **cents** (`1500` = $15.00). Times are RFC 3339 with timezone.
4. The key acts as the organizer's own account: only their experiences, attendees and tickets are reachable; check-in, issue, lookup and void work only on experiences they organize.
5. `GET /v1/events?organizer=me` is the organizer's listing. The plain list is the neighbor's proximity view.
6. A ticket barcode is the string `gerany:ticket:<code>`; `POST …/tickets/issue` returns it as `qr`. Redeem with `POST …/checkin`, validate without redeeming with `GET …/tickets/{code}`.
7. The full machine-readable contract is `packages/api-client/openapi.yaml` in the repository (the `events` tag).

### Ready-to-paste prompts

**Prompt 1 — put my existing schedule on Gerany (start here):**

> Read https://gerany.com/integration-api.md. My schedule lives in [SYSTEM — e.g. Google Calendar / Mindbody / Acuity / a spreadsheet]. First check whether that system can give me a public ICS URL; if it can, walk me through connecting it with one call to `POST /v1/me/calendar-feeds` using my venue's coordinates ([LAT], [LNG]) and show me how to read sync health from `GET /v1/me/calendar-feeds`. Only if there is no ICS export, write a small script that reads my schedule from [SOURCE] and creates each upcoming date with `POST /v1/events` (title, start/end in my timezone, `ticket_price_cents`, `capacity`), storing the returned `event.id` in [DB/file] so re-runs never duplicate, and updating price/capacity with `PATCH` when they change. The API key is in the `GERANY_KEY` environment variable.

**Prompt 2 — sell seats on my own website, tickets on Gerany:**

> Read https://gerany.com/integration-api.md. Add an "Upcoming" section to my site [STACK — e.g. Next.js / WordPress / Squarespace code block] rendered by a server-side job (never client-side — the key must not reach the browser). It fetches `GET /v1/events?organizer=me` with the `GERANY_KEY` env var and shows title, date, price (`ticket_price_cents`/100 as USD) and seats left (`capacity - tickets_sold`). Each card links to `https://gerany.com/events/{id}` where neighbors pay; my site never handles payments.

**Prompt 3 — my booking system already sells the seats; I want one door:**

> Read https://gerany.com/integration-api.md. My bookings come from [SYSTEM]. For every confirmed booking, call `POST /v1/events/{id}/tickets/issue` with `holder_name` (and `note` = my booking reference), keep the returned `qr` string with the booking, render it as a QR code on the confirmation email/page, and on cancellation call `DELETE /v1/events/{id}/tickets/{code}`. Map my classes to Gerany experiences by storing `event.id` per class date; create missing ones with `POST /v1/events`. Handle `sold_out` by telling the customer, `ticket_exists` by reusing the existing code. The key is in `GERANY_KEY`.

**Prompt 4 — door check-in kiosk:**

> Read https://gerany.com/integration-api.md. Build a minimal check-in page for my experience [EVENT_ID]: a QR scanner (or text input) sends the scanned value verbatim to a small server proxy holding `GERANY_KEY`. The proxy first calls `GET /v1/events/[EVENT_ID]/tickets/{code}` and shows the holder's name and `admissible`; on a tap it calls `POST /v1/events/[EVENT_ID]/checkin`. Green + name on 200; distinct red screens for `ticket_used` vs `ticket_unpaid`/`ticket_invalid`; grey for `ticket_not_found`. Show the door list from `GET /v1/events/[EVENT_ID]/attendees` refreshed every 30 s, with `summary.checked_in` of `summary.sold` at the top.

**Prompt 5 — box office: print codes for walk-ins:**

> Read https://gerany.com/integration-api.md. Write a command-line tool: `issue <event_id> "<name>" [note]` calls `POST /v1/events/<event_id>/tickets/issue`, prints the holder name and renders the returned `qr` string as a QR code in the terminal and as a PNG file; `list <event_id>` prints `GET …/attendees` as a table with the summary line; `void <event_id> <code>` calls `DELETE …/tickets/<code>` and explains `ticket_paid` if it comes back. Key from `GERANY_KEY`; exit non-zero on any error and print the error `code`.

**Prompt 6 — verify an integration end-to-end (assistant self-check):**

> Using `GERANY_KEY` from the environment against https://gerany.com/v1: (1) create a $10 test experience starting tomorrow with capacity 2; (2) read it back with `GET /v1/events?organizer=me` and confirm `ticket_price_cents` is 1000; (3) PATCH capacity to 3 and confirm; (4) issue a ticket to "Test Holder", look it up and confirm `admissible` is true, check it in, look it up again and confirm `admissible` is false; (5) issue two more and confirm the fourth returns `sold_out`; (6) fetch the `.ics` and confirm the SUMMARY; (7) DELETE the experience. Print each request, status and a one-line result. If any step returns `verify_required`, `photo_required` or `provider_not_payable`, stop and tell me what to fix in the Gerany app — those are account states, not API errors.

---

*Questions: support@gerany.com. This API is v1 and additive-only: new fields may appear at any time — parse leniently.*
