Vihaya Logo
Vihaya Events
Pricing
Sign in
Developer · API reference

Run ticketing from your own product.

A REST API for events, registrations and payments. Keep your own front end, your own branding and your own checkout — Vihaya handles the tickets, QR codes, gate scanning and payouts behind it.

Get an API key Read the quickstart

Building your own event site? See how Vihaya works as your backend

Base URL
https://events.vihaya.app/api/v1
Auth
x-api-key header
Format
JSON in, JSON out
SDKs
7 languages

On this page

  • Quickstart
  • Authentication
  • Test mode
  • Taking payment
  • SDKs
  • Identity
  • Events
  • Tracks (sessions)
  • Registrations & payment
  • Messaging
  • Memberships (recurring)
  • Door & check-in
  • Webhook management
  • Webhooks
  • QR codes
  • Your own email
  • Errors & limits
Quickstart

Your first call

  1. 1 Create a key at Developer → API keys. It is shown once.
  2. 2 Send it as x-api-key on every request.
  3. 3 Call /me to confirm which account it belongs to.
first-call.sh
curl https://events.vihaya.app/api/v1/me \
  -H "x-api-key: $VIHAYA_API_KEY"
Try it — right here

Paste a key and call GET /me against production. Real request, real response — nothing is simulated.

Your key goes straight from this browser to the Vihaya API on this same domain. It is never stored, never logged by this page, and never sent anywhere else — but it is still a live credential, so use your own and treat a shared screen accordingly.

Then create an event. It is always created as a draft — it has no public page until you publish it.

create-event.sh
curl -X POST https://events.vihaya.app/api/v1/events \
  -H "x-api-key: $VIHAYA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "title": "TechFest 2027",
    "description": "Two days of workshops and talks.",
    "startDate": "2027-03-01",
    "location": "Kollam",
    "capacity": 300,
    "price": 250
  }'

The date field is startDate, not date. The response echoes both, which misleads on a second read.

Authentication

API keys

Every request carries x-api-key. A key is bound to one account and can only ever see that account's events — there is no way to widen it, and no uid or email is ever read from a request body.

  • Revocation is immediate. A revoked key returns 403 on the next call.
  • Origin pinning is optional. Restrict a key to your domains and a request from anywhere else is refused. Server-to-server calls send no Origin and are unaffected.
  • Publishing needs organiser access. Check canPublish on /me; without it a publish returns 403 while everything else works.
  • Keys are stored hashed. We cannot show you a key again — rotate it instead.
Test mode

A sandbox, selected by your key

A whole sandbox account, selected by the key you call with. Build and run your whole integration — registration, payment, check-in, webhooks — without charging anyone, emailing anyone, or putting an event in front of the public.

vihaya_live_sk_

Real events, real payments, real ticket emails.

vihaya_test_sk_

Test events only. No gateway call, no email, no fee, no public page.

Why the mode is in the key, not a header. A header would let one credential act on real or fake data depending on a value your code controls — one forgotten header away from a test suite confirming real registrations, emailing real attendees and moving real money. Because the prefix carries it, a key is permanently one or the other, it is visible in every log and code review, and a test key pasted into production config fails loudly instead of quietly doing damage.

The rules

  • A test key can only act on TEST EVENTS. Create one by calling POST /events with the test key — it is stamped automatically.
  • A live key can never touch a test event, and a test key can never touch a live one. Both directions return 403 with an explanation.
  • GET /events returns only the events belonging to your key's mode, so test fixtures never appear in a live listing.
  • A test event can NEVER be published. It has no public page, appears in no search or listing, and cannot sell a real ticket.
  • Test events are fee-exempt, so they can never contribute to a payout, a revenue total or an organiser balance.

End to end in four calls

test-mode.sh
# 1. Mint a test key (dashboard, or POST /v1/keys with { "mode": "test" })
export VIHAYA_TEST_KEY=vihaya_test_sk_...

# 2. Everything below is fake. Nothing is charged, nobody is emailed.
curl -X POST https://events.vihaya.app/api/v1/events \
  -H "x-api-key: $VIHAYA_TEST_KEY" \
  -H "content-type: application/json" \
  -d '{ "title": "Fixture", "description": "Scratch event",
        "startDate": "2027-01-01", "price": 250 }'
# -> 201, isTestEvent: true, status: "draft" (always)

