# Archive a ticket group (/docs/api/v1/events/ticket-groups/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Archives a ticket group by setting its status to "archived". The ticket group will no longer be visible but data is preserved.
---
# Archive a ticket type (/docs/api/v1/events/ticket-groups/ticket-types/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Archives a ticket type by setting its status to "archived". The ticket type will no longer be visible but data is preserved.
---
# Archive checkout page (/docs/api/v1/checkout-pages/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Archives the specified checkout page.
---
# Archive event (/docs/api/v1/events/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Archives the specified event.
---
# Archive form (/docs/api/v1/forms/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Archives the specified form.
---
# Authentication (/docs/api/authentication)
The Checkout Page API uses API key authentication with Bearer tokens. All API requests must include a valid API key in the authorization header.
## API key format [#api-key-format]
All requests to the Checkout Page API must include an authorization header with your API key in the following format:
```
Authorization: Bearer YOUR_API_KEY
```
## Getting your API key [#getting-your-api-key]
1. **Sign up for Checkout Page**: Create an account at [checkoutpage.com](https://checkoutpage.com)
2. **Access your dashboard**: Navigate to your seller dashboard
3. **Generate API key**: Go to Settings > API Keys and generate a new API key
4. **Copy your key**: Securely store your API key - it won't be shown again
**Keep your API key secure**: Never expose your API key in client-side code, public repositories,
or share it publicly. Store it securely as an environment variable.
## Making authenticated requests [#making-authenticated-requests]
### cURL example [#curl-example]
```bash
curl -X GET https://api.checkoutpage.com/v1/customers \
-H "Authorization: Bearer sk_live_1234567890abcdef" \
-H "Content-Type: application/json"
```
### JavaScript example [#javascript-example]
```javascript
const apiKey = process.env.CHECKOUTPAGE_API_KEY;
const response = await fetch('https://api.checkoutpage.com/v1/customers', {
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
```
### Python example [#python-example]
```python
import os
import requests
api_key = os.environ.get('CHECKOUTPAGE_API_KEY')
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}
response = requests.get(
'https://api.checkoutpage.com/v1/customers',
headers=headers,
)
data = response.json()
```
## Authentication errors [#authentication-errors]
If authentication fails, you'll receive the following response.
**HTTP Status**: 401 Unauthorized
```json
{
"error": "Unauthorized",
"message": "Missing or invalid API key"
}
```
## Test mode [#test-mode]
Purchases made while a page is in [test mode](/docs/build/test-mode) create test records with `livemode: false`. Payments, bookings, subscriptions, subscription payments, invoices, submissions and tickets all work this way.
### Reading test data [#reading-test-data]
A live key (`sk_live_`) reads live records by default. To read test records with it, add `livemode=false` to the list and get endpoints for those records:
```bash
curl "https://api.checkoutpage.com/v1/payments?livemode=false" \
-H "Authorization: Bearer sk_live_1234567890abcdef"
```
Without `livemode=false`, a live key leaves test records out, so fetching a test record by its id returns `404`.
Keys that start with `sk_test_` can only read test data: `livemode=true` returns `403` with the message `Test API keys can only read test data. Remove livemode=true or use a live API key.`
### Writes [#writes]
Write endpoints don't take `livemode`. Creating a booking, cancelling a subscription and regenerating an invoice use the mode of your API key, so a live key creates and changes live records. Validating a ticket finds the ticket by its id, whatever its mode, and updating a ticket works on live tickets.
## Best practices [#best-practices]
### Environment variables [#environment-variables]
Store your API key as an environment variable:
```bash
# .env file
CHECKOUTPAGE_API_KEY=sk_live_1234567890abcdef
```
### Error handling [#error-handling]
Always handle authentication errors gracefully:
```javascript
async function makeApiRequest() {
try {
const response = await fetch('https://api.checkoutpage.com/v1/endpoint', {
headers: {
Authorization: `Bearer ${process.env.CHECKOUTPAGE_API_KEY}`,
},
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication failed - check your API key');
}
throw new Error(`API request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request error:', error.message);
throw error;
}
}
```
### Security considerations [#security-considerations]
* **Never log API keys**: Ensure API keys are not logged in your application
* **Use HTTPS only**: All API requests must use HTTPS
* **Rotate keys regularly**: Generate new API keys periodically and revoke old ones
* **Limit key access**: Only give API access to systems that need it
## Next steps [#next-steps]
Now that you understand authentication, explore these API capabilities:
---
# booking.abandoned (/docs/api/v1/webhooks/booking.abandoned)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a booking is abandoned. A booking is abandoned when a customer starts but does not complete the booking. [Learn more](https://checkoutpage.com/docs/articles/cart-abandonment)
---
# booking.canceled (/docs/api/v1/webhooks/booking.canceled)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a booking is canceled
---
# booking.created (/docs/api/v1/webhooks/booking.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a new booking is created
---
# booking.paid (/docs/api/v1/webhooks/booking.paid)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a booking payment is completed
---
# Cancel a payment (/docs/api/v1/payments/cancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Cancels a one-time payment and returns it with `orderStatus: canceled`. A cancel is final and cannot be undone. It ends the whole order, order bumps included, puts the product, variant and order bump stock back, and removes the buyer's access to the order's files and license key. It does not move money: pass `refund` to also refund the customer in the same request. Revenue totals stay gross. Optional `sendCancellationEmail` emails the customer. Fires the `payment.canceled` webhook.
---
# Cancel a submission (/docs/api/v1/submissions/cancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Cancels a form submission and returns it with `orderStatus: canceled`. A cancel is final and cannot be undone. A canceled submission no longer counts toward "Prevent duplicate submissions", so the same customer can submit the form again, and its files leave the customer portal. Optional `sendCancellationEmail` emails the customer. Fires the `submission.canceled` webhook.
---
# Cancel a subscription (/docs/api/v1/subscriptions/cancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Cancels a customer's subscription. Provide exactly one of `cancelImmediately`, `cancelAtPeriodEnd`, or `cancelAt` to control the cancellation timing. The three timing fields are mutually exclusive — omitting all of them returns 400 (timing must be explicit). Optional `reason` is stored on the subscription; optional `sendCancellationEmail` triggers a customer email.
---
# Checkout Page MCP (/docs/build/mcp)
The Checkout Page MCP server lets AI tools like Claude, ChatGPT and Cursor build checkout pages, events and forms in your Checkout Page account. Describe what you sell in plain English. Your AI creates the page, edits it when you ask and adds it to your site.
It's built on the [Model Context Protocol](https://modelcontextprotocol.io), an open standard for connecting AI assistants to the tools and data they need.
Building a website with AI? See [how Checkout Page adds payments to AI-built sites](https://checkoutpage.com/mcp).
The server URL is:
[https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
## What can the Checkout Page MCP do? [#what-can-the-checkout-page-mcp-do]
Once connected, you can ask your AI tool to do things like:
* *"Create a checkout page called 'Spring Workshop' priced at $99."*
* *"Set up an event for our May gala with Early bird tickets at $40 and Standard tickets at $60, capped at 200 seats."*
* *"Build a registration form for the May webinar. Collect name, email, and which session they want to attend."*
* *"Add a 'Dietary needs' question to the gala event."*
* *"Offer a $15 parking pass as an add-on on the gala event."*
* *"Create a 20% off coupon called SPRING20 that expires May 31."*
* *"Make the Spring Workshop page match our brand: dark green buttons and a serif heading font."*
* *"Add the Spring Workshop checkout to the pricing section of the site we're building, as a popup button."*
* *"Show me the last 10 registrations for the May webinar."*
Your AI uses the tools below to build and change your checkout pages, events and forms.
## Available tools [#available-tools]
All tools run with your account's permissions and respect the same access controls as the
dashboard. The `list_*` and `get_*` tools never change data. The `create_*`, `update_*`,
`upload_*`, `archive_*` and `reorder_*` tools add or change data. The `delete_*` tools remove data
and cannot be undone.
| What you can do | Tools |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build checkout pages | `create_checkout_page`, `update_checkout_page`, `get_checkout_page`, `list_checkout_pages` |
| Build events and tickets | `create_event`, `update_event`, `get_event`, `list_events`, `create_ticket_group`, `update_ticket_group`, `archive_ticket_group`, `create_ticket_type`, `update_ticket_type`, `archive_ticket_type` |
| Build forms | `create_form`, `update_form`, `get_form`, `list_forms` |
| Add questions to a page | `create_field`, `update_field`, `delete_field` |
| Set up products | `create_product`, `update_product`, `delete_product`, `get_product`, `list_products` |
| Offer order bumps and add-ons | `create_order_bump`, `update_order_bump`, `delete_order_bump`, `reorder_order_bumps`, `list_order_bumps` |
| Create coupons | `create_coupon`, `update_coupon`, `list_coupons` |
| Charge tax | `create_tax_rate`, `list_tax_rates` |
| Style pages with themes | `create_theme`, `update_theme`, `delete_theme`, `get_theme`, `list_themes`, `list_theme_properties` |
| Upload images and files | `upload_file` |
| Put a page on your website | `add_to_website`: ready-to-paste code for an embed, a popup button or a link |
| Look up payments and subscriptions | `list_payments`, `get_payment`, `list_subscriptions`, `get_subscription`, `list_subscription_payments`, `get_subscription_payment`, `prepare_subscription_cancellation` |
| Look up bookings and attendees | `list_bookings`, `get_booking`, `create_booking`, `list_tickets`, `update_ticket` |
| Look up form submissions | `list_submissions`, `get_submission` |
| Look up customers and invoices | `list_customers`, `get_customer`, `update_customer`, `list_invoices`, `get_invoice`, `regenerate_invoice` |
| Send events to your own server | `create_webhook`, `update_webhook`, `delete_webhook`, `get_webhook`, `list_webhooks` |
`prepare_subscription_cancellation` does not cancel anything. It checks the subscription and gives you the dashboard link where you cancel it yourself. `create_booking` issues real tickets and sends the confirmation email, without taking a card payment.
## Before you start [#before-you-start]
You'll need a Checkout Page account. The MCP server uses OAuth, so there are no API keys to copy or paste. Your AI tool walks you through a one-click sign-in the first time you connect.
## Connect your AI tool [#connect-your-ai-tool]
Each AI tool has its own settings screen for adding a connector. Pick yours below. The steps take about a minute. Once connected, try the test prompt at the end of each section to confirm it's working.
### ChatGPT [#chatgpt]
Checkout Page is in the ChatGPT plugin directory:
1. Open the [Checkout Page plugin in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_69ddd7207c5c8191aea3c813eb305ac2).
2. Click **Connect**, then sign in to Checkout Page.
Or add it as a custom connector in Developer mode:
1. Open **ChatGPT.com** and go to **Settings → Security and login**. Enable **Developer mode**.
2. Close settings and go to [chatgpt.com/plugins](https://chatgpt.com/plugins).
3. Click the **+** in the top right.
4. Enter:
* **Name**: Checkout Page
* **MCP server URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
* **Authentication**: OAuth
5. Toggle **I understand and want to continue** on.
6. Click **Create**, then follow the Checkout Page OAuth flow to connect.
7. Start a **new** chat.
8. Click **+** and search for **Checkout Page**.
**Test it:** ask *"List my checkout pages, events and forms in Checkout Page."*
### Claude [#claude]
1. Open **Claude** and go to **Settings → Connectors**.
2. Click **Add custom connector**.
3. Enter:
* **Name**: Checkout Page
* **MCP server URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
4. Click **Add**, then click **Connect** on the **Checkout Page** connector and sign in.
**Test it:** ask *"List my checkout pages, events and forms in Checkout Page."*
### Cursor [#cursor]
The fastest way is the one-click install:
Open Cursor, then click [**Add Checkout Page to
Cursor**](cursor://anysphere.cursor-deeplink/mcp/install?name=checkoutpage\&config=eyJ1cmwiOiJodHRwczovL21jcC5jaGVja291dHBhZ2UuY29tIn0=)
and approve the install.
Or add it manually:
1. Open Cursor, then go to **Settings → MCP**.
2. Click **Add new MCP server**.
3. Enter:
* **Name**: checkoutpage
* **URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
4. Click **Save**, then sign in via the OAuth prompt.
**Test it:** in Cursor's chat, ask *"List my checkout pages, events and forms in Checkout Page."*
### VS Code [#vs-code]
GitHub Copilot Chat and the official MCP extension both support our server.
1. Open VS Code and run the **MCP: Add Server** command from the Command Palette (`Cmd/Ctrl + Shift + P`).
2. Choose **HTTP**.
3. Enter the URL: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
4. Name it: checkoutpage
5. Sign in via the OAuth prompt that opens in your browser.
**Test it:** in Copilot Chat, ask *"List my checkout pages, events and forms in Checkout Page."*
### Codex [#codex]
In the Codex CLI, one command adds the server and starts the sign-in:
```sh
codex mcp add checkoutpage --url https://mcp.checkoutpage.com
```
Or add it in the Codex app:
1. Open **Codex** and go to **Settings → MCP servers**.
2. Click **+ Add server**.
3. Choose **Streamable HTTP** as the transport.
4. Enter:
* **Name**: Checkout Page
* **URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
5. Click **Save**, then sign in via the OAuth prompt.
**Test it:** ask *"List my checkout pages, events and forms in Checkout Page."*
### Claude Code [#claude-code]
Two steps: add the server, then sign in from inside Claude Code.
```sh
claude mcp add --transport http checkoutpage https://mcp.checkoutpage.com
```
Then start a session:
```sh
claude
```
In the session:
1. Type `/mcp`.
2. Use arrow keys to highlight `checkoutpage` and press enter.
3. Choose **authenticate** and follow the OAuth prompt.
**Test it:** ask *"List my checkout pages, events and forms in Checkout Page."*
### Lovable [#lovable]
1. In Lovable, open **Connectors** and select the **+** button.
2. Choose **MCP server**.
3. Enter:
* **Server name**: Checkout Page
* **Server URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
4. Keep **OAuth**, click **Add & authorize**, then sign in to Checkout Page.
**Test it:** ask *"List my checkout pages, events and forms in Checkout Page."*
### Cline [#cline]
1. In the Cline panel, click the **MCP Servers** icon in the top toolbar.
2. Open the **Remote Servers** tab.
3. Enter:
* **Server Name**: checkoutpage
* **Server URL**: [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com)
* **Transport Type**: Streamable HTTP
4. Click **Add Server**. Cline opens the **Configure** tab.
5. Click **Authenticate** on the **checkoutpage** server, then sign in to Checkout Page in your browser.
**Test it:** in Cline, ask *"List my checkout pages, events and forms in Checkout Page."*
### Other AI tools [#other-ai-tools]
Any client with MCP support can connect to [https://mcp.checkoutpage.com](https://mcp.checkoutpage.com) using OAuth. If your tool asks for a transport, choose **Streamable HTTP**. Most clients accept this config:
```json
{
"mcpServers": {
"checkoutpage": {
"url": "https://mcp.checkoutpage.com"
}
}
}
```
## Example prompts [#example-prompts]
### Launch a new product [#launch-a-new-product]
You're prepping for a new workshop and want a checkout page and early-bird discount live in minutes.
Paste this into your AI tool:
```text
Create a checkout page called "Pricing Strategy Workshop" priced at $199. Write a confident product description (3–4 sentences) that pitches a half-day workshop for founders and product leaders who want to raise prices without losing customers. Turn on the coupon code field so customers can apply discounts. Then create a coupon code EARLYBIRD that takes 20% off and expires May 15. Send me both the checkout URL and the coupon code when you're done.
```
The assistant calls `create_checkout_page` (with a written description and the coupon field enabled), then `create_coupon`, and returns the live checkout URL and the discount code.
You can use the same pattern for any new offer: a course launch, a beta signup page, or a limited-time promotion.
### Set up an event with tickets [#set-up-an-event-with-tickets]
You're running a gala and want ticket sales open today, with an early-bird price and a question for each guest.
Paste this into your AI tool:
```text
Create an event called "Spring Gala" on May 16 at 7:00 PM at the Riverside Hall. Add two ticket types: "Early bird" at $40 and "Standard" at $60, with 200 tickets in total. Ask each guest for their dietary needs. Write a short, warm description of the evening, then send me the event link.
```
The assistant calls `create_event` with the ticket group, both ticket types and the dietary needs question, and returns the live event link. Ask it later to add a ticket type, change a price or close early-bird sales, and it calls `update_event` or `update_ticket_type`.
### Add a checkout to a website you're building [#add-a-checkout-to-a-website-youre-building]
You're building a site with an AI tool like Lovable, Bolt, v0, Cursor, or Claude Code and want people to buy right on the page.
Paste this into your AI tool:
```text
Create a checkout page called "Pricing Strategy Workshop" priced at $199, then add it to the pricing section of this website as an inline embed.
```
The assistant calls `create_checkout_page`, then `add_to_website`, and places the embed code in your site. It uses a React component when the site is built with React, and plain HTML otherwise. Ask for a popup button instead if you'd rather keep customers on the page until they click.
### Test your checkout [#test-your-checkout]
Before you launch, you want to buy from your own checkout and see exactly what a customer gets, without paying.
Paste this into your AI tool:
```text
Test my "Pricing Strategy Workshop" checkout page in Checkout Page test mode. Put the page in test mode, open it in the browser and buy it with the test card 4242 4242 4242 4242 and a new email address. Then find the test payment it created and check that it's marked livemode false. Switch test mode back off when you're done, and tell me what you bought and what you found.
```
The assistant calls `list_checkout_pages` to find the page and `update_checkout_page` with `testmode: true`, pays in its browser, calls `list_payments` with `livemode: false` to find the test payment, then calls `update_checkout_page` again to switch test mode off.
You can use the same pattern for an event or a form, or to check a webhook or integration fires before you rely on it.
### Answer a customer billing question [#answer-a-customer-billing-question]
A customer emails asking when their next payment will hit and you want a draft reply you can send back in seconds.
Paste their question into your AI tool:
```text
A customer named Jane Smith (jane@example.com) just emailed asking: "When is my next payment, and how much will I be charged?" Look up her account in Checkout Page, find her active subscription, and draft a friendly reply I can send back. Include the next billing date and the amount.
```
The assistant calls `list_customers` to find Jane by email, then `list_subscriptions` to pull her active plan, and drafts a ready-to-send reply with the next billing date and amount.
You can use the same pattern for refund requests, receipt re-sends, or any one-off support question that needs account context.
### Tips for better results [#tips-for-better-results]
* Ask the AI to write copy for you: product titles, descriptions, confirmation messages. Otherwise pages come out blank or generic.
* Be explicit about settings the AI can't guess (e.g. *"turn on the coupon code field"*).
* Stick to specific records and creation tasks. AI assistants are less reliable at totals and rankings ("top 5 pages by revenue") because that requires reading every payment. For numbers like that, use the dashboard's analytics view.
* Some settings aren't available through the MCP yet. If a request doesn't take effect, open the page in your dashboard to finish the change.
## Test mode [#test-mode]
Your assistant can make test purchases on a checkout page, event or form without paying, then check what they created. See [Test mode](/docs/build/test-mode) for how it works.
* **Switch it on and off.** `create_checkout_page`, `create_event` and `create_form`, and the matching `update_*` tools, take `testmode: true` or `testmode: false`. `list_checkout_pages`, `list_events` and `list_forms` take `testmode: true` to find pages still in test mode.
* **Read test records.** The list and get tools for payments, bookings, subscriptions, subscription payments, invoices and submissions, and `list_tickets`, take `livemode: false`. Without it they return live records only.
When your AI tool connects, the server also gives your assistant a short primer on test mode, so you can ask in plain words.
### Test prompts [#test-prompts]
Two prompts script a full test run. They show up as slash commands or prompts in AI tools that support MCP prompts. In Claude Code, for example, type `/mcp__checkoutpage__` and pick one.
| Prompt | What it does | Arguments |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `test_checkout` | Switches a checkout page, event or form into test mode, makes a test purchase in a browser, checks the records, then sets test mode back to how it was. | `pageId`, and optionally `pageType`: `checkout` (default), `event` or `form` |
| `test_upsell_funnel` | Puts both pages of an existing one-click upsell funnel into test mode, runs it once accepting and once declining the offer, checks the records, then sets test mode back to how it was. It doesn't create pages. | `initialPageId`, and optionally `upsellPageId` |
For example, to test an event in Claude Code, type the prompt name, the event's id and its page type:
```text
/mcp__checkoutpage__test_checkout {eventId} event
```
For funnels, see [Test an upsell funnel](/docs/build/test-upsell-funnel).
To pay, your assistant needs a browser tool, such as Claude in Chrome, a Playwright MCP server or ChatGPT agent mode. Without one, it gives you the link to pay yourself, then carries on checking the records.
While a page is in test mode, anyone who visits it gets the product without paying. The prompts
set test mode back to how it was at the end. If a run stops early, ask your assistant *"Which of
my pages are in test mode?"* and switch them off.
## Troubleshooting [#troubleshooting]
Check that your browser isn't blocking popups for your AI tool. Close and reopen the connector
dialog to try again.
Make sure the connector is turned on in the current chat. In ChatGPT, you turn it on per-chat
under the **+** menu. In Claude, confirm it shows as **Connected** under **Settings →
Connectors**.
Your sign-in expired. Open the connector settings and click **Reconnect** (or re-run the
authenticate step in Claude Code / Codex).
Remove the connector from your AI tool's settings, then revoke the access token from **Settings
→ Authorized apps** in your Checkout Page dashboard.
Email [support@checkoutpage.com](mailto:support@checkoutpage.com) with the AI tool you're using and a
screenshot of the error.
## Security and privacy [#security-and-privacy]
* The MCP server only acts on your behalf after you sign in via OAuth.
* Tokens are scoped to your account. Revoke access anytime from your Checkout Page settings.
* Read tools (`list_*`, `get_*`) never change data. Write tools (`create_*`, `update_*`, `upload_*`, `archive_*`, `reorder_*`) and destructive tools (`delete_*`) are clearly named so your assistant can ask before making changes. Deletes cannot be undone, so confirm before your assistant runs a `delete_*` tool.
* Data your assistant looks up (customer details, payment info) is sent to your AI tool to generate a response, the same way it works for any other connector. Choose AI tools whose data-handling policies match your needs.
---
# checkout_page.created (/docs/api/v1/webhooks/checkout_page.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a checkout page is created
---
# checkout_page.updated (/docs/api/v1/webhooks/checkout_page.updated)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a checkout page is updated and when it is archived or restored. Changes to the checkout page's product trigger product.updated instead. Check status for the current state.
---
# Create a coupon (/docs/api/v1/coupons/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a coupon. A coupon has either an amountOff and currency or percentOff. The coupon created will be automatically synchronized with Stripe if you have a connected stripe account. If you are not connected the endpoint will throw an error. In addition to Checkout Page rate limits, Stripe [rate limits](https://docs.stripe.com/rate-limits) apply when creating coupons.
---
# Create a tax rate (/docs/api/v1/tax-rates/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a tax rate in Checkout Page and is automatically synchronized with Stripe. In addition to Checkout Page rate limits, Stripe [rate limits](https://docs.stripe.com/rate-limits) apply when creating tax rates.
---
# Create a theme (/docs/api/v1/themes/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a theme your pages can use. Start from an existing theme with `baseThemeId` to keep its full design, then set what should differ in `variables`. `variables` takes the same shape as a page's `themeOverrides`: keys you leave out keep their value, and `null` resets a property, or everything under a key. Order bump styling, which only the dashboard can change, is kept. Custom CSS needs a plan that includes it, and the Powered by badges can only be changed with a paid subscription.
---
# Create a webhook (/docs/api/v1/webhooks/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a webhook endpoint that receives the selected events at an https URL. This is the only API response that returns the signing `secret`; it can be viewed or rotated later in the dashboard. A seller can have at most 10 webhooks and each URL can be registered once. See the event pages in this section for payload shapes and the Signature page for verifying deliveries.
---
# Create booking (/docs/api/v1/bookings/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates an event booking without collecting card payment. The full booking flow runs: tickets are issued, a confirmation email with the ticket PDF is sent to the customer, live-mode bookings decrement ticket-type capacity, and booking webhooks fire. With a `paymentOption` the booking is recorded as `unpaid` and settled outside checkout via the chosen manual option (`invoice` or `cash_on_delivery`); a free booking simply has nothing due. With `complimentary: true` the booking is recorded as `paid` with `amount`, `amountPaid` and `amountDue` of 0 and `isComplimentary: true`, while the ticket lines keep their face value and `complimentaryDiscountAmount` records what they would have cost. Storefront rules are enforced: every required field on the event must be supplied, tickets must be on sale and in stock, and the event registration limit applies. Requires a paid plan and a connected Stripe account.
---
# Create checkout page (/docs/api/v1/checkout-pages/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a checkout page together with its product and default fields in a single request.
Use this endpoint whenever you need to accept payments. It supports four payment flows, controlled by the `productData.price` object:
* **One-time payment** — set `price.amount` and `price.currency`. Do not set `price.recurring`, `price.paymentPlan`, or `price.payWhatYouWant`.
* **Pay What You Want (PWYW)** — set `price.payWhatYouWant: true`. `price.amount` becomes the minimum; leave it at `0` to allow free purchases. PWYW cannot be combined with subscriptions or payment plans.
* **Subscription (recurring)** — set `price.recurring` with `interval` (`day`, `week`, `month`, or `year`) and `intervalCount`. Optionally add `trialPeriodDays` or a `billingCycleAnchorConfig`.
* **Payment plan (installments)** — set `price.paymentPlan` with `interval`, `intervalCount`, and `planIterations` (the fixed number of payments after which the plan completes).
`price.recurring` and `price.paymentPlan` are mutually exclusive — set only one.
Default name and email fields are always created automatically. Pass `fields` to add extra checkout fields.
---
# Create event (/docs/api/v1/events/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates an event with ticket groups and ticket types.
---
# Create field for checkout page (/docs/api/v1/checkout-pages/fields/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new custom field on the specified checkout page.
---
# Create field for event (/docs/api/v1/events/fields/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new custom field on the specified event.
---
# Create field for form (/docs/api/v1/forms/fields/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new custom field on the specified form.
---
# Create form (/docs/api/v1/forms/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a form for lead capture and submissions. The form includes custom fields and optional downloadable files available to customers after submission.
---
# Create ticket group (/docs/api/v1/events/ticket-groups/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new ticket group on the specified event page.
---
# Create ticket type (/docs/api/v1/events/ticket-groups/ticket-types/create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Creates a new ticket type within the specified ticket group.
---
# customer.created (/docs/api/v1/webhooks/customer.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a customer is created
---
# customer.updated (/docs/api/v1/webhooks/customer.updated)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a customer is updated
---
# Delete a tax rate (/docs/api/v1/tax-rates/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Soft-deletes a tax rate. The rate is removed from any pages that reference it; those pages keep working but tax is no longer applied to them. The Stripe tax rate object is left intact for historical record.
---
# Delete a theme (/docs/api/v1/themes/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes a theme your account created. A theme that pages still use, or that is your store's default theme, can't be deleted: move those pages to another theme first. Built-in themes can't be deleted.
---
# Delete a webhook (/docs/api/v1/webhooks/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes a webhook. Deliveries stop immediately; the delivery history is retained. The response is the final state of the webhook — a later read of the same id returns 404. The returned `status` is the value before deletion; deletion does not change it.
---
# Delete field for checkout page (/docs/api/v1/checkout-pages/fields/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Permanently deletes a field from the specified checkout page.
---
# Delete field for event (/docs/api/v1/events/fields/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Permanently deletes a field from the specified event.
---
# Delete field for form (/docs/api/v1/forms/fields/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Permanently deletes a field from the specified form.
---
# Delete file (/docs/api/v1/files/delete)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Deletes a file.
---
# Download file (/docs/api/v1/files/download)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generates a temporary presigned URL for downloading a private file. Only works for files in the "files" bucket. Images are publicly accessible via their location URL.
---
# Download the ticket PDF (/docs/api/v1/bookings/download-ticket-pdf)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the booking's ticket PDF as the response body (`application/pdf`), containing every ticket in the booking. The response is the file itself, not JSON. Each request returns the current PDF, so ticket changes (for example attendee updates) are reflected on the next download. This endpoint requires your API key, so never call it from a browser, mobile app, or email link - proxy it through your own server instead: your user clicks a download button in your app, your server calls this endpoint and streams the file back. To link to the PDF from an email or host it at a stable URL, download it and attach or re-host it yourself. Returns 404 when no PDF exists, for example when the booking is unpaid or abandoned.
---
# Download ticket PDFs (/docs/build/download-ticket-pdfs)
Every paid event booking has a ticket PDF containing all tickets for that booking. You can securely download the latest version through the API and then serve it through your own app, attach it to emails, or store a copy yourself. The PDF endpoint requires your Checkout Page API key, so it should only be called from your server - never directly from a customer's browser or email. Each download returns the current ticket PDF.
## Prerequisites [#prerequisites]
To get the most out of this guide, you'll need:
* A Checkout Page [API key](/docs/api/authentication)
* An event with at least one paid booking
## 1. Install [#1-install]
Get the Checkout Page Node.js SDK, or skip this step and use plain `fetch`:
npm
yarn
pnpm
```bash
npm install @checkoutpage/sdk
```
```bash
yarn add @checkoutpage/sdk
```
```bash
pnpm add @checkoutpage/sdk
```
## 2. Get a booking id [#2-get-a-booking-id]
Any of these gives you the `id` to download with:
* a [`booking.paid`](/docs/api/v1/webhooks/booking.paid) webhook, delivered when a booking is paid
* [List all bookings](/docs/api/v1/bookings/list), filtered by customer, order or search
* [Get booking details](/docs/api/v1/bookings/get), if you already store booking ids
## 3. Download the PDF [#3-download-the-pdf]
```bash
curl https://api.checkoutpage.com/v1/bookings/{bookingId}/ticket-pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-o tickets.pdf
```
The response body is the PDF file itself (`application/pdf`), not JSON. It returns `404` when no PDF exists, for example while a booking is unpaid or abandoned.
## 4. Serving the ticket PDF from your app [#4-serving-the-ticket-pdf-from-your-app]
Your [API key](/docs/api/authentication) must stay on your server. Never call this endpoint directly from a browser, email or mobile app - always proxy through your own server.
In your app, add a download button that calls your server; your server securely fetches the PDF and returns it.
SDK
fetch
```js
import { createCheckoutPageClient, NotFoundError } from '@checkoutpage/sdk';
const client = createCheckoutPageClient({ apiKey: process.env.CHECKOUTPAGE_API_KEY });
app.get('/my-tickets/:bookingId', requireLogin, async (req, res) => {
try {
const pdf = await client.bookings.downloadTicketPdf(req.params.bookingId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="tickets.pdf"');
res.send(Buffer.from(pdf));
} catch (err) {
if (err instanceof NotFoundError) return res.sendStatus(404);
throw err;
}
});
```
```js
app.get('/my-tickets/:bookingId', requireLogin, async (req, res) => {
const response = await fetch(
`https://api.checkoutpage.com/v1/bookings/${req.params.bookingId}/ticket-pdf`,
{ headers: { Authorization: `Bearer ${process.env.CHECKOUTPAGE_API_KEY}` } }
);
if (!response.ok) return res.sendStatus(404);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="tickets.pdf"');
res.send(Buffer.from(await response.arrayBuffer()));
});
```
From there it's up to you: stream it as a download, render it inline, or attach it to an email you send.
Check that the booking belongs to the signed-in customer before returning the PDF. Your API key
can download any booking's tickets, so your server is the place to enforce who sees what.
## Related guides [#related-guides]
* [Render ticket QR codes](/docs/build/render-ticket-qr-codes)
---
# Get a webhook (/docs/api/v1/webhooks/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one webhook by id, including its subscribed events, status and delivery counters. The signing secret is not returned. The API returns it only when the webhook is created, and it can be viewed or rotated in the dashboard.
---
# Get account details (/docs/api/v1/account/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the identity of the seller account the API key belongs to: store slug, display name and logo.
---
# Get booking details (/docs/api/v1/bookings/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one event booking by ID, including customer and billing details, status, tax breakdown, the purchased tickets, and refund history when present.
---
# Get checkout page (/docs/api/v1/checkout-pages/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns detailed information for a specific checkout page.
---
# Get customer details (/docs/api/v1/customers/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns detailed information for a specific customer.
---
# Get event (/docs/api/v1/events/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns detailed information for a specific event.
---
# Get field for checkout page (/docs/api/v1/checkout-pages/fields/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a single field configured on the specified checkout page.
---
# Get field for event (/docs/api/v1/events/fields/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a single field configured on the specified event.
---
# Get field for form (/docs/api/v1/forms/fields/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves a single field configured on the specified form.
---
# Get form (/docs/api/v1/forms/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns detailed information for a specific form.
---
# Get invoice details (/docs/api/v1/invoices/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one invoice by ID, including line items, tax breakdown, fees, buyer and seller billing details, and a freshly signed `invoiceUrl` for the hosted PDF.
---
# Get payment details (/docs/api/v1/payments/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one payment by ID, including customer and billing details, status, tax breakdown, the purchased price snapshot, and refund history when present.
---
# Get product (/docs/api/v1/products/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves the details of an existing product by its ID.
---
# Get submission details (/docs/api/v1/submissions/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one form submission by ID, including captured customer details, billing/shipping data, submitted field values, and request metadata when present.
---
# Get subscription details (/docs/api/v1/subscriptions/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one subscription by ID, including billing details, status, schedule, coupon snapshot, and refund history when present.
---
# Get subscription payment details (/docs/api/v1/subscription-payments/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one subscription payment by ID — a single charge in a subscription billing cycle, including its payment and invoice status, billing reason, payment method, and Stripe references. To list every payment on a subscription, use the list endpoint with `subscriptionId`.
---
# Get ticket group (/docs/api/v1/events/ticket-groups/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a single ticket group configured on the specified event page.
---
# Get ticket type (/docs/api/v1/events/ticket-groups/ticket-types/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a single ticket type configured within the specified ticket group.
---
# Introduction (/docs/api)
The Checkout Page API is organized around REST principles. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.
## Base URL [#base-url]
All API requests should be made to:
```
https://api.checkoutpage.com
```
## Versioning [#versioning]
The Checkout Page API uses URL-based versioning. The current stable version is **v1**. All API endpoints are prefixed with the version number:
```
https://api.checkoutpage.com/v1/{endpoint}
```
We maintain backwards compatibility within each major version. When we introduce breaking changes, we release a new major version (e.g., v2). You'll have ample time to migrate before we deprecate older versions.
## Rate limiting [#rate-limiting]
API requests are rate limited based on the store and api key to ensure fair usage and system stability.
By default, stores are limited to:
* **500 requests per minute**
By default, API keys are limited to:
* **100 requests per minute**
If you exceed your api key or store rate limit, you'll receive a `429 Too Many Requests`.
Need higher rate limits? Contact our [support team](/docs/support) to discuss custom rate limits
for your use case.
## Authentication [#authentication]
The Checkout Page API uses API key authentication with Bearer tokens. All API requests must be authenticated.
```bash
curl https://api.checkoutpage.com/v1/payments \
-H "Authorization: Bearer YOUR_API_KEY"
```
Learn more about authentication in our [Authentication guide](/docs/api/authentication).
## HTTP status codes [#http-status-codes]
The Checkout Page API uses conventional HTTP response codes to indicate the success or failure of an API request.
| Status Code | Description |
| ----------- | ------------------------------------------------------- |
| `200` | Successful request |
| `201` | Resource created successfully |
| `400` | Bad request - Invalid request parameters |
| `401` | Unauthorized - Invalid or missing API key |
| `403` | Forbidden - API key lacks required permissions |
| `404` | Not found - The requested resource doesn't exist |
| `429` | Too many requests - Rate limit exceeded |
| `500` | Internal server error - Something went wrong on our end |
## Errors [#errors]
When an error occurs, the API returns a JSON response with the following structure:
```json
{
"status": "error",
"type": "error",
"message": "Human-readable error description"
}
```
### Example error response [#example-error-response]
```json
{
"status": "error",
"type": "error",
"message": "Missing or invalid API key"
}
```
## Pagination [#pagination]
List endpoints that return multiple items are paginated using cursor-based pagination. Results are returned in reverse chronological order (newest first). These endpoints accept the following query parameters:
| Parameter | Type | Default | Description |
| ---------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `limit` | integer | 20 | Number of items per page. Minimum value is 1, maximum is 100. |
| `starting_after` | string | - | A cursor (resource ID) to start after. Retrieves items that appear after this cursor in the list. Cannot be used with `ending_before`. |
| `ending_before` | string | - | A cursor (resource ID) to end after. Retrieves items that appear before this cursor in the list. Cannot be used with `starting_after`. |
### Paginated response [#paginated-response]
```json
{
"data": [...],
"has_more": true,
"total": 150
}
```
The `has_more` field indicates whether there are additional items available. Use the ID of the last item in the `data` array with `starting_after` to fetch the next page.
### Pagination examples [#pagination-examples]
**Get the first page:**
```javascript
const response = await fetch('https://api.checkoutpage.com/v1/payments?limit=50', {
headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
const firstPage = await response.json();
```
**Get the next page** using `starting_after` with the last item's ID:
```javascript
const lastId = firstPage.data[firstPage.data.length - 1].id;
const response = await fetch(
`https://api.checkoutpage.com/v1/payments?limit=50&starting_after=${lastId}`,
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } },
);
const nextPage = await response.json();
```
**Get the previous page** using `ending_before` with the first item's ID:
```javascript
const firstId = nextPage.data[0].id;
const response = await fetch(
`https://api.checkoutpage.com/v1/payments?limit=50&ending_before=${firstId}`,
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } },
);
const previousPage = await response.json();
```
## Timestamps [#timestamps]
All timestamps are returned in ISO 8601 format:
```
2024-01-15T10:30:00.000Z
```
## Getting started [#getting-started]
Ready to get started? Follow these steps:
1. [Create an account](https://checkoutpage.com) on Checkout Page
2. [Generate your API key](/docs/api/authentication#getting-your-api-key) from the dashboard
3. [Make your first API request](/docs/api/v1/customers/list) to verify your integration
## API Resources [#api-resources]
Explore our API endpoints organized by resource:
## Support [#support]
Need help? We're here for you:
* **Support**: Reach out to [our support team](https://checkoutpage.com/contact) for questions or issues.
* **Knowledge base**: Browse [our knowledge base](/docs) for guides, tutorials, and frequently asked questions.
* **API reference**: Browse [our comprehensive API documentation](/docs/api) for developers.
* **Developer support**: Get help from our technical team for integration or development issues.
---
# List all bookings (/docs/api/v1/bookings/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your event bookings. The bookings are sorted in order, with the most recent bookings appearing first.
---
# List all coupons (/docs/api/v1/coupons/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your coupons. The coupons are sorted in order, with the most recent coupons appearing first.
---
# List all customers (/docs/api/v1/customers/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your customers. The customers are sorted in order, with the most recent customers appearing first.
---
# List all invoices (/docs/api/v1/invoices/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of invoices. Invoices belong to payments and bookings only. Filter by `chargeId` to retrieve the invoice(s) for a specific payment or booking. Sorted with the most recent invoices first.
---
# List all payments (/docs/api/v1/payments/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your payments. The payments are sorted in order, with the most recent payments appearing first.
---
# List all submissions (/docs/api/v1/submissions/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of form submissions owned by the authenticated seller, sorted newest-first. Supports filtering by customer, form page, status, lifecycle status (`orderStatus`), and UTC creation timestamps. Canceled submissions are included unless `orderStatus` filters them out.
---
# List all subscription payments (/docs/api/v1/subscription-payments/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your subscription payments. The subscription payments are sorted in order, with the most recent payments appearing first.
---
# List all subscriptions (/docs/api/v1/subscriptions/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your subscriptions. The subscriptions are sorted in order, with the most recent subscriptions appearing first.
---
# List all tickets (/docs/api/v1/tickets/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of tickets across your event bookings, sorted with the most recent tickets appearing first.
---
# List checkout pages (/docs/api/v1/checkout-pages/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your checkout pages.
---
# List events (/docs/api/v1/events/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your events.
---
# List fields for checkout page (/docs/api/v1/checkout-pages/fields/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all fields configured on the specified checkout page, ordered by display order.
---
# List fields for event (/docs/api/v1/events/fields/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all fields configured on the specified event, ordered by display order.
---
# List fields for form (/docs/api/v1/forms/fields/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieves all fields configured on the specified form, ordered by display order.
---
# List forms (/docs/api/v1/forms/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns a paginated list of your forms.
---
# List tax rates (/docs/api/v1/tax-rates/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns all tax rates. Tax rates are sorted in order, with the most recent tax rates appearing first.
---
# List themes (/docs/api/v1/themes/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the themes your pages can use: the themes your account created and the built-in (`global`) themes, sorted by id from highest to lowest, so a theme you create appears before older ones. Built-in themes were installed together, so their order doesn't follow `createdAt`. A retired built-in theme is only listed while one of your pages uses it.
---
# List ticket groups (/docs/api/v1/events/ticket-groups/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns all ticket groups configured on the specified event page.
---
# List ticket types (/docs/api/v1/events/ticket-groups/ticket-types/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns all ticket types configured within the specified ticket group.
---
# List webhooks (/docs/api/v1/webhooks/list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the webhooks registered for the account, newest first, with delivery counters and the time of the last attempt. Secrets are never returned. Filter by `status` or by a single `event`.
---
# payment.abandoned (/docs/api/v1/webhooks/payment.abandoned)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a payment is abandoned. A payment is abandoned when a customer starts but does not complete the checkout. [Learn more](https://checkoutpage.com/docs/articles/cart-abandonment)
---
# payment.canceled (/docs/api/v1/webhooks/payment.canceled)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a payment is canceled
---
# payment.created (/docs/api/v1/webhooks/payment.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a new payment is created
---
# payment.paid (/docs/api/v1/webhooks/payment.paid)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a payment is successfully paid
---
# product.created (/docs/api/v1/webhooks/product.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a product is created
---
# product.updated (/docs/api/v1/webhooks/product.updated)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a product is updated
---
# Regenerate an invoice PDF (/docs/api/v1/invoices/regenerate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Regenerates the PDF for an invoice and returns the updated invoice and PDF. Refreshes billing details, PO number, and the denormalized customer / product fields from the source Charge. Preserves line items, amounts, taxes, fees, and status. The previous PDF is overwritten in place; the returned `invoiceUrl` is a freshly signed link.
---
# Render ticket QR codes (/docs/build/render-ticket-qr-codes)
Every ticket has a `checkInCode`, which is the credential the Checkout Page check-in scanner reads at the door. You can encode this value as a QR code to build your own tickets in your app, including your own ticket PDFs.
## Prerequisites [#prerequisites]
To get the most out of this guide, you'll need:
* A Checkout Page [API key](/docs/api/authentication)
* An event with at least one paid booking
## 1. Install [#1-install]
Get a QR code library, plus the Checkout Page Node.js SDK if you prefer it over plain `fetch`:
npm
yarn
pnpm
```bash
npm install qrcode @checkoutpage/sdk
```
```bash
yarn add qrcode @checkoutpage/sdk
```
```bash
pnpm add qrcode @checkoutpage/sdk
```
## 2. Get the check-in code [#2-get-the-check-in-code]
A booking produces one ticket per attendee. Fetch them with [List all tickets](/docs/api/v1/tickets/list), filtered by `bookingId` (from a [`booking.paid`](/docs/api/v1/webhooks/booking.paid) webhook or the [bookings endpoints](/docs/api/v1/bookings/list)):
SDK
curl
```js
import { createCheckoutPageClient } from '@checkoutpage/sdk';
const client = createCheckoutPageClient({ apiKey: process.env.CHECKOUTPAGE_API_KEY });
const tickets = await client.tickets.list({ bookingId });
```
```bash
curl "https://api.checkoutpage.com/v1/tickets/?bookingId={bookingId}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Each ticket in the response includes its `checkInCode`.
## 3. Render it as a QR code [#3-render-it-as-a-qr-code]
Encode the value with any QR library. In the browser with [`qrcode`](https://www.npmjs.com/package/qrcode):
```js
import QRCode from 'qrcode';
await QRCode.toCanvas(document.getElementById('ticket-qr'), ticket.checkInCode);
```
Scanning the rendered QR code with the Checkout Page check-in scanner admits the guest. It is the same QR code printed on the official ticket PDF. Your rendered QR code and the PDF stay interchangeable at the door.
The check-in code is the admission credential, so show it only to the ticket holder. Treat the
value as opaque - its format may change. Duplicate scans are flagged to door staff.
## Related guides [#related-guides]
* [Download ticket PDFs](/docs/build/download-ticket-pdfs)
---
# Retrieve a theme (/docs/api/v1/themes/get)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns one theme: a theme your account created or a built-in (`global`) theme.
---
# submission.canceled (/docs/api/v1/webhooks/submission.canceled)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a form submission is canceled
---
# submission.created (/docs/api/v1/webhooks/submission.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a form is submitted
---
# submission.updated (/docs/api/v1/webhooks/submission.updated)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a form submission is updated
---
# subscription.abandoned (/docs/api/v1/webhooks/subscription.abandoned)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is abandoned. A subscription is abandoned when a customer starts but does not complete the checkout. [Learn more](https://checkoutpage.com/docs/articles/cart-abandonment)
---
# subscription.active (/docs/api/v1/webhooks/subscription.active)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is successfully created and becomes active.
---
# subscription.canceled (/docs/api/v1/webhooks/subscription.canceled)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is updated to be canceled. A subscription can be canceled immediately, at the end of the subscription period or on a custom date. This webhook does not mean the subscription is expired.
---
# subscription.completed (/docs/api/v1/webhooks/subscription.completed)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when after all payments in a payment plan have been paid.
---
# subscription.created (/docs/api/v1/webhooks/subscription.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is created. Subscriptions are created after a customer has submitted your checkout. A created subscription may or may not transition into an active, trialing or scheduled subscription depending on whether the payment is successful.
---
# subscription.expired (/docs/api/v1/webhooks/subscription.expired)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a canceled subscription reaches its scheduled canceled date and transitions to expired.
---
# subscription.scheduled (/docs/api/v1/webhooks/subscription.scheduled)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is successfully created in a scheduled state.
---
# subscription.trialing (/docs/api/v1/webhooks/subscription.trialing)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a subscription is successfully created in a trialing state.
---
# Test an upsell funnel (/docs/build/test-upsell-funnel)
A one-click upsell shows the buyer a second offer straight after they pay, and charges it without asking for their card again. In this guide you build a two-page funnel with the API, run it in [test mode](/docs/build/test-mode) once accepting the offer and once declining it, and check the records each run created. No money is taken.
While test mode is on, anyone who visits these pages gets the products without paying. Switch it
off on both pages as soon as you finish testing.
## Prerequisites [#prerequisites]
To get the most out of this guide, you'll need:
* A Checkout Page [API key](/docs/api/authentication)
* A product for the first page and one for the offer. The requests below create both, so you only need a name and a price for each.
* Your Stripe account connected, with card payments turned on. One-click upsells charge the buyer's saved card.
## 1. Create the upsell page [#1-create-the-upsell-page]
Create the page that makes the offer first. The first page's upsell step must point at a page that already exists.
```bash
curl -X POST https://api.checkoutpage.com/v1/checkout-pages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Workshop recording upsell",
"productData": {
"title": "Workshop recording",
"price": { "amount": 2900, "currency": "usd" }
},
"testmode": true
}'
```
`amount` is in the smallest currency unit, so `2900` is $29.00. Note the `id` in the response. It's the `{upsellPageId}` in the next steps.
## 2. Create the first page with an upsell step [#2-create-the-first-page-with-an-upsell-step]
Now create the page the buyer starts on, with a `funnelSteps` entry that points at the upsell page:
```bash
curl -X POST https://api.checkoutpage.com/v1/checkout-pages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pricing strategy workshop",
"productData": {
"title": "Pricing strategy workshop",
"price": { "amount": 19900, "currency": "usd" }
},
"funnelSteps": [
{
"type": "upsell",
"order": 1,
"enabled": true,
"config": { "action": "checkout", "pageId": "{upsellPageId}" }
}
],
"testmode": true
}'
```
Note the `id` and `url` in the response. The `id` is the `{firstPageId}` in the next steps.
The `checkout` action shows the offer on the same checkout, using the page in `pageId`. In the dashboard, this is **One-click upsell** set to **Show one-click upsell**, with **After payment step** set to **Show upsell on same page**.
A `checkout` action upsell picks up test mode from the first purchase, so switching on the first page alone is enough for a test run. Switching both, as here, also lets you test the upsell page on its own.
To add an upsell to a page you already have, send `funnelSteps` in a `PATCH` to `/v1/checkout-pages/{pageId}`. An update replaces the whole `funnelSteps` array, so include every step you want to keep.
## 3. Run the funnel and accept the offer [#3-run-the-funnel-and-accept-the-offer]
1. Open the first page's `url`. Check that you see the **Test payments only** banner.
2. Pay with a new email address and the test card 4242 4242 4242 4242, with any future expiry date and any CVC.
3. When the offer appears, accept it. It goes through without asking for the card again.
## 4. Run it again and decline [#4-run-it-again-and-decline]
1. Open the first page's `url` again.
2. Pay with a different new email address and the same test card.
3. When the offer appears, decline it with the **No thanks, I don't want this** link, or the label you set for it.
## 5. Check the records [#5-check-the-records]
List the test payments on each page with `livemode=false`:
```bash
# The first page: one payment per run
curl "https://api.checkoutpage.com/v1/payments?pageId={firstPageId}&livemode=false" \
-H "Authorization: Bearer YOUR_API_KEY"
# The upsell page: one payment, from the run where you accepted
curl "https://api.checkoutpage.com/v1/payments?pageId={upsellPageId}&livemode=false" \
-H "Authorization: Bearer YOUR_API_KEY"
```
What to expect:
* The first page has two test payments, one for each email address.
* The upsell page has one test payment, from the run where you accepted. It has `upsell: true`, `upsellChargeId` set to that run's first payment, and `upsellPageId` set to the first page, where the funnel started.
* Declining creates no record, so the run where you declined has nothing on the upsell page.
* Without `livemode=false`, a live API key leaves these payments out of both lists.
The upsell payment, trimmed:
```json
{
"data": [
{
"id": "{upsellPaymentId}",
"pageId": "{upsellPageId}",
"customerEmail": "first-run@example.com",
"amount": 2900,
"currency": "usd",
"status": "paid",
"livemode": false,
"upsell": true,
"upsellChargeId": "{firstPaymentId}",
"upsellPageId": "{firstPageId}"
}
]
}
```
If the offer sells a subscription, the upsell record is a subscription: list `/v1/subscriptions?pageId={upsellPageId}&livemode=false` instead. If the first page sells a subscription, the upsell record has `upsellSubscriptionId`, the first subscription, in place of `upsellChargeId`.
## 6. Switch test mode off [#6-switch-test-mode-off]
Switch test mode off on both pages when you finish. While it's on, anyone who visits gets the product without paying.
```bash
curl -X PATCH https://api.checkoutpage.com/v1/checkout-pages/{firstPageId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "testmode": false }'
curl -X PATCH https://api.checkoutpage.com/v1/checkout-pages/{upsellPageId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "testmode": false }'
```
## Upsells on your own site (redirect action) [#upsells-on-your-own-site-redirect-action]
With the `redirect` action, the offer lives on your own site: the buyer is sent to `config.redirect.url`, a page of yours with a Checkout Page embed or popup for the offer. In the dashboard, this is **One-click upsell** set to **Show one-click upsell**, with **After payment step** set to **Redirect to url with upsell**.
```json
{
"funnelSteps": [
{
"type": "upsell",
"order": 1,
"enabled": true,
"config": {
"action": "redirect",
"redirect": { "url": "https://example.com/special-offer" }
}
}
]
}
```
To test it, follow the steps above with these differences:
* **Switch test mode on for both pages**: the first page, and the page embedded at the redirect URL. A redirect upsell doesn't pick up test mode from the first purchase.
* **Match the URL exactly.** The URL in the step must match the page the checkout is embedded on: same origin and same path, including any trailing slash. `https://example.com/special-offer/` doesn't match `https://example.com/special-offer`, and `https://www.example.com` doesn't match `https://example.com`. The query string is ignored.
* **Use one checkout host.** Both steps must load the checkout from the same host: your custom domain or your checkoutpage.com subdomain, not one of each. Check the checkout URL in both embed codes.
* **Stay on one site.** Embed the first checkout and the offer on the same website.
## Test with an AI agent [#test-with-an-ai-agent]
If you use the [Checkout Page MCP server](/docs/build/mcp), the `test_upsell_funnel` prompt tests a funnel you already have. It doesn't create pages, so build the funnel first, with steps 1 and 2 or in the dashboard. Give it the first page's id as `initialPageId`, and optionally the offer's page as `upsellPageId`. Your AI assistant then:
1. Finds the first page's upsell step. If there is no enabled upsell step, it stops and tells you.
2. Notes both pages' current test mode, then switches test mode on for both.
3. Runs the funnel once accepting and once declining the offer, each with a new email address.
4. Checks the records with `livemode: false`.
5. Sets each page's test mode back to the value it noted. A page that was already in test mode stays in test mode.
In Claude Code, for example, type the prompt name followed by the first page's id:
```text
/mcp__checkoutpage__test_upsell_funnel {firstPageId}
```
In clients without prompts, ask in plain words, for example *"Test the upsell funnel on my Pricing strategy workshop checkout in test mode."*
To pay, your assistant needs a browser tool, such as Claude in Chrome, a Playwright MCP server or ChatGPT agent mode. Without one, it gives you the link to pay yourself, then carries on.
## Troubleshooting [#troubleshooting]
The upsell only works when both steps load from the same checkout host (your custom domain or your checkoutpage.com subdomain, not a mix), and the buyer reaches the offer within an hour of the first payment. Most problems come down to one of these rules.
### The offer never appears [#the-offer-never-appears]
* The two steps load the checkout from different hosts. Use the same one for both.
* More than an hour passed between the first payment and the offer.
* Redirect action: the URL in the step doesn't exactly match the page the offer is embedded on. Check the trailing slash, `www.` and `https`.
* The upsell step isn't `enabled`, or isn't the first enabled `upsell` step in `funnelSteps`.
### The offer asks for the card again [#the-offer-asks-for-the-card-again]
* One-click charges only work when the first payment was made by card. After Apple Pay, Google Pay, Link or a bank payment, the offer shows a card form. Test purchases never charge a card, so this shows up on live payments only.
* In a test run, a card form usually means the offer page loaded as a normal checkout instead of an offer. Check the rules under "The offer never appears".
### Declining the offer does nothing [#declining-the-offer-does-nothing]
If **No thanks, I don't want this** (or your own label for it) does nothing, the offer can't find the first payment. This happens when the two steps load the checkout from different hosts, or when the embeds sit on different websites. Load both from the same checkout host, on the same site.
## Next steps [#next-steps]
---
# Test mode (/docs/build/test-mode)
Test mode lets you buy from your own checkout page, event or form without paying. Use it to check the whole flow before you take real payments: the checkout itself, the emails and files your buyer receives, your webhooks and your integrations. Each page has its own switch, and while it's on, every purchase on that page is a test purchase.
While test mode is on, anyone who visits the page gets the product without paying. Switch it off
as soon as you finish testing.
## How test mode works [#how-test-mode-works]
Test mode is a switch on each checkout page, event and form. You test the real page, with the same URL, products and settings your buyers see.
While the switch is on:
* Every purchase on the page is a test purchase. No money is taken and no payment is created in Stripe.
* The buyer still gets everything the page delivers: confirmation emails, files, license keys, tickets, customer portal access, integrations and webhooks.
* The records the purchase creates, such as the payment, booking or submission, have `livemode: false`.
* Every visitor sees a **Test payments only** banner on the page.
This works the same on your hosted page, your custom domain, embeds and popups. Nothing changes in your embed code.
Switch test mode off and the page takes live payments again straight away.
## What test mode labels [#what-test-mode-labels]
The records in the first column carry a `livemode` field: `false` for a test purchase and `true` for a live one. Everything in the second column has no mode and is shared between live and test purchases.
| Carries `livemode` | Shared between live and test |
| --------------------- | --------------------------------------------------------------- |
| Payments | Checkout pages, events and forms (they have the switch instead) |
| Bookings | Products |
| Subscriptions | Coupons |
| Subscription payments | Tax rates |
| Invoices | Webhooks |
| Form submissions | Custom fields |
| Tickets | Files |
| | Customers |
| | License keys |
## There are no test customers [#there-are-no-test-customers]
There is no test checkout, test event or test customer. Checkout Page matches buyers to customers by email address, so one person has one customer record across all of their purchases, live or test. A test purchase creates or reuses the same customer record a live purchase with that email address would, and the customer isn't labelled as test.
This means:
* If you test with a real customer's email address, the test purchase appears on their customer record. Use a new email address for each test.
* `customer.created` webhooks carry no `livemode`, so a new customer from a test purchase looks like any other.
* If your Stripe account is connected, a new email address also creates a customer in Stripe, just as a live purchase would.
To find the customers behind your test purchases, [list your test payments](#read-test-data) and read each payment's `customerId`.
## Turn test mode on [#turn-test-mode-on]
### In the dashboard [#in-the-dashboard]
1. Open the page in the page editor.
2. Click **Test payments** in the toolbar. On a form it's called **Test submissions**.
3. Under **Where should test payments work?** (**Where should test submissions work?** on a form), choose **Everywhere**.
**Preview only** lets you make test payments inside the dashboard preview, but it doesn't switch the page: your visitors keep paying for real. Only **Everywhere** puts the page in test mode.
To switch it off, click **Turn off test mode**.
### With the API [#with-the-api]
Set `testmode` when you create or update a checkout page, event or form:
```bash
curl -X PATCH https://api.checkoutpage.com/v1/checkout-pages/{pageId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "testmode": true }'
```
For an event, use `/v1/events/{eventId}`, and for a form, `/v1/forms/{formId}`. To create a page that starts in test mode, send `"testmode": true` in the `POST` body.
The response includes `testmode`, and `testmodeEnabledAt`, the time test mode was switched on. `testmodeEnabledAt` is read-only and is `null` while test mode is off. Trimmed:
```json
{
"data": {
"id": "{pageId}",
"type": "checkout",
"status": "published",
"testmode": true,
"testmodeEnabledAt": "2026-09-14T13:15:06.239Z"
}
}
```
To find pages you left in test mode, filter the list with `testmode=true`. `/v1/events` and `/v1/forms` take the same filter, and `testmode=false` returns only pages that are not in test mode.
```bash
curl "https://api.checkoutpage.com/v1/checkout-pages?testmode=true" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### With the MCP server [#with-the-mcp-server]
With the [Checkout Page MCP server](/docs/build/mcp) connected, ask your AI assistant, for example *"Put my Spring Workshop checkout in test mode."* It calls `update_checkout_page`, `update_event` or `update_form` with `testmode: true`. To find pages still in test mode, ask *"Which of my pages are in test mode?"* and it calls `list_checkout_pages`, `list_events` and `list_forms` with `testmode: true`.
## Make a test purchase [#make-a-test-purchase]
1. Switch test mode on for the page.
2. Open the page wherever your buyers use it: its URL, your custom domain, or your own site with the embed or popup. Check that you see the **Test payments only** banner. In a popup, it sits above the title.
3. Fill in the checkout with a new email address you can read, and pay with the test card:
* Card number: 4242 4242 4242 4242
* Expiry: any future date
* CVC: any 3 digits
* Postcode: any
4. Check your inbox for the confirmation email, and your webhook endpoint or integration for the events.
Real cards are rejected while test mode is on, so you can't be charged by mistake.
Embeds and popups need no changes. Test mode belongs to the page, so your embed or popup code stays the same. If an older guide had you add `?preview=true&testing=true&testingPayments=true` to the checkout URL, you don't need those parameters: they don't put the page in test mode for your visitors. Use the switch instead.
## Read test data [#read-test-data]
Test records are kept apart from live ones. To read them with your API key, add `livemode=false`:
```bash
# List the test payments on a page
curl "https://api.checkoutpage.com/v1/payments?pageId={pageId}&livemode=false" \
-H "Authorization: Bearer YOUR_API_KEY"
# Get one test payment
curl "https://api.checkoutpage.com/v1/payments/{paymentId}?livemode=false" \
-H "Authorization: Bearer YOUR_API_KEY"
```
`livemode=false` works on these endpoints:
| Records | Endpoints |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Payments | [List](/docs/api/v1/payments/list), [get](/docs/api/v1/payments/get) |
| Bookings | [List](/docs/api/v1/bookings/list), [get](/docs/api/v1/bookings/get), [download ticket PDF](/docs/api/v1/bookings/download-ticket-pdf) |
| Subscriptions | [List](/docs/api/v1/subscriptions/list), [get](/docs/api/v1/subscriptions/get) |
| Subscription payments | [List](/docs/api/v1/subscription-payments/list), [get](/docs/api/v1/subscription-payments/get) |
| Invoices | [List](/docs/api/v1/invoices/list), [get](/docs/api/v1/invoices/get) |
| Form submissions | [List](/docs/api/v1/submissions/list), [get](/docs/api/v1/submissions/get) |
| Tickets | [List](/docs/api/v1/tickets/list) |
Without `livemode=false`, a live API key returns live records only. Fetching a test record by its id without it returns `404`.
With the MCP server, pass `livemode: false` to the list and get tools for the same records, and to `list_tickets`.
Keys that start with `sk_test_` can only read test data: `livemode=true` returns `403` with the message `Test API keys can only read test data. Remove livemode=true or use a live API key.` See [Authentication](/docs/api/authentication#test-mode) for how API keys and test mode fit together.
## Webhooks [#webhooks]
Webhooks fire for test purchases just as they do for live ones, so you can test your handler end to end. `livemode` sits on the object inside `data`, for example `data.payment.livemode` or `data.booking.livemode`. The event itself has no top-level `livemode`. Trimmed:
```json
{
"event": "payment.paid",
"eventId": "payment_paid_{paymentId}",
"sellerId": "{sellerId}",
"timestamp": "2026-09-14T13:20:11.000Z",
"data": {
"payment": {
"id": "{paymentId}",
"status": "paid",
"livemode": false
}
}
}
```
Branch on it in your handler, for example to skip shipping an order. [Verify the signature](/docs/api/v1/webhooks/signature) first, as for any webhook:
```js
// payload is the webhook body, after you verify its signature
const [record] = Object.values(payload.data);
if (record.livemode === false) {
// A test purchase: skip anything that should only happen for a real order.
}
```
`customer.created` and `customer.updated` events carry no `livemode`, because customers have no mode.
## Limitations [#limitations]
* Cards that normally decline or ask for 3D Secure succeed, because the payment never goes to Stripe. You can't test failed payments or payment retries in test mode.
* License keys from a test purchase aren't labelled as test.
* Test purchases don't use up stock or ticket quantities, don't count towards a coupon's usage limit, and aren't recorded in Stripe Tax.
* Subscriptions still need your Stripe account connected.
* A page the buyer reaches after checkout through the after-payment "go to another checkout" action isn't an upsell, so it doesn't pick up test mode from the first purchase. Switch test mode on for that page too.
* A one-click upsell shown on your own site (the redirect action) doesn't pick up test mode from the first purchase either. See [Test an upsell funnel](/docs/build/test-upsell-funnel).
## Next steps [#next-steps]
---
# ticket.created (/docs/api/v1/webhooks/ticket.created)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Triggered when a ticket is created for an event booking
---
# Update a coupon (/docs/api/v1/coupons/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates a coupon. Ticket types are scoped through pages, so any ticketTypeIds must belong to the pageIds sent in the same request, or — when pageIds are omitted — to the pages already on the coupon.
---
# Update a customer (/docs/api/v1/customers/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the editable details of a customer. Only the fields you supply are changed; omitted fields are left as-is. The `address` and `shipping` subdocuments follow patch semantics — supply only the sub-fields you want to change, and the rest are preserved. Send `null` for any optional field (or for a whole subdocument) to clear it. The primary `email` field cannot be cleared. Requests with an empty body are rejected.
---
# Update a tax rate (/docs/api/v1/tax-rates/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates a tax rate. In addition to Checkout Page rate limits, Stripe [rate limits](https://docs.stripe.com/rate-limits) apply when updating `displayName`. Display name changes are synced to Stripe.
---
# Update a theme (/docs/api/v1/themes/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Changes a theme your account created. Every page using it changes too. `variables` takes the same shape as a page's `themeOverrides`: keys you leave out keep their value, and `null` resets a property, or everything under a key. Order bump styling, which only the dashboard can change, is kept. Custom CSS needs a plan that includes it, and the Powered by badges can only be changed with a paid subscription. Built-in themes are read-only.
---
# Update a ticket (/docs/api/v1/tickets/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the attendee details and metadata held on a ticket. Only the fields you supply are changed; omitted fields are left as-is. Send `null` for `attendeeName` to clear it; `attendeeEmail` cannot be cleared. `metadata` takes per-entry operations matching the ticket validation endpoint: each entry adds or overwrites its key, `null` deletes it, and entries not named are left untouched (at most 20 entries per ticket). Requests with an empty body are rejected. When attendee details change and the booking has a ticket PDF, it is regenerated so the next download from the bookings ticket-pdf endpoint reflects them. The booking and customer records are not affected.
---
# Update a ticket type (/docs/api/v1/events/ticket-groups/ticket-types/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates an existing ticket type within the specified ticket group.
---
# Update a webhook (/docs/api/v1/webhooks/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the name, URL, subscribed events, custom headers or status of a webhook. Only supplied fields change; `events` and `customHeaders` are replaced wholesale. The URL must use https and must not be registered on another of your webhooks. Set `status` to `inactive` to pause deliveries and `active` to resume; `failed` is system-managed. The secret cannot be changed here. An empty body is accepted and leaves the webhook unchanged.
---
# Update checkout page (/docs/api/v1/checkout-pages/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates checkout page settings. Related resources are managed via dedicated endpoints.
---
# Update event (/docs/api/v1/events/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates event settings. Ticket groups and ticket types are managed via dedicated endpoints.
---
# Update field for checkout page (/docs/api/v1/checkout-pages/fields/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates an existing field on the specified checkout page.
---
# Update field for event (/docs/api/v1/events/fields/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates an existing field on the specified event.
---
# Update field for form (/docs/api/v1/forms/fields/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates an existing field on the specified form.
---
# Update form (/docs/api/v1/forms/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates form settings and attached files.
---
# Update product (/docs/api/v1/products/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates the specified product with the provided values. Only the fields provided will be updated.
---
# Update ticket group (/docs/api/v1/events/ticket-groups/update)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Updates an existing ticket group on the specified event page.
---
# Upload file (/docs/api/v1/files/upload)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Upload a file directly to Checkout Page. Accepts multipart/form-data. Files can be used throughout the API by attaching them in `fileId` or `imageId` properties.
---
# Validate ticket (/docs/api/v1/tickets/validate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Validates a ticket using its check-in code - the value read from the ticket QR code (the ticket's `checkInCode`). Optionally manages metadata entries on the ticket.
---
# Webhook signature (/docs/api/v1/webhooks/signature)
Webhook security is critical. All webhooks sent by Checkout Page include a signature in the `x-webhook-signature` header that you should verify to ensure the webhook is authentic and hasn't been tampered with.
## Why verify signatures? [#why-verify-signatures]
Verifying webhook signatures protects your application by:
* Ensuring the webhook came from Checkout Page
* Preventing request forgery and replay attacks
* Detecting if the webhook payload has been modified in transit
## How it works [#how-it-works]
Checkout Page generates a signature using HMAC-SHA256 with your webhook secret. The signature is sent in the `x-webhook-signature` header with every webhook request in the format `sha256=`. You can verify the signature by:
1. Taking the raw webhook payload as a string
2. Computing an HMAC-SHA256 hash using your webhook secret
3. Comparing the computed hash with the signature in the header using constant-time comparison
The signature format is always `sha256=<64-character-hex-string>`. You must parse out the hex digest portion before comparison.
**Important**: Always use constant-time comparison (not standard string equality) to prevent
timing attacks. This prevents attackers from using the time taken to compare signatures to guess
the correct signature character-by-character.
## Webhook headers [#webhook-headers]
Every webhook request includes the following headers:
| Header | Format | Description |
| ---------------------------- | -------------- | ------------------------------------------ |
| `x-webhook-signature` | `sha256=` | HMAC-SHA256 signature of the payload |
| `x-webhook-timestamp` | Unix timestamp | Time the webhook was sent |
| `x-webhook-delivery-attempt` | Number | Attempt number (1, 2, 3, etc.) for retries |
**Example `x-webhook-signature` header value:**
```
sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
## Implementation [#implementation]
### Node.js [#nodejs]
Here's the recommended function to verify webhook signatures:
```typescript
import crypto from 'crypto';
/**
* Verify webhook signature
*/
export function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string,
): boolean {
try {
// Parse the signature header format: "sha256="
const [algorithm, receivedDigest] = signatureHeader.split('=');
// Verify algorithm is sha256
if (algorithm !== 'sha256' || !receivedDigest) {
return false;
}
// Generate the expected hex digest
const expectedDigest = generateWebhookDigest(payload, secret);
// Compare the two hex digests using constant-time comparison
const expectedBuffer = Buffer.from(expectedDigest, 'utf8');
const receivedBuffer = Buffer.from(receivedDigest, 'utf8');
if (expectedBuffer.length !== receivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
} catch (error) {
return false;
}
}
/**
* Generate the HMAC-SHA256 hex digest of the payload (the part of the header after "sha256=")
*/
export function generateWebhookDigest(payload: string, secret: string): string {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
```
### Using with Express [#using-with-express]
Verify the request body exactly as it arrived, before parsing it. Use `express.raw()` on the webhook route so `req.body` holds the raw bytes, and register the route before any app-wide `express.json()`, which would parse the body first.
```typescript
import express from 'express';
import { verifyWebhookSignature } from './webhook-utils';
app.post('/webhooks/events', express.raw({ type: 'application/json' }), (req, res) => {
const signatureHeader = req.headers['x-webhook-signature'];
const payload = req.body.toString('utf8');
const isValid = verifyWebhookSignature(payload, signatureHeader, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
// Parse only after the signature checks out
const webhook = JSON.parse(payload);
const timestamp = req.headers['x-webhook-timestamp'];
const attempt = req.headers['x-webhook-delivery-attempt'];
console.log('Webhook received:', {
event: webhook.event,
eventId: webhook.eventId,
timestamp,
attempt,
});
res.status(200).json({ success: true });
});
```
## Best practices [#best-practices]
### Store your webhook secret securely [#store-your-webhook-secret-securely]
Never hardcode your webhook secret. Always use environment variables:
```bash
# .env file
WEBHOOK_SECRET=1234567890abcdef
```
### Log webhook events [#log-webhook-events]
For debugging, log incoming webhooks (but never log the signature):
```typescript
console.log('Webhook received', {
event: webhook.event,
eventId: webhook.eventId,
timestamp: webhook.timestamp,
sellerId: webhook.sellerId,
// Do NOT log: signature, secret, or sensitive data
});
```
### Handle errors gracefully [#handle-errors-gracefully]
```typescript
app.post('/webhooks/events', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const signatureHeader = req.headers['x-webhook-signature'];
// Verify signature exists
if (!signatureHeader) {
return res.status(400).json({ error: 'Missing webhook signature' });
}
const payload = req.body.toString('utf8');
const isValid = verifyWebhookSignature(payload, signatureHeader, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
// Process webhook...
res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
```
### Idempotency [#idempotency]
Webhook deliveries may be retried. Ensure your webhook handler is idempotent by:
* Storing the `eventId` of each webhook you process
* Checking if a webhook has already been processed before handling it
```typescript
const processedWebhooks = new Set();
app.post('/webhooks/events', express.raw({ type: 'application/json' }), (req, res) => {
// ... signature verification ...
const webhook = JSON.parse(req.body.toString('utf8'));
// Check if we've already processed this webhook
if (processedWebhooks.has(webhook.eventId)) {
return res.status(200).json({ success: true });
}
// Process the webhook
// ... your logic here ...
processedWebhooks.add(webhook.eventId);
res.status(200).json({ success: true });
});
```
## Common issues [#common-issues]
### "Invalid webhook signature" [#invalid-webhook-signature]
**Cause 1**: The payload was parsed before verifying
* Verify against the raw request body, exactly as received
* Don't parse the JSON and serialize it again: the result can differ from what was signed, for example in spacing
**Cause 2**: Secret is incorrect or missing
* Verify `WEBHOOK_SECRET` is set in your environment variables
* Check that it matches the secret shown in your dashboard
**Cause 3**: Missing or malformed signature header
* Ensure the `x-webhook-signature` header is present
* Check for typos in the header name
* Verify the signature format is `sha256=<64-char-hex-string>`
### Testing webhooks [#testing-webhooks]
In the [Checkout Page dashboard](https://checkoutpage.com/dashboard/settings/webhooks), open the webhook's actions menu and select **Send test event**. This sends a signed request with dummy data to your webhook, so you can check that your endpoint verifies it.
## Rotating your secret [#rotating-your-secret]
Rotation is immediate: the previous secret stops working and every delivery from then on is signed with the new one, including retries of earlier events.
1. In the dashboard, go to **Settings > Webhooks**, open the webhook's actions menu and select **View signing secret**.
2. Select **Rotate secret**, then **Rotate now** to confirm.
3. Update your endpoint with the new secret straight away. Deliveries it rejects while it still has the old secret are retried briefly and then not sent again.
4. Use **Send test event** from the same menu to confirm your endpoint accepts the new signature.
## Security considerations [#security-considerations]
* **Never skip signature verification** in production
* **Use HTTPS** for all webhook endpoints
* **[Rotate secrets](#rotating-your-secret)** periodically for enhanced security
* **Log failures** for monitoring and debugging
* **Set timeouts** to prevent hanging requests
* **Use idempotency keys** to handle retried webhooks
## Next steps [#next-steps]
Explore these webhook-related topics: