# API Keys (/docs/api-keys) Manage API keys in the [dashboard](/dashboard/api-keys). Use a separate key for each application and environment. ## Create a key [#create-a-key] Select **New key**, then give it a name such as `checkout-production`. Copy the secret into your server’s `EVENTLAYER_KEY` environment variable. The secret is shown only once. ## Rotate a key [#rotate-a-key] 1. Create a replacement key in the dashboard. 2. Update your application’s server environment and deploy it. 3. Confirm requests work, then delete the old key. ## Revoke a key [#revoke-a-key] Delete it from **API Keys** in the dashboard. Requests using that key will receive `401 Unauthorized`. If a key is lost or exposed, replace it. See [authentication](/docs/authentication) to use your key in the SDK or HTTP requests. --- # API reference (/docs/api-reference) The interactive reference lists Eventlayer’s HTTP operations, request bodies and response schemas. [Start with the quickstart](/docs/quickstart) if you are making your first call. ## Authentication [#authentication] Use your server-side API key for API requests. Manage keys and webhook endpoints in the dashboard. Incoming webhooks use a separate signing secret. See [authentication](/docs/authentication). ## SDK and HTTP field names [#sdk-and-http-field-names] SDK input uses camelCase, such as `attendeeId` and `startAt`. HTTP request bodies use fields such as `attendee_id` and `start_at`. Resource responses retain the HTTP field names. Examples label their language and execution context. ## Handle the response [#handle-the-response] The SDK returns `{ data, error, response }`. Inspect `error` before using `data`. Network failures throw. Raw HTTP responses have their own status and response envelope; the interactive reference documents those schemas. Use [errors and request IDs](/docs/errors), [pagination](/docs/pagination) and [retries and recovery](/docs/retries) when building your integration. --- # Apple Wallet Templates (/docs/apple-wallet-templates) Templates control the colours, text and images on your Apple Wallet passes. Use the [pass designer](/dashboard/templates) for a live preview, or create a template through the API. ## Create a template [#create-a-template] ```ts const { data: template, error } = await eventlayer.appleWalletTemplates.create({ backgroundColor: "rgb(60, 60, 60)", foregroundColor: "rgb(255, 255, 255)", logoText: "After Hours", }); if (error) throw error; ``` ## Apply it to an event [#apply-it-to-an-event] ```ts const { error } = await eventlayer.events.update({ eventId: "ev_123", appleWalletTemplateId: template.id, }); if (error) throw error; ``` Now [generate a Wallet pass](/docs/guides/apple-wallet) for an assigned ticket. ## Override it for a ticket type [#override-it-for-a-ticket-type] Assign `appleWalletTemplateId` with `eventlayer.ticketTypes.update()` to give a type its own design. Ticket-type fields take precedence; null fields fall back to the event template. Set an assignment to `null` to remove it. ## Upload an image [#upload-an-image] Pass your uploaded file to the SDK: ```ts const { error } = await eventlayer.appleWalletTemplates.uploadImage({ templateId: "awt_123", imageType: "hero", file, contentType: "image/png", }); if (error) throw error; ``` ## Template properties [#template-properties] | Property | Type | Description | | --------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | | `background_color` | `string \| null` | Background color in CSS format (e.g. `rgb(60, 60, 60)`). | | `foreground_color` | `string \| null` | Text color in CSS format (e.g. `rgb(255, 255, 255)`). | | `label_color` | `string \| null` | Label color in CSS format (e.g. `rgb(200, 200, 200)`). | | `logo_text` | `string \| null` | Text displayed next to the pass logo. | | `header_text` | `string \| null` | Text displayed in the header row of the pass. | | `fine_print` | `string \| null` | Fine print text shown on the back of the pass. | | `sharing_prohibited` | `boolean \| null` | Prevents sharing the pass via Wallet's sharing feature. | | `grouping_identifier` | `string \| null` | Groups related passes together in Wallet. | | `show_barcode` | `boolean \| null` | Whether the QR code barcode is shown on the pass. Defaults to `true`. Set to `false` to hide it. | The template values are written into the pass JSON during generation. Apple requires `appLaunchURL` to be paired with an associated App Store app. Contact Eventlayer support to enable this integration for your account. If it is not enabled, Eventlayer safely omits `appLaunchURL` from the pass. The response also includes `hero_image_url`, `logo_image_url`, and `background_image_url`. These are read-only: they are set by the image upload endpoint below, not via `create` or `update`. ## Image types [#image-types] Images are uploaded as `multipart/form-data` to `POST /v1/apple-wallet-templates/{id}/images` (fields: `image_type` and `file`). Uploads are validated before storage: formats PNG, JPEG, or WebP, max 10MB, and within the maximum dimensions for the slot. | Image Type | Pass File | Purpose | Max Dimensions | | ------------ | ---------------- | ------------------------------------------- | -------------- | | `hero` | `strip.png` | Hero image displayed on the pass strip. | 2488 x 840 | | `logo` | `logo.png` | Logo image displayed next to the logo text. | 640 x 220 | | `background` | `background.png` | Background image behind the pass content. | 2488 x 2160 | See the [API reference](/docs/api-reference) for listing, updating and deleting templates. --- # Attendees (/docs/attendees) An attendee is a person registered for one event. Tickets describe their access; an attendee can have more than one ticket. ## Register an attendee with a ticket [#register-an-attendee-with-a-ticket] ```ts const { data: attendee, error } = await eventlayer.attendees.create({ eventId: "ev_123", name: "Maya Chen", email: "maya@example.com", createTicket: true, }); if (error) throw error; console.log(attendee.id, attendee.ticket?.id); ``` The attendee and assigned ticket are created together. If either cannot be created, neither is saved. `createTicket: true` uses the event’s only ticket type. When there are multiple types, choose one with `createTicket: { ticketTypeId: "tt_123" }`. ## Register without a ticket [#register-without-a-ticket] Omit `createTicket` to create only the attendee. You can [issue or assign a ticket](/docs/tickets) later. ## List attendees [#list-attendees] ```ts const { data: page, error } = await eventlayer.attendees.list({ eventId: "ev_123", }); if (error) throw error; console.log(page.items); ``` See [pagination](/docs/pagination) for more results, or [connect a registration form](/docs/guides/website-registration) for a complete flow. Update and delete operations are in the [API reference](/docs/api-reference). --- # Authentication (/docs/authentication) Use an API key to make requests from your server. [Create a key in the dashboard](/dashboard/api-keys). ## TypeScript [#typescript] Set `EVENTLAYER_KEY` in your server environment: ```bash EVENTLAYER_KEY=sk_your_key_here ``` The SDK reads it automatically: ```ts import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); ``` You can also pass your key directly: `new Eventlayer("sk_your_key_here")`. Keep keys out of browser and mobile application code. ## HTTP [#http] Send your key in the `Authorization` header: ```bash curl https://api.eventlayer.dev/v1/event \ -H "Authorization: Bearer $EVENTLAYER_KEY" ``` ## Incoming webhooks [#incoming-webhooks] Use your endpoint’s signing secret to [verify webhooks](/docs/guides/webhooks). A signing secret verifies incoming events; it does not authenticate API requests. [Issue your first ticket](/docs/quickstart) or [manage your API keys](/docs/api-keys). --- # Check-in (/docs/check-in) Your scanner reads a ticket. Your backend asks Eventlayer to record admission. The ticket state on the server determines whether the operation succeeds. ## Make the transition [#make-the-transition] Check-in requires an assigned ticket and an active ticket type. Use a fresh, disposable ticket: check-in cannot be reversed. ```ts title="check-in.ts · server only · API key" const { data: ticket, error } = await eventlayer.tickets.checkIn({ ticketId: "tkt_from_your_scanner", }); if (error) { console.error(error.code, error.requestId); } else { console.log(ticket.status); // "checked_in" } ``` One successful admission changes the ticket to `checked_in`. A second scan receives `TICKET_ALREADY_CHECKED_IN`; it must not show another admission success. ## Explain each result [#explain-each-result] | Result | Meaning | Your scanner should | | ---------------------------------- | -------------------------------------------------- | -------------------------- | | `200`, ticket is `checked_in` | This request recorded admission | Show success | | `409`, `TICKET_ALREADY_CHECKED_IN` | The ticket was already used | Ask staff to review | | `409`, `FAILED_PRECONDITION` | Current ticket or ticket-type state prevents entry | Show the reason | | `404`, `NOT_FOUND` | No accessible ticket was found | Reject the lookup | | No response | Outcome is unknown | Reconcile before admitting | Two scanners can read the same assigned ticket at once. Only one can complete its first check-in. A prior lookup is not authority to admit a guest. Your backend also authenticates staff and checks that the ticket belongs at this entry point. An API key grants workspace access; it does not implement your staff permissions or venue policy. --- # Errors (/docs/errors) Eventlayer uses standard HTTP status codes and a stable error code for each kind of failure. Every error code always has the same HTTP status and title. ## Error schema [#error-schema] Errors use an `error` object inside the standard API response envelope: ```json { "status": 422, "error": { "type": "https://eventlayer.dev/docs/errors#validation-failed", "title": "Request validation failed", "status": 422, "code": "VALIDATION_FAILED", "detail": "One or more fields are invalid.", "requestId": "req_01k2example", "fields": [ { "field": "endAt", "message": "The end time must be after the start time." } ] } } ``` | Field | Type | Description | | ----------- | ------ | -------------------------------------------------------------------------- | | `type` | string | Stable URI identifying the problem category. | | `title` | string | Stable summary associated with the error code. | | `status` | number | HTTP status associated with the error code. | | `code` | string | Stable value for programmatic error handling. | | `detail` | string | Explanation specific to this occurrence. | | `requestId` | string | Identifier to include when contacting support. | | `fields` | array | Fields related to the failure. Empty when the error is not field-specific. | Branch on `code`, `status`, and `fields[].field`. Treat `title`, `detail`, and `fields[].message` as human-readable text that may improve over time. ## Handling errors with TypeScript [#handling-errors-with-typescript] The TypeScript SDK returns API failures as data rather than throwing them: ```ts title="errors.ts · server only" const { data: ticket, error, response, } = await eventlayer.tickets.checkIn({ ticketId: "tkt_123" }); if (error) { switch (error.code) { case "TICKET_ALREADY_CHECKED_IN": // This operation was already completed. break; case "VALIDATION_FAILED": for (const field of error.fields) { console.error(field.field, field.message); } break; case "RATE_LIMIT_EXCEEDED": console.error(response.headers.get("retry-after")); break; default: console.error(error.requestId, error.detail); } } else { console.log(ticket.status); } ``` Network failures where no HTTP response is received still throw. ## Error reference [#error-reference] ### `INVALID_REQUEST` [#invalid_request] * **Status:** `400 Bad Request` * **Message:** The request could not be parsed or understood. * **Suggested action:** Check the request syntax, JSON body, and query-string encoding before retrying. ### `AUTHENTICATION_REQUIRED` [#authentication_required] * **Status:** `401 Unauthorized` * **Message:** The operation requires authentication using one of its supported authentication schemes. * **Suggested action:** Supply your API key in the Authorization header. See [authentication](/docs/authentication). ### `INVALID_API_KEY` [#invalid_api_key] * **Status:** `401 Unauthorized` * **Message:** The supplied API key is invalid, expired, or revoked. * **Suggested action:** Check the `Authorization: Bearer` header or create a replacement key. See [Authentication](/docs/authentication). ### `PASS_TOKEN_INVALID` [#pass_token_invalid] * **Status:** `401 Unauthorized` * **Message:** The supplied Apple Wallet pass token is invalid. * **Suggested action:** Generate a new signed pass URL instead of modifying or reusing the invalid token. ### `PASS_TOKEN_EXPIRED` [#pass_token_expired] * **Status:** `401 Unauthorized` * **Message:** The supplied Apple Wallet pass token has expired. * **Suggested action:** Generate a new signed pass URL and retry the download. ### `NOT_FOUND` [#not_found] * **Status:** `404 Not Found` * **Message:** The directly addressed resource could not be found. * **Suggested action:** Verify the resource ID and account scope. Eventlayer may return the same error when a resource exists outside the authenticated account. ### `ROUTE_NOT_FOUND` [#route_not_found] * **Status:** `404 Not Found` * **Message:** The requested API route does not exist. * **Suggested action:** Check the base URL, API path, and SDK version. ### `METHOD_NOT_ALLOWED` [#method_not_allowed] * **Status:** `405 Method Not Allowed` * **Message:** The HTTP method is not supported by the requested route. * **Suggested action:** Use the method documented in the API reference or call the corresponding SDK resource method. ### `FAILED_PRECONDITION` [#failed_precondition] * **Status:** `409 Conflict` * **Message:** The target resource does not satisfy a prerequisite for the operation. * **Suggested action:** Read `detail` and `fields`, update the resource state or configuration, and retry. For example, an event must have a ticket type before tickets can be issued. ### `TICKET_ALREADY_CHECKED_IN` [#ticket_already_checked_in] * **Status:** `409 Conflict` * **Message:** The ticket has already been checked in. * **Suggested action:** Treat the check-in as already completed or show the existing check-in state. Do not repeatedly retry the mutation. ### `UNSUPPORTED_MEDIA_TYPE` [#unsupported_media_type] * **Status:** `415 Unsupported Media Type` * **Message:** The request uses a content type that the endpoint does not accept. * **Suggested action:** Use the `Content-Type` documented for the endpoint. JSON requests normally use `application/json`; image uploads use multipart form data. ### `VALIDATION_FAILED` [#validation_failed] * **Status:** `422 Unprocessable Content` * **Message:** One or more submitted fields or references are invalid for the operation. * **Suggested action:** Use the `fields` array to associate each message with the corresponding request field, correct the values, and retry. ### `PAYLOAD_TOO_LARGE` [#payload_too_large] * **Status:** `413 Content Too Large` * **Message:** The request or uploaded image exceeds the permitted size. * **Suggested action:** Reduce or compress the payload before retrying. ### `RATE_LIMIT_EXCEEDED` [#rate_limit_exceeded] * **Status:** `429 Too Many Requests` * **Message:** The API key has exceeded its request rate. * **Suggested action:** Respect the `Retry-After` response header and reduce concurrency. See [Rate Limits](/docs/rate-limits). ### `REQUEST_TIMEOUT` [#request_timeout] * **Status:** `408 Request Timeout` * **Message:** Eventlayer could not complete the request within the permitted time. * **Suggested action:** A timeout can leave the outcome unknown. See [retries and recovery](/docs/retries) before repeating a write. ### `INTERNAL_SERVER_ERROR` [#internal_server_error] * **Status:** `500 Internal Server Error` * **Message:** Eventlayer could not complete the request because of an unexpected server failure. * **Suggested action:** Retry with backoff. If the problem continues, contact support and include `requestId`. ## Status code summary [#status-code-summary] | Status | Codes | | ------ | ---------------------------------------------------------------------------------------- | | `400` | `INVALID_REQUEST` | | `401` | `AUTHENTICATION_REQUIRED`, `INVALID_API_KEY`, `PASS_TOKEN_INVALID`, `PASS_TOKEN_EXPIRED` | | `404` | `NOT_FOUND`, `ROUTE_NOT_FOUND` | | `405` | `METHOD_NOT_ALLOWED` | | `408` | `REQUEST_TIMEOUT` | | `409` | `FAILED_PRECONDITION`, `TICKET_ALREADY_CHECKED_IN` | | `413` | `PAYLOAD_TOO_LARGE` | | `415` | `UNSUPPORTED_MEDIA_TYPE` | | `422` | `VALIDATION_FAILED` | | `429` | `RATE_LIMIT_EXCEEDED` | | `500` | `INTERNAL_SERVER_ERROR` | ## Next steps [#next-steps] --- # Event time (/docs/event-time) After Hours starts at **19:00 in Sydney on 7 November 2026**. That corresponds to **08:00 UTC**. Store the timezone of the event alongside the instant so your product can display the intended venue time. ## Send an instant and a timezone [#send-an-instant-and-a-timezone] The SDK accepts `startAt` and `endAt` with `utc` and `timezone` fields. Raw HTTP uses `start_at` and `end_at`. Do not send the returned `local` field as input. ```ts title="event-time.ts · server only" const { data: event, error } = await eventlayer.events.create({ name: "After Hours", startAt: { utc: "2026-11-07T08:00:00Z", timezone: "Australia/Sydney", }, }); if (error) throw error; ``` The response includes the local representation: ```json title="Response excerpt · start_at" { "utc": "2026-11-07T08:00:00Z", "timezone": "Australia/Sydney", "local": "2026-11-07T19:00:00" } ``` ## Collect local time carefully [#collect-local-time-carefully] A datetime input without a timezone does not identify an instant. When your product collects a venue-local time, also collect the IANA timezone and resolve that pair to UTC before calling the API. The dashboard provides this conversion when you configure an event there. Daylight-saving transitions can make a local time ambiguous or nonexistent. Ask the organiser to resolve that choice; do not silently interpret it in the browser’s timezone or append `Z` to a local datetime. ## Display the event’s intended time [#display-the-events-intended-time] Use the returned `local` value for the venue-local representation, or format `utc` with `timezone` explicitly. Label any attendee-local conversion so people know which timezone they are seeing. See the [API reference](/docs/api-reference) for the complete event schema. --- # Events (/docs/events) An event holds its attendees, ticket types and tickets. ## Create an event [#create-an-event] ```ts const { data: event, error } = await eventlayer.events.create({ name: "After Hours", startAt: { utc: "2026-11-07T08:00:00Z", timezone: "Australia/Sydney", }, }); if (error) throw error; ``` This creates a General Admission ticket type too. Your event is ready to [register attendees and issue tickets](/docs/attendees). Times include a UTC timestamp and the event’s timezone. See [event time](/docs/event-time) for converting local time, displaying dates and handling daylight saving. ## List events [#list-events] ```ts const { data: events, error } = await eventlayer.events.list(); if (error) throw error; ``` ## Configure an event [#configure-an-event] Use the [dashboard](/dashboard) to edit the schedule, manage ticket types and assign a Wallet template. For all API fields and update operations, see the [API reference](/docs/api-reference). --- # Eventlayer documentation (/docs) Eventlayer is an API for adding events, attendees, tickets, Apple Wallet passes and check-in to your product. Your product stays in front; Eventlayer provides the event infrastructure underneath. ## Choose your next step [#choose-your-next-step] ## Keep building [#keep-building] Use the [TypeScript SDK](/docs/sdk) for typed calls and result handling. Read [errors and request IDs](/docs/errors) when something fails, and [retries and recovery](/docs/retries) before adding automatic retries to your application. Integration questions are welcome. [Send us the request ID](/support) and the context of what you are building. --- # The Eventlayer model (/docs/model) Your product supplies the event page, registration form and checkout. Eventlayer keeps the event, its attendees and their tickets connected underneath. ## Start with an event [#start-with-an-event] **After Hours** is an event. It holds the schedule and venue. Its attendees, ticket types and tickets belong to that event. By default, creating the event also creates a General Admission ticket type. ## Separate the person from their access [#separate-the-person-from-their-access] **Maya Chen** is an attendee. Her General Admission ticket is a separate record. She could also hold a workshop ticket. Model separate admissions as separate tickets when each one needs its own check-in. A **ticket type** describes the access: General Admission, VIP or Staff. A **ticket** is one instance of that access, optionally assigned to an attendee. | Resource | Example | What it represents | | --------------- | ---------------------- | ------------------------------------------------------ | | Event | After Hours | Schedule, venue and event context | | Attendee | Maya Chen | A person registered for this event | | Ticket type | General Admission | A category of access | | Ticket | Maya’s assigned ticket | One admission, with its own lifecycle | | Wallet template | After Hours branding | Pass appearance inherited from an event or ticket type | ## Follow the ticket [#follow-the-ticket] An available ticket becomes assigned when linked to an attendee. Check-in records admission against that same ticket. A Wallet pass is generated from the ticket and remains connected to it after installation. Webhooks let your application react to changes. Your product controls email, payment, CRM and other work triggered by those notifications. ## Choose how to configure it [#choose-how-to-configure-it] The API and dashboard work with the same resources. Use the API when the workflow belongs in your product; use the dashboard to configure and inspect resources visually. The Wallet designer is useful for previewing a template. --- # Pagination (/docs/pagination) Paginated resources return `items` and `meta`. Use cursor IDs to continue through a collection. Check the resource reference: `events.list()` returns an array, while resources such as tickets and attendees support pagination. ## Read one page [#read-one-page] ```ts title="list-tickets.ts · server only" const { data: page, error } = await eventlayer.tickets.list({ eventId: "ev_your_event_id", limit: 50, }); if (error) throw error; console.log(page.items); if (page.meta.has_more) { const after = page.items.at(-1)?.id; // Pass this cursor as `after` in the next list request. console.log(after); } ``` Keep the same filters when requesting subsequent pages. Do not assume the first page contains every attendee or ticket in an event. ## Iterate through a collection [#iterate-through-a-collection] The SDK handles subsequent requests with `listAll()`. Each yielded item is a result envelope, including any error that stops pagination. ```ts title="all-tickets.ts · server only" for await (const { data: ticket, error } of eventlayer.tickets.listAll({ eventId: "ev_your_event_id", })) { if (error) throw error; console.log(ticket.id, ticket.status); } ``` If a page fails, report the incomplete operation instead of treating the records read so far as a complete collection. See [retries and recovery](/docs/retries). --- # Issue your first ticket (/docs/quickstart) Create an event for **After Hours**, register **Maya Chen**, and issue her ticket. ## 1. Set up [#1-set-up] [Create an API key](/dashboard/api-keys) and set it as EVENTLAYER\_KEY in your server environment. The SDK reads it automatically. ```bash npm install eventlayer ``` ## 2. Create your ticket [#2-create-your-ticket] Run these calls in your server-side TypeScript code. ```ts title="first-ticket.ts · server only" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); const { data: event, error: eventError } = await eventlayer.events.create({ name: "After Hours", startAt: { utc: "2026-11-07T08:00:00Z", timezone: "Australia/Sydney", }, }); if (eventError) throw eventError; const { data: attendee, error: attendeeError } = await eventlayer.attendees.create({ eventId: event.id, name: "Maya Chen", email: "maya@example.com", createTicket: true, }); if (attendeeError) throw attendeeError; console.log(attendee.ticket?.id, attendee.ticket?.status); // tkt_… assigned ``` Each event starts with a General Admission ticket type, so you can issue a ticket without creating a type first. Your ticket is now assigned to Maya. View it in the [dashboard](/dashboard). ## Next steps [#next-steps] * [Add an Apple Wallet pass](/docs/guides/apple-wallet) * [Check in the ticket](/docs/guides/check-in) * [Connect a registration form](/docs/guides/website-registration) For production integrations, see [error handling](/docs/errors) and [retries and recovery](/docs/retries). --- # Rate Limits (/docs/rate-limits) Requests sharing an API key share a rate limit. When it is exceeded, the API returns `429 Too Many Requests` with code `RATE_LIMIT_EXCEEDED`. ## When a request is limited [#when-a-request-is-limited] Reduce concurrent requests and wait before trying again. Follow `Retry-After` when the response includes it. The SDK retries `429` responses automatically unless retries are disabled. For large imports, use a queue with limited concurrency instead of sending every request at once. See [retries and recovery](/docs/retries) before retrying writes. ## Plan for your volume [#plan-for-your-volume] Contact [support](/support) to discuss your expected request volume and available limits before launching. --- # Retries and recovery (/docs/retries) A lost response does not tell you whether the server saved a change. Recovery starts with the operation you were performing and what your application knows. ## SDK retry behaviour [#sdk-retry-behaviour] The TypeScript SDK defaults to three retries with exponential backoff and jitter. It retries timed-out requests and HTTP `429`, `500`, `502`, `503` and `504` responses. The default request timeout is 30 seconds. Other network errors throw immediately. For workflows that create attendees, tickets or other resources, disable automatic retries until your application has a recovery path: ```ts title="eventlayer.ts · server only" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(undefined, { maxRetries: 0 }); ``` This still reads `EVENTLAYER_KEY` from your environment. See the [SDK](/docs/sdk) for configuration and [rate limits](/docs/rate-limits) for `429` responses. ## Recover according to the operation [#recover-according-to-the-operation] | Operation | After a failure or lost response | | -------------------------------- | ---------------------------------------------------------------------------- | | Read a resource | Retry within your timeout and rate-limit budget | | Create an attendee or ticket | Reconcile whether it exists before creating again | | Create an attendee with a ticket | Both are saved together; reconcile a lost response before submitting again | | Check in a ticket | Treat a lost response as unknown; do not infer that your scanner admitted it | | Receive a webhook | Deduplicate durable processing; deliveries can repeat | ## Save progress in your product [#save-progress-in-your-product] Store the booking or order identity in your database, along with the returned Eventlayer resource IDs. Serialize work for the same booking. An application submission key helps prevent duplicate requests, but it cannot make your database and the remote API one transaction. Keep unknown outcomes visible for reconciliation. A retry can create another registration even when the first response was lost. --- # TypeScript SDK (/docs/sdk) Use the `eventlayer` package from your server. It provides typed resource methods, result handling, pagination and webhook verification. ## Install and initialise [#install-and-initialise] ```bash title="Terminal · project directory" npm install eventlayer ``` Set `EVENTLAYER_KEY` in your server environment, then initialise a client: ```ts title="eventlayer.ts · server only" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); ``` For a complete runnable example, [issue your first ticket](/docs/quickstart). ## Handle typed results [#handle-typed-results] HTTP responses return `{ data, error, response }`. Check `error` before reading `data`. Network failures where no HTTP response is received still throw. ```ts title="get-ticket.ts · server only" const { data: ticket, error } = await eventlayer.tickets.get({ ticketId: "tkt_your_ticket_id", }); if (error) { console.error(error.code, error.requestId); } else { console.log(ticket.status); } ``` ## Client options [#client-options] The defaults are enough to get started. For timeouts and retry settings, see [retries and recovery](/docs/retries). ## Find the right method [#find-the-right-method] | Resource | Example method | | --------- | ------------------------------------------------------- | | Events | `eventlayer.events.create()` | | Attendees | `eventlayer.attendees.create()` | | Tickets | `eventlayer.tickets.create()` | | Check-in | `eventlayer.tickets.checkIn()` | | Wallet | `eventlayer.tickets.generateAppleWalletPassSignedUrl()` | Required arguments are shown in the guides and [API reference](/docs/api-reference). Manage webhook endpoints and delivery history in the dashboard. ## Keep going [#keep-going] Use [pagination](/docs/pagination) to iterate through collections and [webhook verification](/docs/guides/webhooks) to validate signed events. The [package documentation](https://www.npmjs.com/package/eventlayer) has the SDK’s full configuration and resource details. --- # Ticket Types (/docs/ticket-types) Ticket types group tickets by access, such as General Admission, VIP or Staff. Every new event starts with a General Admission type. ## Add a ticket type [#add-a-ticket-type] ```ts const { data: ticketType, error } = await eventlayer.ticketTypes.create({ eventId: "ev_123", name: "VIP", description: "Priority seating and backstage access.", }); if (error) throw error; ``` Pass `ticketType.id` as `ticketTypeId` when [issuing a ticket](/docs/tickets). If an event has only one ticket type, Eventlayer selects it automatically. ## List ticket types [#list-ticket-types] ```ts const { data: page, error } = await eventlayer.ticketTypes.list({ eventId: "ev_123", }); if (error) throw error; console.log(page.items); ``` ## Pause check-in for a type [#pause-check-in-for-a-type] ```ts const { error } = await eventlayer.ticketTypes.update({ ticketTypeId: "tt_123", status: "inactive", }); if (error) throw error; ``` An inactive type blocks check-in. Existing assigned tickets remain assigned. Set the status to `active` to allow check-in again. Deleting a type removes its tickets. Use deactivation when you only need to pause entry. You can also manage types in the [dashboard](/dashboard). For optional fields, including Wallet templates, see the [API reference](/docs/api-reference). To start an event without a default type, pass `disableDefaultTicketType: true` when creating it. --- # Tickets (/docs/tickets) A ticket grants access to an event. It belongs to a ticket type and can be assigned to an attendee. ## Issue a ticket [#issue-a-ticket] ```ts const { data: ticket, error } = await eventlayer.tickets.create({ eventId: "ev_123", attendeeId: "atd_123", }); if (error) throw error; ``` The ticket starts as `assigned`. If the event has multiple ticket types, include `ticketTypeId`. To create the attendee too, use [attendee creation with a ticket](/docs/attendees). ## Create inventory, then assign it [#create-inventory-then-assign-it] Omit `attendeeId` when creating a ticket to leave it `available`. Assign it when you know who is coming: ```ts const { data: ticket, error } = await eventlayer.tickets.assign({ ticketId: "tkt_123", attendeeId: "atd_123", }); if (error) throw error; ``` The attendee must belong to the same event. ## Ticket lifecycle [#ticket-lifecycle] Tickets follow a defined status lifecycle: | Status | Description | | ------------ | ---------------------------------------------------------------------- | | `available` | Ticket has been issued but not assigned to an attendee. | | `assigned` | Ticket is assigned to an attendee. | | `checked_in` | Admission has been recorded. Check-in cannot be reversed. | | `cancelled` | Access was removed; a record is retained for an installed Wallet pass. | Removing access uses `tickets.delete()`. If a device is registered for the Wallet pass, a cancelled record remains so the pass can be updated. Otherwise the ticket is deleted. See [access removal](/docs/guides/payments#refunds-and-access-removal). ## Check in a ticket [#check-in-a-ticket] ```ts const { data: ticket, error } = await eventlayer.tickets.checkIn({ ticketId: "tkt_123", }); if (error) throw error; ``` See [check-in](/docs/check-in) for requirements and response codes. ## List tickets [#list-tickets] ```ts const { data: page, error } = await eventlayer.tickets.list({ eventId: "ev_123", status: "assigned", }); if (error) throw error; ``` Results are in `page.items`. See [pagination](/docs/pagination) for more results. ## Apple Wallet passes [#apple-wallet-passes] An assigned ticket can become a signed Apple Wallet pass. Follow [Add Apple Wallet tickets](/docs/guides/apple-wallet) to generate its download URL. --- # Webhooks (/docs/webhooks) Webhooks send changes to your application without polling. ## Set up an endpoint [#set-up-an-endpoint] Open **Webhooks → New endpoint** in the [dashboard](/dashboard/webhooks). Enter your HTTPS URL, choose the event types you need and save the signing secret as `EVENTLAYER_WEBHOOK_SECRET` on your server. ## Verify an incoming event [#verify-an-incoming-event] ```ts import { verifyWebhook } from "eventlayer"; const event = await verifyWebhook({ body: await request.text(), headers: request.headers, secret: process.env.EVENTLAYER_WEBHOOK_SECRET!, }); if (event.event_type === "ticket.checked_in") { console.log(event.data.id); } ``` Use the raw request body. The SDK verifies the signature and timestamp before returning a typed event. For a complete handler, follow [React to ticket changes](/docs/guides/webhooks). ## Event types [#event-types] | Event | Triggered when | | --------------------- | ------------------------------------ | | `event.created` | A new event is created. | | `event.updated` | An event is updated. | | `event.deleted` | An event is deleted. | | `attendee.created` | A new attendee is added to an event. | | `attendee.updated` | An attendee is updated. | | `attendee.deleted` | An attendee is removed. | | `ticket_type.created` | A new ticket type is created. | | `ticket_type.updated` | A ticket type is updated. | | `ticket_type.deleted` | A ticket type is deleted. | | `ticket.created` | A new ticket is issued. | | `ticket.updated` | A ticket is updated. | | `ticket.deleted` | A ticket is deleted. | | `ticket.assigned` | A ticket is assigned to an attendee. | | `ticket.checked_in` | An attendee checks in. | ## Payload [#payload] Each event contains `id`, `event_type`, `timestamp`, `attempt` and `data`. The data contains the affected resource; deletion events contain only its ID. ## Handle duplicates [#handle-duplicates] Store the verified `event.id` with your processing result. Skip events you have already processed so retries and manual replays do not repeat side effects. Return `2xx` after durably accepting the event. Delivery order is not guaranteed. Automatic retries keep the same event and delivery IDs. A manual replay creates a new delivery ID but keeps the original event ID and resource payload. `Webhook-Delivery-Id` identifies a delivery, not a unique business change. ## Retry behavior [#retry-behavior] Network failures, timeouts, rate limits, and server errors are retried with backoff. Redirects and most other client errors are treated as permanent failures. Retryable HTTP responses are `408`, `425`, `429`, and `5xx`. Retries are scheduled after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, and 10 hours, for up to seven attempts in total. Worker availability may delay an attempt beyond its scheduled time. After the retry schedule is exhausted, the delivery is marked `failed`. ## Inspect failures and replay [#inspect-failures-and-replay] Open **Webhooks → Delivery log** in the dashboard to see attempts, HTTP responses and the next retry time. Select a delivery to investigate it, then use **Retry** or **Resend** after fixing the cause. Replays use the endpoint’s current URL. ## Signature details [#signature-details] For consumers without the SDK, verify HMAC-SHA256 over `Webhook-Timestamp + "." + raw_request_body` using your endpoint signing secret. Compare against `v1` values in `Webhook-Signature` in constant time and reject timestamps more than five minutes from the current time. Secret rotation has a 24-hour overlap. During it, the header can contain multiple signatures; accept one that matches a trusted secret. Manage endpoints and rotate secrets in the dashboard. --- # Add Apple Wallet tickets (/docs/guides/apple-wallet) Add an Apple Wallet download to your ticket page. ## 1. Create a template [#1-create-a-template] In **Dashboard → Passes**, design a template and attach it to your event. A ticket type can override the event’s template. See [template options](/docs/apple-wallet-templates) for fields and imagery. ## 2. Generate the pass URL [#2-generate-the-pass-url] Use an assigned ticket from the [quickstart](/docs/quickstart). Run this on your backend after checking the attendee can access the ticket. ```ts title="Wallet download · server" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); const { data: pass, error } = await eventlayer.tickets .generateAppleWalletPassSignedUrl({ ticketId: ticket.id }); if (error) throw error; ``` ## 3. Show the download link [#3-show-the-download-link] Pass the URL to your ticket page. ```tsx Add to Apple Wallet ``` The URL is private and expires after 24 hours. Generate a fresh one when the attendee requests it. For emails, link to your ticket page instead. Adding a pass keeps the same ticket. It does not check the attendee in. ## Next steps [#next-steps] [Build a check-in app](/docs/guides/check-in). --- # Build a check-in app (/docs/guides/check-in) Your scanner reads a ticket ID. Your backend asks Eventlayer to check it in. Use an assigned test ticket. Check-in is one-time and cannot be undone. ## Check in the ticket [#check-in-the-ticket] Authenticate staff and confirm the ticket belongs at this door before making the call. Keep your API key on the backend. ```ts title="Check-in · server" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); const { data: ticket, error } = await eventlayer.tickets.checkIn({ ticketId: "tkt_your_ticket", }); if (error) throw error; console.log(ticket.status); // checked_in ``` A successful response records admission. If two scanners submit the same ticket, only one can complete its first check-in. ## Show the result [#show-the-result] | Result | Scanner message | | ---------------------------- | ------------------ | | Success | Checked in | | TICKET\_ALREADY\_CHECKED\_IN | Already checked in | | FAILED\_PRECONDITION | Ticket unavailable | | NOT\_FOUND | Ticket not found | If the response is lost, show an unknown result for staff to review. See [errors](/docs/errors) and [recovery](/docs/retries) for handling failures. ## Next steps [#next-steps] [React to check-in events](/docs/guides/webhooks) to update your CRM or send a follow-up. --- # Sync attendees to your CRM (/docs/guides/crm) Connect Eventlayer webhooks to your CRM’s API. ## 1. Subscribe to attendee changes [#1-subscribe-to-attendee-changes] In **Dashboard → Webhooks**, subscribe to attendee.created and attendee.updated. Follow the [webhook guide](/docs/guides/webhooks) to verify incoming requests. ## 2. Map the contact fields [#2-map-the-contact-fields] Use the verified event’s attendee data in your CRM’s create or update call. ```ts title="CRM mapping · after webhook verification" if ( event.event_type === "attendee.created" || event.event_type === "attendee.updated" ) { const contact = { externalId: event.data.id, name: event.data.name, email: event.data.email, }; // Send contact to your CRM using its API. } ``` Save the Eventlayer attendee ID alongside your CRM contact ID. Attendees belong to an event, so your application decides how to match people across events. ## 3. Add attendance [#3-add-attendance] Subscribe to ticket.checked\_in to record attendance. Link that ticket to its attendee, then update the contact’s event history using the ticket ID as the participation key. Deduplicate webhook processing by event.id. Use create-or-update operations in your CRM so retries do not create duplicate records. See [webhook delivery](/docs/webhooks) for retries and replay. ## Next steps [#next-steps] [Send registration and ticket emails](/docs/guides/email). --- # Send registration and ticket emails (/docs/guides/email) Eventlayer supplies the ticket activity. Your email provider sends the message. ## 1. Choose a trigger [#1-choose-a-trigger] | Message | Trigger | | ------------ | --------------------------------------------------- | | Confirmation | ticket.created with an attendee, or ticket.assigned | | Follow-up | ticket.checked\_in | | Reminder | Your application’s scheduler | Use the [webhook guide](/docs/guides/webhooks) to receive and verify these events. ## 2. Prepare the message [#2-prepare-the-message] After a successful registration, use the attendee and ticket-page URL saved by your application to prepare an email. ```ts title="Confirmation email · server" const message = { to: attendee.email, subject: "Your event ticket is ready", text: "View your ticket: " + ticketPageUrl, }; ``` Send these fields through your email provider’s API. The ticket page can generate a fresh [Wallet download](/docs/guides/apple-wallet). ## 3. Send once [#3-send-once] Use a stable message key, such as ticket ID + attendee ID + message type, to avoid sending again when a webhook or job is retried. Schedule reminders in your application. Before sending, check the current event time, ticket status and recipient. Respect their communication preferences. See [retries and recovery](/docs/retries) for failed or uncertain sends. ## Next steps [#next-steps] [Sync event activity to your CRM](/docs/guides/crm). --- # Build complete event flows (/docs/guides) Start with the ticket from the [quickstart](/docs/quickstart), then build the registration, delivery or admission flow your product needs. Each guide focuses on one task with a short example. ## Start with registration [#start-with-registration] ## Build on the ticket [#build-on-the-ticket] ## About the examples [#about-the-examples] Examples use the TypeScript SDK on your server, with EVENTLAYER\_KEY set in the environment. Your application supplies the interface and connects any email, payment or CRM providers. See [errors](/docs/errors) and [retries](/docs/retries) when adding production failure handling. --- # Issue tickets after payment (/docs/guides/payments) Use your payment provider for checkout. Use Eventlayer to issue the ticket. ## 1. Confirm the payment [#1-confirm-the-payment] Verify your payment provider’s webhook and match it to a paid order in your database. Use the order’s event and attendee IDs; a browser redirect is not proof of payment. Create the attendee with the [registration flow](/docs/guides/website-registration) if you have not already saved one. ## 2. Issue the ticket [#2-issue-the-ticket] Once your backend confirms the order is paid, create its ticket. ```ts title="Paid order · server" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); const { data: ticket, error } = await eventlayer.tickets.create({ eventId: order.eventId, attendeeId: order.attendeeId, ticketTypeId: order.ticketTypeId, }); if (error) throw error; ``` Save ticket.id on the order and show your confirmation page. If a payment notification arrives again, reuse the saved ticket. See [retries and recovery](/docs/retries) for partial failures. ## Refunds and access removal [#refunds-and-access-removal] A refund through your payment provider does not remove the Eventlayer ticket. When your refund policy requires it, remove access using the saved ticket ID. ```ts title="Remove access · server" const { error } = await eventlayer.tickets.delete({ ticketId: order.ticketId, }); if (error) throw error; ``` This removes access. A registered Wallet device keeps a cancelled ticket record; otherwise the ticket is deleted. Keep the order in your own database. ## Next steps [#next-steps] [Send the ticket email](/docs/guides/email) or [add a Wallet pass](/docs/guides/apple-wallet). --- # React to ticket changes (/docs/guides/webhooks) Run your own code when a ticket is assigned or checked in. ## 1. Create an endpoint [#1-create-an-endpoint] In **Dashboard → Webhooks**, add your server’s HTTPS endpoint and choose the events you need. Save the signing secret as EVENTLAYER\_WEBHOOK\_SECRET. ## 2. Verify the request [#2-verify-the-request] Inside your request handler, pass the raw body and headers to the SDK. ```ts title="Webhook handler · server" import { verifyWebhook } from "eventlayer"; const event = await verifyWebhook({ body: await request.text(), headers: request.headers, secret: process.env.EVENTLAYER_WEBHOOK_SECRET!, }); if (event.event_type === "ticket.checked_in") { console.log(event.data.id, event.data.checked_in_at); } ``` Verification throws for invalid requests. Do not process those payloads. ## 3. Handle the event [#3-handle-the-event] Save the verified event before returning a successful response. Use event.id to deduplicate retries and replays, then process your email, CRM or analytics work. For delivery history, signing and retries, see the [webhook reference](/docs/webhooks). ## Next steps [#next-steps] [Send ticket emails](/docs/guides/email) or [sync attendees to your CRM](/docs/guides/crm). --- # Add events to your website (/docs/guides/website-registration) Your form collects the attendee’s details. Your backend creates their ticket. Start with an event from the [quickstart](/docs/quickstart). ## 1. Collect a name and email [#1-collect-a-name-and-email] Post your form to a route in your application. ```html
``` ## 2. Issue the ticket [#2-issue-the-ticket] In your server handler, validate the form and use your event’s ID. The SDK reads your server’s EVENTLAYER\_KEY. ```ts title="Registration handler · server" import { Eventlayer } from "eventlayer"; const eventlayer = new Eventlayer(); const form = await request.formData(); const { data: attendee, error: attendeeError } = await eventlayer.attendees.create({ eventId: "ev_your_event", name: String(form.get("name")), email: String(form.get("email")), createTicket: true, }); if (attendeeError) throw attendeeError; ``` Save `attendee.id` and `attendee.ticket.id` with your registration, then show your confirmation page. Your application handles capacity and registration rules. For paid events, [confirm payment before issuing the ticket](/docs/guides/payments). The attendee and ticket are created together. If either fails, neither is saved. For duplicate submissions and lost responses, see [retries and recovery](/docs/retries). ## Next steps [#next-steps] [Add a Wallet pass](/docs/guides/apple-wallet) or [send a confirmation email](/docs/guides/email).