# 3. Register somebody. A PAID test event returns a REAL Razorpay order.
curl -X POST .../events/{id}/register \
  -H "x-api-key: $VIHAYA_TEST_KEY" \
  -d '{ "name": "Asha", "email": "asha@example.com" }'
# -> { orderId, registrationId, amount, currency, key: "rzp_test_..." }
#    Open Razorpay Checkout with that key, pay with a TEST card, then POST
#    again with { registrationId, orderId, paymentId } to confirm — the same
#    two calls as production.

# 4. Admit them at your own door.
curl -X POST .../events/{id}/check-in \
  -H "x-api-key: $VIHAYA_TEST_KEY" \
  -d '{ "registrationId": "..." }'
# -> { success: true, changed: true, data: { checkedIn: true, ... } }

Test mode is the real flow, on a test account. Test mode runs the REAL two-call flow against Vihaya's Razorpay TEST account. You get a real order, open real Razorpay Checkout with the `key` we return, pay with Razorpay's test cards or test UPI, and confirm with the payment id exactly as in production. The only difference is which Razorpay account it runs on — `rzp_test_` credentials cannot move real money. Your integration is therefore genuinely exercised rather than simulated, and the code path is identical to live.

Emails and webhooks

Nothing is emailed. Ticket delivery is off on every test event, and a broadcast always previews however you set `dryRun` — the addresses in your test data may well be real ones, and "it was only a test" is not something you can say to whoever received the mail. The preview still returns the true recipient count and sample, so the endpoint is fully testable.

Webhooks DO fire in test mode — that is the point. `registration.confirmed` is delivered and signed exactly as in production, so you can drive your receiver end to end without a real booking.

What it is not

  • It is not a separate database you can seed in bulk — create test events through the API like any other.
  • It runs on Vihaya's Razorpay test account, not yours. You are testing the integration, not your own gateway settlement.
  • Test events still appear in your own dashboard, badged as test. They are drafts, so nobody else can see them.
Registrations

Taking payment from your own front end

A free event is one call and it is confirmed immediately. A paid event is two, because the payment itself has to happen in the buyer's browser.

1Post the attendee — we return an order
step-1
POST https://events.vihaya.app/api/v1/events/{eventId}/register
{ "name": "Asha Menon", "email": "asha@example.com", "phone": "+919000000000" }

→ 200
{
  "orderId":        "order_TZud2hMOtfhq9f",
  "registrationId": "pNd1m0TRqN8Hx5a1QopI",
  "amount":         25000,            // paise
  "currency":       "INR",
  "key":            "rzp_live_..."    // public key — safe in a browser
}
2Confirm after Razorpay checkout
step-2
// 2. Your front end opens Razorpay Checkout with that key + orderId.
//    On success Razorpay hands you a razorpay_payment_id.

POST https://events.vihaya.app/api/v1/events/{eventId}/register
{
  "registrationId": "pNd1m0TRqN8Hx5a1QopI",
  "orderId":        "order_TZud2hMOtfhq9f",
  "paymentId":      "pay_XXXXXXXXXXXX"
}

→ confirmed. Ticket + QR issued.

A paymentId alone is never proof of payment. We verify it against Razorpay and bind it to both the order and the registration before anything is confirmed — a forged id is refused and the registration stays pending.

Settlement: payments are collected into Vihaya's Razorpay account, not yours. The organiser withdraws from the dashboard and funds land by UPI in about two working days. Ask us before building if you need direct settlement.

Libraries

Official SDKs — 7 languages

All seven wrap the same four calls and the same base URL, so the shape is identical whichever you pick.

npm install vihaya-sdk
v1.5.0vihaya-sdkRegistrySource
vihaya-sdk
import { VihayaClient } from 'vihaya-sdk';

const vihaya = new VihayaClient(process.env.VIHAYA_API_KEY);

const events = await vihaya.events.list();
const result = await vihaya.events.register(eventId, {
  name: 'Asha Menon',
  email: 'asha@example.com',
  phone: '+919000000000',
});

Wrapped by the SDKs

  • events.list() → GET /events
  • events.get(id) → GET /events/{id}
  • events.register(id, data) → POST /events/{id}/register — both phases
  • payments.verify(data) → POST /payments/verify

Use REST directly for

  • Creating, updating, publishing or deleting an event
  • Tracks (list / add / update)
  • Listing registrations, and analytics
  • Broadcast email
  • whoami (/me)
Endpoints

Identity

