Loading...
Generated from the live specification, so this page cannot fall behind the running API.
/api/v1/meWho am IResolves the key to an account: name, email, phone on file, organiser status, whether it can publish, and how many events it owns.
Start here. `canPublish` is false until the account has been granted organiser access — publishing will 403 until then.
Returns · 200
{ userId, email, name, phone, isOrganizer, canPublish, events: { total, draft, published }, apiKeyId }/api/v1/eventsList your eventsEvery event this key's account owns, newest first, undated events last.
Scoped to the caller. You can never see another organiser's events.
Returns · 200
{ success, data: Event[] }/api/v1/eventsCreate an eventCreates an event. It is always created as a DRAFT — it has no public page and cannot sell until you publish it.
The required date field is `startDate`. Sending `date` returns 400 — the response object echoes both, which misleads on a second read. `sendTicketEmails` is accepted by the schema but NOT written: switching Vihaya's ticket email off is a platform-staff action (it is fixed in Firestore rules so an organiser cannot silence their own attendees' tickets). Ask support. Until they do, sending your own ticket email means the attendee receives BOTH.
| Field | Type | Notes |
|---|---|---|
titlereq | string | Shown publicly. The URL slug is derived from it. |
descriptionreq | string | Shown on the event page. |
startDatereq | string | e.g. "2027-03-01". Not `date` — that is only echoed back in the response. |
endDate | string | Must be on or after startDate. |
location | string | Venue or city. |
capacity | number | 0 means unlimited. |
price | number | Rupees. 0 or omitted makes the event free. |
sendTicketEmails | boolean | ACCEPTED BUT IGNORED — see the gotcha. Automatic ticket email stays ON. |
specialPrices | array | Ticket tiers. Set `membersOnly: true` on one to restrict it to people holding a live membership on this account. |
Returns · 201
201 { success, data: Event }/api/v1/events/{id}Get one eventThe full event document, including its tracks.
Returns · 200
{ success, data: Event }/api/v1/events/{id}Update an eventChange any editable field. Send `{ "status": "published" }` to put it on sale, `"draft"` to take it down.
Publishing is refused with 403 if the account has no organiser grant. Unpublishing is never blocked.
| Field | Type | Notes |
|---|---|---|
status | "draft" | "published" | Publishing requires organiser access — check `canPublish` on /me first. |
Returns · 200
{ success, data: Event }/api/v1/events/{id}Delete an eventPermanently removes an event you own, and every registration under it.
⚠️ THIS ONE DOES NOT ACCEPT AN API KEY. Unlike every other endpoint here it requires a signed-in session — `Authorization: Bearer <Firebase ID token>` — and answers 401 "Missing Authorization Bearer token" to `x-api-key`. Deletion is recursive and irreversible, so it was deliberately left on the dashboard's own auth. An event still holding PAID registrations is refused with 409 — refund them first. `?force=true` is honoured for Vihaya staff only; test events skip the check, since their payments are not real.
Returns · 200
{ success, deleted }/api/v1/events/{id}/tracksList tracksEvery track on the event, in order.
Tracks are addressed by ARRAY INDEX, not by an id — they do not have one.
Returns · 200
{ success, data: Track[], count }/api/v1/events/{id}/tracksAdd a trackAppends a bookable session to the event.
| Field | Type | Notes |
|---|---|---|
titlereq | string | Session name. |
price | number | Rupees. 0 is free. |
capacity | number | Seats for this session. 0 means unlimited. |
date | string | Defaults to the parent event's date. |
Returns · 201
201 { success, data: { index, ... } }/api/v1/events/{id}/tracks/{index}Update a trackEdits one track by its zero-based index.
An out-of-range index returns 404 rather than creating a track.
Returns · 200
{ success, data: Track }/api/v1/events/{id}/registerRegister an attendeeFiles a registration. On a free event it confirms immediately. On a paid event this is a two-step call — see “Taking payment” above.
A paymentId alone is not proof of payment — it is verified against Razorpay and bound to both the order and the registration before anything is confirmed.
| Field | Type | Notes |
|---|---|---|
name | string | Attendee name, printed on the ticket. |
email | string | Where the ticket goes. Refused if the address provably cannot receive mail. |
phone | string | Contact number. |
customFields | object | Answers to the event's own questions, keyed by field name. |
teamMembers | array | For team events. Each member gets their own ticket and QR, so each email is validated too. |
promoCode | string | Applied server-side if valid and in date. |
registrationId | string | Step 2 only — the id returned by step 1. |
paymentId | string | Step 2 only — Razorpay payment id from checkout. |
orderId | string | Step 2 only — the order id returned by step 1. |
Returns · 201
Free: { success, registrationId }. Paid step 1: { orderId, registrationId, amount, currency, key }./api/v1/events/{id}/registrationsList registrationsThe attendee roster for one event.
This returns attendees' names and email addresses. Treat the response as personal data.
Returns · 200
{ success, data: Registration[], count }/api/v1/events/{id}/availabilitySeats remainingCapacity, seats taken and what is left — for the event, or for one track via `?subEventId=`. Per-tier counts come back too.
`remaining` is null when capacity is unlimited, never 0 — a `0` here genuinely means sold out. These are the same numbers the confirm transaction enforces, so a picker built on this can never promise a seat that is about to be refused. Reachable by a `checkout` key.
Returns · 200
{ success, data: { capacity, taken, remaining, soldOut, tiers[] } }/api/v1/events/{id}/analyticsEvent analyticsCounts and revenue without pulling the whole roster.
Prefer this over listing registrations when you only need a number.
Returns · 200
{ eventId, eventTitle, totalRegistrations, confirmedCount, pendingCount, cancelledCount, checkedInCount, totalRevenue, capacity, fillRate }/api/v1/events/{id}/broadcastEmail attendeesSends one message to an audience resolved from the event's own registrations. Previews by default; a real send takes two calls.
`to`, `emails`, `bcc`, `cc` and `recipients` are refused with 400. Recipients are always the event's own attendees — this is not a mail relay.
| Field | Type | Notes |
|---|---|---|
subjectreq | string | 200 characters or fewer. |
messagereq | string | 20,000 characters or fewer. Line breaks are preserved. |
audience | string | Which registrations to include. Defaults to confirmed. |
subEventId | string | Narrow to one track. |
dryRun | boolean | Defaults to TRUE. Only an explicit false sends. |
confirmedRecipientCount | number | Required when dryRun is false. Must equal the count the preview returned. |
Returns · 200
Preview: { dryRun: true, recipientCount, sampleRecipients }. Send: { sent, failed, recipientCount }./api/v1/memberships/plansList your plansEvery membership plan on this account, in this key's mode.
Returns · 200
{ success, data: Plan[], count }/api/v1/memberships/plansCreate a planDefines what a member pays. `fixed_term`: paid once, valid for `termMonths`. `recurring`: billed every period on UPI AutoPay (creates the matching plan at Razorpay).
⚠️ (Recurring plans) ₹15,000 IS A HARD CEILING, AND IT IS REFUSED HERE ON PURPOSE. A UPI AutoPay mandate can be registered up to ₹1,00,000, but only debits at or below ₹15,000 go through without the member authenticating EVERY time — which is not a subscription, it is a recurring reminder to pay. A plan priced above it registers perfectly at Razorpay and then fails as a product on the first real renewal, so this refuses it rather than letting you discover that later.
| Field | Type | Notes |
|---|---|---|
namereq | string | Members see this on their bank mandate. 80 characters or fewer. |
amountreq | number | Rupees (per period for recurring, once for fixed_term). Two decimal places at most. |
kind | "fixed_term" | "recurring" | Default recurring. |
termMonths | number | fixed_term only, required there: 1 to 120. 12 = a year, 48 = four years. |
period | "weekly" | "monthly" | "quarterly" | "yearly" | Recurring only, required there: how often it bills. |
description | string | Shown to the member. 500 characters. |
totalCount | number | Billing cycles before it ends. 0 or absent = until cancelled. |
Returns · 201
201 { success, data: Plan }/api/v1/memberships/subscribeEnrol a memberRecurring plan: creates the subscription a member authorises with a UPI AutoPay mandate. Fixed-term plan: creates a one-time Razorpay order (`orderId`, `amount`, `keyId`) to open Checkout on; paying again in the last 30 days or after expiry renews. Reachable by a `checkout` key.
⚠️ A 201 HERE IS NOT A MEMBER. Razorpay answers `created`: nothing is charged and nobody has joined until they approve the mandate in their UPI app, which arrives as `subscription.authenticated` on your webhook. Treat this response as "ask them to authorise", never as success — otherwise every person who opens the UPI app and changes their mind is recorded as a paying member. Enrolling the same email on the same plan twice returns the existing membership instead of registering a second mandate.
| Field | Type | Notes |
|---|---|---|
planIdreq | string | A plan on your own account, in the same mode as your key. |
namereq | string | The member. |
emailreq | string | Refused if it provably cannot receive mail — renewal notices are the only channel. |
phone | string | Optional contact number. |
Returns · 201
201 { success, pendingAuthorisation: true, membershipId, subscriptionId, keyId }/api/v1/memberships/{id}/cancelStop billing a memberCancels the mandate. By default at the end of the current billing period, so the member keeps the time they have paid for.
Cancelling twice is not an error — it returns `alreadyEnded: true` and changes nothing, because the money has already stopped, which is what the caller wanted. If Razorpay refuses the cancellation the local record is deliberately left ALONE and the call fails: telling you billing had stopped while the mandate kept debiting is the one lie this endpoint must never tell.
| Field | Type | Notes |
|---|---|---|
immediate | boolean | End it now instead — for a mistaken enrolment or a disputed charge. The member loses the remainder of the period they paid for. |
Returns · 201
{ success, data: Membership, message }/api/v1/memberships/membersList members / check oneYour members, in this key's mode. Pass `email` (and optionally `planId`) to check one person — read `goodStanding` on the row.
A fixed-term member stays `active` in storage after their date; `goodStanding` and `displayStatus: "expired"` are worked out when you ask. Not available to a `checkout` key — a roster is personal data. There is no API to add a member: a member pays through the join link or `/memberships/subscribe`. Only a Vihaya admin can add one by hand, from the dashboard; those rows carry `source: "manual"`.
| Field | Type | Notes |
|---|---|---|
planId | string | Only this plan. |
email | string | Only this member. |
Returns · 200
{ success, data: Membership[] (with goodStanding, displayStatus, validUntil), count }/api/v1/events/{id}/check-inLook a ticket upResolve a scanned QR WITHOUT admitting it: who is at the door, whether they may come in, and how many of their party have already arrived.
Use this for the operator's confirmation screen. Checking whether somebody is already in must never be the thing that marks them in — that is what this endpoint is for.
| Field | Type | Notes |
|---|---|---|
registrationIdreq | string | The value encoded in the QR (the booking id, or its `ticketCode` after a transfer). A JSON payload is accepted too. |
Returns · 200
{ success, data: { name, email, ticketType, admissible, checkedIn, arrived, expected, members[], refusalReason? } }/api/v1/events/{id}/check-inAdmit a ticketMarks a booking — or one member of a team booking — as arrived. Send `undo: true` to reverse it.
RE-SCANNING IS SAFE. A repeat scan returns 200 with `changed: false` and `alreadyCheckedIn: true`, writes nothing and does not move the timestamp — a gate re-scans constantly, so that is normal traffic, not an error. A refunded or cancelled ticket is refused with 409 and a `refusalReason` you can put on the screen. The OLD QR of a transferred ticket is refused with 409 and `reason: "transferred"`.
| Field | Type | Notes |
|---|---|---|
registrationIdreq | string | The QR payload — the booking id, or its `ticketCode` after a transfer. `qr` is accepted as an alias. |
memberIndex | number | Admit ONE member of a team booking, by position. Omit to admit the whole booking. |
undo | boolean | Reverse a check-in — for the wrong scan at a busy gate. |
Returns · 201
{ success, changed, alreadyCheckedIn, data: { ...status } }/api/v1/webhooksRead your webhookThe configured URL, whether it is enabled, and the event types it receives.
The signing secret is never returned here. It is shown once, on the PUT that created or rotated it.
Returns · 200
{ success, data: { url, enabled, secretSet, events[] } | null }/api/v1/webhooksSet or update itCreates the webhook, changes its URL, or pauses it. Returns the signing secret only when one is generated.
SAVE THE `secret` FROM THIS RESPONSE — it cannot be read back. The secret is always generated by us and never accepted from the caller: a caller-chosen one can be weak or reused, and the whole value of the signature is that only we and your receiver could have produced it.
| Field | Type | Notes |
|---|---|---|
url | string | https, public host. Optional when you are only toggling `enabled` on an existing hook. |
enabled | boolean | Defaults true. Set false to pause delivery without losing the configuration. |
rotateSecret | boolean | Issue a fresh signing secret. The old one stops verifying immediately. |
Returns · 200
{ success, data: {...}, secret? }/api/v1/webhooksStop deliveringRemoves the endpoint and its secret together.
Returns · 200
{ success, data: null }Machine-readable source: https://events.vihaya.app/api/v1/openapi.json. Import it into Postman, Insomnia or any OpenAPI 3.1 tool.