Which account does this key belong to, and what may it do?

GET/api/v1/meWho am I

Resolves the key to an account: name, email, phone on file, organiser status, whether it can publish, and how many events it owns.

Returns

{ userId, email, name, phone, isOrganizer, canPublish, events: { total, draft, published }, apiKeyId }

Start here. `canPublish` is false until the account has been granted organiser access — publishing will 403 until then.

Endpoints

Events

Create and manage events. A new event is always a draft until you publish it.

GET/api/v1/eventsList your events

Every event this key's account owns, newest first, undated events last.

Returns

{ success, data: Event[] }

Scoped to the caller. You can never see another organiser's events.

POST/api/v1/eventsCreate an event

Creates an event. It is always created as a DRAFT — it has no public page and cannot sell until you publish it.

FieldTypeNotes
titlereqstringShown publicly. The URL slug is derived from it.
descriptionreqstringShown on the event page.
startDatereqISO 8601e.g. "2027-03-01". Not `date` — that is only echoed back in the response.
endDateISO 8601Must be on or after startDate.
locationstringVenue or city.
capacitynumber0 means unlimited.
pricenumberRupees. 0 or omitted makes the event free.
sendTicketEmailsbooleanACCEPTED BUT IGNORED — see the gotcha. Automatic ticket email stays ON.
specialPricesarrayTicket tiers. Set `membersOnly: true` on one to restrict it to people holding a live membership on this account.

Returns

201 { success, data: Event }

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.

GET/api/v1/events/{id}Get one event

The full event document, including its tracks.

Returns

{ success, data: Event }
PATCH/api/v1/events/{id}Update an event

Change any editable field. Send `{ "status": "published" }` to put it on sale, `"draft"` to take it down.

FieldTypeNotes
status"draft" | "published"Publishing requires organiser access — check `canPublish` on /me first.

Returns

{ success, data: Event }

Publishing is refused with 403 if the account has no organiser grant. Unpublishing is never blocked.

DELETE/api/v1/events/{id}Delete an event

Permanently removes an event you own, and every registration under it.

Returns

{ success, deleted }

⚠️ 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 holding PAID registrations is refused with 409 unless you add `?force=true`; test events skip that check, since their payments are not real.

Endpoints

Tracks (sessions)

A festival with several bookable sessions. Each track carries its own date, price and capacity.

GET/api/v1/events/{id}/tracksList tracks

Every track on the event, in order.

Returns

{ success, data: Track[], count }

Tracks are addressed by ARRAY INDEX, not by an id — they do not have one.

POST/api/v1/events/{id}/tracksAdd a track

Appends a bookable session to the event.

FieldTypeNotes
titlereqstringSession name.
pricenumberRupees. 0 is free.
capacitynumberSeats for this session. 0 means unlimited.
dateISO 8601Defaults to the parent event's date.

Returns

201 { success, data: { index, ... } }
PATCH/api/v1/events/{id}/tracks/{index}Update a track

Edits one track by its zero-based index.

Returns

{ success, data: Track }

An out-of-range index returns 404 rather than creating a track.

Endpoints

Registrations & payment

Take bookings from your own front end. This is the headless path — see the walkthrough above.

POST/api/v1/events/{id}/registerRegister an attendee

Files a registration. On a free event it confirms immediately. On a paid event this is a two-step call — see “Taking payment” above.

FieldTypeNotes
namestringAttendee name, printed on the ticket.
emailstringWhere the ticket goes. Refused if the address provably cannot receive mail.
phonestringContact number.
customFieldsobjectAnswers to the event's own questions, keyed by field name.
teamMembersarrayFor team events. Each member gets their own ticket and QR, so each email is validated too.
promoCodestringApplied server-side if valid and in date.
registrationIdstringStep 2 only — the id returned by step 1.
paymentIdstringStep 2 only — Razorpay payment id from checkout.
orderIdstringStep 2 only — the order id returned by step 1.

Returns

Free: { success, registrationId }. Paid step 1: { orderId, registrationId, amount, currency, key }.

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.

GET/api/v1/events/{id}/registrationsList registrations

The attendee roster for one event.

Returns

{ success, data: Registration[], count }

This returns attendees' names and email addresses. Treat the response as personal data.

GET/api/v1/events/{id}/availabilitySeats remaining

Capacity, seats taken and what is left — for the event, or for one track via `?subEventId=`. Per-tier counts come back too.

Returns

{ success, data: { capacity, taken, remaining, soldOut, tiers[] } }

`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.

GET/api/v1/events/{id}/analyticsEvent analytics

Counts and revenue without pulling the whole roster.

Returns

{ eventId, eventTitle, totalRegistrations, confirmedCount, pendingCount, cancelledCount, checkedInCount, totalRevenue, capacity, fillRate }

Prefer this over listing registrations when you only need a number.

Endpoints

Messaging

Email an event's own attendees. You can never name a recipient.

POST/api/v1/events/{id}/broadcastEmail attendees

Sends one message to an audience resolved from the event's own registrations. Previews by default; a real send takes two calls.

FieldTypeNotes
subjectreqstring200 characters or fewer.
messagereqstring20,000 characters or fewer. Line breaks are preserved.
audiencestringWhich registrations to include. Defaults to confirmed.
subEventIdstringNarrow to one track.
dryRunbooleanDefaults to TRUE. Only an explicit false sends.
confirmedRecipientCountnumberRequired when dryRun is false. Must equal the count the preview returned.

Returns

Preview: { dryRun: true, recipientCount, sampleRecipients }. Send: { sent, failed, recipientCount }.

`to`, `emails`, `bcc`, `cc` and `recipients` are refused with 400. Recipients are always the event's own attendees — this is not a mail relay.

Endpoints

Memberships (recurring)

Charge members on a repeating UPI AutoPay mandate. A first slice — see the limits below before you build on it.

GET/api/v1/memberships/plansList your plans

Every membership plan on this account, in this key's mode.

Returns

{ success, data: Plan[], count }
POST/api/v1/memberships/plansCreate a plan

Defines what a member is charged and how often. Creates the matching plan at Razorpay.

FieldTypeNotes
namereqstringMembers see this on their bank mandate. 80 characters or fewer.
amountreqnumberRupees per period. Two decimal places at most.
periodreq"weekly" | "monthly" | "quarterly" | "yearly"How often it bills.
descriptionstringShown to the member. 500 characters.
totalCountnumberBilling cycles before it ends. 0 or absent = until cancelled.

Returns

201 { success, data: Plan }

⚠️ ₹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.

POST/api/v1/memberships/subscribeEnrol a member

Creates the subscription a member then authorises with a UPI AutoPay mandate. Reachable by a `checkout` key.

FieldTypeNotes
planIdreqstringA plan on your own account, in the same mode as your key.
namereqstringThe member.
emailreqstringRefused if it provably cannot receive mail — renewal notices are the only channel.
phonestringOptional contact number.

Returns

201 { success, pendingAuthorisation: true, membershipId, subscriptionId, keyId }

⚠️ 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.

POST/api/v1/memberships/{id}/cancelStop billing a member

Cancels the mandate. By default at the end of the current billing period, so the member keeps the time they have paid for.

FieldTypeNotes
immediatebooleanEnd it now instead — for a mistaken enrolment or a disputed charge. The member loses the remainder of the period they paid for.

Returns

{ success, data: Membership, message }

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.

Endpoints

Door & check-in

Run your own gate. Scan a ticket, admit it, undo a mistake — the same rules the Vihaya check-in desk uses.

GET/api/v1/events/{id}/check-inLook a ticket up

Resolve 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.

FieldTypeNotes
registrationIdreqstringThe value encoded in the QR (the booking id, or its `ticketCode` after a transfer). A JSON payload is accepted too.

Returns

{ success, data: { name, email, ticketType, admissible, checkedIn, arrived, expected, members[], refusalReason? } }

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.

POST/api/v1/events/{id}/check-inAdmit a ticket

Marks a booking — or one member of a team booking — as arrived. Send `undo: true` to reverse it.

FieldTypeNotes
registrationIdreqstringThe QR payload — the booking id, or its `ticketCode` after a transfer. `qr` is accepted as an alias.
memberIndexnumberAdmit ONE member of a team booking, by position. Omit to admit the whole booking.
undobooleanReverse a check-in — for the wrong scan at a busy gate.

Returns

{ success, changed, alreadyCheckedIn, data: { ...status } }

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"`.

Endpoints

Webhook management

Configure where we push booking notifications, without clicking through the dashboard.

GET/api/v1/webhooksRead your webhook

The configured URL, whether it is enabled, and the event types it receives.

Returns

{ success, data: { url, enabled, secretSet, events[] } | null }

The signing secret is never returned here. It is shown once, on the PUT that created or rotated it.

PUT/api/v1/webhooksSet or update it

Creates the webhook, changes its URL, or pauses it. Returns the signing secret only when one is generated.

FieldTypeNotes
urlstringhttps, public host. Optional when you are only toggling `enabled` on an existing hook.
enabledbooleanDefaults true. Set false to pause delivery without losing the configuration.
rotateSecretbooleanIssue a fresh signing secret. The old one stops verifying immediately.

Returns

{ success, data: {...}, secret? }

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.

DELETE/api/v1/webhooksStop delivering

Removes the endpoint and its secret together.

Returns

{ success, data: null }
Webhooks

Keep your system in sync

We POST to an endpoint you own the moment something happens, so you do not have to poll.

Configure it two ways. In the dashboard at /profile/integrations, or over the API with GET / PUT / DELETE /api/v1/webhooks. The signing secret is generated by us, returned once, and never readable again. One endpoint per organiser account, covering every event they own.

  • Must be https. Plain http is refused.
  • Must resolve to a public host — loopback, link-local and RFC1918 literals are refused, because we POST to this from our own servers.
  • Redirects are NOT followed. A 302 is the standard way to walk a validated public URL round to an internal one.

Events

There is exactly one. Subscribing to any other name means nothing will ever arrive.

registration.confirmed

A booking is confirmed and its ticket issued — after payment has been verified, or immediately on a free event.

Fired last on the confirmation path, after the ticket email. It is the signal to do your own post-booking work.

The payload

registration.confirmed
POST <your endpoint>
X-Vihaya-Event:     registration.confirmed
X-Vihaya-Timestamp: 1789430651760          // unix MILLISECONDS, not seconds
X-Vihaya-Signature: sha256=<hex>           // note the prefix
Content-Type:       application/json

{
  "type":      "registration.confirmed",
  "createdAt": "2027-09-01T10:04:11.760Z",
  "data": {
    "registrationId": "pNd1m0TRqN8Hx5a1QopI",  // the booking — stable for its whole life
    "ticketCode":     "pNd1m0TRqN8Hx5a1QopI",  // ← this IS the QR payload (changes on a transfer)
    "eventId":        "8fK2...",
    "eventTitle":     "TechFest 2027",
    "name":           "Asha Menon",
    "email":          "asha@example.com",
    "phone":          "+919000000000",
    "ticketType":     "VIP",        // null if the event has no tiers
    "subEventId":     null,         // the track, when one was booked
    "quantity":       1,            // seats this ONE booking admits
    "amountPaid":     25000,
    "currency":       "INR",
    "paymentStatus":  "paid"        // or "free"
  }
}

Verifying the signature

verify-webhook.js
// The signature covers `${timestamp}.${rawBody}` — the RAW bytes,
// before any JSON.parse. Parsing and re-stringifying changes them.
import crypto from 'crypto';

const timestamp = req.headers['x-vihaya-timestamp'];

// ⚠️ The header value is PREFIXED. Strip it before comparing, or the two
// buffers are different lengths and timingSafeEqual THROWS.
const signature = String(req.headers['x-vihaya-signature']).replace(/^sha256=/, '');

const expected = crypto
  .createHmac('sha256', process.env.VIHAYA_WEBHOOK_SECRET)   // sha256, NOT sha512
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');

const ok = crypto.timingSafeEqual(
  Buffer.from(signature, 'hex'),
  Buffer.from(expected, 'hex'),
);
if (!ok) return res.status(401).end();

// Reject anything older than five minutes.
if (Date.now() - Number(timestamp) > 5 * 60_000) return res.status(401).end();

Delivery guarantees

RetriesNone. One attempt per event.
Timeout5 seconds, then aborted.
On failureLogged on our side and dropped. The attendee still gets their ticket.
OrderingNot guaranteed. Use `createdAt` if order matters.
Response bodyNever read. Returning data to us achieves nothing.
Replay protectionThe timestamp is inside the signed payload — reject a stale `t` yourself.

A dropped delivery is gone. Nothing is retried, so reconcile against GET /events/{id}/registrations rather than treating the webhook as a durable queue.

Tickets

What is in the QR code

The registration id, as a plain string. Example: pNd1m0TRqN8Hx5a1QopI No URL, no JSON, no signature — just the id.

So you never need an endpoint from us to draw a ticket. The moment you have the registration id you have the whole payload, and you can render the QR yourself at any size, in any format, inside your own ticket design. There is no endpoint that returns a QR image, and there does not need to be.

Where to get the id

  • registrationId from POST /events/{id}/register
  • registrationId from the registration.confirmed webhook
  • id on each row of GET /events/{id}/registrations

Rendering it

render-qr.js
// Any QR library. This is exactly what our own mailer does.
import QRCode from 'qrcode';

const png = await QRCode.toBuffer(registrationId, {
  type: 'png', width: 480, margin: 1, errorCorrectionLevel: 'M',
});

The scanner additionally parses `{"eventId":"…","registrationId":"…"}` if you would rather encode JSON. A bare id is what Vihaya itself emits.

Team bookings issue one ticket per member, each with its OWN id. The webhook carries the primary registration id; pull the roster to get each member's.

Recipe

Send your own ticket email

Vihaya’s ticket email is not customisable — but you do not have to use it. Everything needed to send your own already crosses the wire, so it is your template, your domain and your sender reputation.

1Point a webhook at your server

Set the URL and secret at /profile/integrations, then verify the signature on every delivery (see above).

2Read the attendee off the payload

`name`, `email`, `eventTitle`, `ticketType` and `quantity` are all in `data` — no follow-up call needed for a standard ticket.

3Render the QR yourself

Encode `data.ticketCode` with any QR library (it equals `data.registrationId` until a ticket is transferred). It scans in the Vihaya check-in app exactly like ours does, because it is the same payload.

4Send from your own domain

Your ESP, your template, your branding. Nothing in this step touches Vihaya.

5Ask support to switch OUR email off

This is the one step you cannot self-serve. `sendTicketEmails` is fixed in Firestore rules so an organiser cannot silence their own attendees' tickets by accident — support flips it. Until then the attendee gets both emails.

Switching our delivery off does NOT reduce your platform fee today. The delivery slice of the rate is currently 0% — the published rate and the platform's own rate are the same number — so there is nothing to give back. Do not plan around a discount.

Reference

Errors and rate limits

Every failure returns the same shape:

error
{ "error": "A sentence describing what went wrong." }
400The request is wrong — a missing field, a bad date, or a recipient list on a broadcast.
401No `x-api-key` header, or the key is not recognised.
403The key is valid but not allowed: revoked, wrong Origin, not your event, or publishing without organiser access.
404No such event, or a track index that does not exist.
409The world changed under you — currently only a broadcast whose audience moved since your preview.
413Too many recipients for one broadcast. Narrow it by track or audience.
429Rate limited. Back off and retry.
500 / 502 / 503Our side, or the payment gateway. Safe to retry an idempotent read.

Rate limits

ScopeLimitPer
List events (GET /events)120 / minuteper API key
List events (GET /events)240 / minuteper IP address
Get one event (GET /events/{id})300 / minuteper API key
Get one event (GET /events/{id})600 / minuteper IP address
Broadcast preview60 / minuteper API key
Broadcast send6 / minuteper API key
MCP tool calls60 / minuteper API key
Everything elseNo hard limit todayBe reasonable; this may change

Every endpoint on this page was driven against production on 2026-09-09. Something missing or wrong? Tell us.

§07 · Colophon
Vihaya Logo
Vihaya Events

The affordable ticketing stack for every kind of event in India. Concerts, fests, weddings, hackathons & more.

Kerala, INsupport@vihaya.app
What's new

Product updates, event stories, and build notes.

Read the latest updates →

Product

  • Event ticketing
  • Browse events
  • Event registration
  • AI form builder (coming soon)
  • Analytics
  • Integrations
  • Pricing

Company

  • About
  • Blog
  • News
  • Press kit

Compare

  • vs BookMyShow
  • vs MakeMyPass
  • vs KonfHub
  • vs Townscript

Use cases

  • All event types
  • Ticketing by city
  • College fest ticketing
  • Hackathon registration
  • Concert ticketing
  • Conference ticketing
  • Wedding registration

Support

  • Contact
  • Help center
  • Getting started
  • Documentation
  • Build on our API
  • FAQs
  • Report an issue
  • Sitemap

Legal

  • Terms
  • Privacy
  • Refunds
  • Shipping
EcosystemVihayaEventsResubyEduVihaya AI

© 2026 Vihaya Events. All rights reserved.

Built in IndiaShips weekly