# Changelog (/changelog)
## Refunds, credit notes, and webhook reliability [#refunds-credit-notes-and-webhook-reliability]
**Order refunds API & credit notes** — Merchants can now issue refunds via the `POST /v0/orders/:id/refunds` API endpoint. For invoice-backed orders, a credit note is automatically generated and emailed to the buyer, keeping your accounting records accurate.
**Cancel pending payments** — A new `POST /v0/payments/:id/cancel` endpoint lets you programmatically cancel any pending payment, including automatic subscription plan-change rollback when applicable.
**Subscription confirmation emails now show renewal details** — Confirmation emails for new subscriptions include the next renewal date and the amount that will be charged, so customers always know what to expect.
**Webhook auto-disable with notifications** — Endpoints that repeatedly fail or are blocked (e.g. by SSRF protection) are now automatically disabled after a configurable threshold. You'll receive an email notification when a webhook is blocked, failing, recovering, or disabled, and the Dashboard shows the block reason on each attempt.
**Faster embedded checkout** — The embedded checkout element loads noticeably faster thanks to middleware pre-warming and font-loading optimisations.
## Webhook Management API, Subscription Filters & Upcoming Payment Preview [#webhook-management-api-subscription-filters--upcoming-payment-preview]
**Webhook Management via API**
You can now manage your webhook endpoints and inspect delivery history entirely through the v0 API. New endpoints let you create, update, and delete webhook configurations, and query per-event delivery attempts — making it straightforward to automate webhook lifecycle management and debug delivery failures programmatically. Full reference is available in the Webhooks section of the API docs.
**Subscription Date Filters & Upcoming Payment Preview**
`GET /subscriptions` now accepts `createdFrom` and `createdTo` query parameters for date-range filtering. A new `GET /subscriptions/{id}/upcoming-payment` endpoint returns a preview of the next charge amount, date, and tax breakdown — useful for surfacing renewal details in customer-facing UIs.
**Bug Fixes**
* Fixed an invoice tax breakdown that was showing the gross line amount instead of the net (pre-tax) amount.
* Fixed a duplicate-invoice issue that could occur when a `subscription_cycle` `invoice.paid` event was delivered more than once.
* Fixed a stuck "domain removal in progress" banner that persisted after domain teardown completed.
* Trial orders no longer incur a platform fee.
## End trials early, tax calculation API, and business buyer support at checkout [#end-trials-early-tax-calculation-api-and-business-buyer-support-at-checkout]
**End subscription trials via API**
Sellers and integrators can now end a subscription's trial period immediately using the new `POST /api/v0/subscriptions/{id}/end-trial` endpoint, without waiting for the trial to expire naturally.
**Tax calculation endpoint**
A new `POST /api/v0/tax/calculate` endpoint lets integrators fetch the applicable tax for a product before a purchase is initiated, making it easier to display accurate pricing to buyers.
**"I'm a business" at checkout**
Buyers can now identify themselves as a business entity directly on the minimal checkout form. This enables B2B purchase flows and ensures the correct tax treatment is applied at the time of purchase.
**Custom subscription management links in emails**
Sellers can now configure a custom subscription management URL in Store Settings. When set, this URL will be used in customer notification emails instead of the default Fungies-hosted portal link.
## `customFields` validation on Checkout Element [#customfields-validation-on-checkout-element]
Passing `customFields` when creating a Checkout Element via the API now returns a clear `400` error instead of silently ignoring the field. If your integration sends this parameter, please remove it before upgrading to avoid unexpected rejections. The API reference has been updated to reflect this behaviour.
## Custom-data webhook docs corrected [#custom-data-webhook-docs-corrected]
The developer documentation for `customFields` in webhook payloads has been corrected to accurately describe the payload shape and clarify key-matching requirements.
{/* New entries are prepended here automatically at release time — see
ci-scripts/cut-release.sh in fungies-apps. Entries below v3.64 are a manual
backfill from CHANGELOG.md, classified against the same public/internal policy. */}
## Multi-item subscriptions [#multi-item-subscriptions]
The subscription create/update/charge API now accepts multiple line items — invoices
and webhooks show the full itemized breakdown instead of one summed line. We also fixed
a dashboard crash when selecting an offer whose name contains a quote character.
## Account deactivation, and storefronts publish by default [#account-deactivation-and-storefronts-publish-by-default]
* Admins can now deactivate (reversible) or permanently erase a member account, with a
confirmation email and a login banner shown after deactivation.
* New workspaces now publish their storefront by default instead of starting unpublished.
* Fixed the "Stripe not connected" banner showing incorrectly, and workspace metadata not
updating right after connecting a Stripe account.
* Fixed a subscription item not reflecting an upgraded offer after a failed payment.
## Workspace bans and a new subscription webhook [#workspace-bans-and-a-new-subscription-webhook]
* Admins can now ban a workspace for abuse or ToS violations — the store goes offline
immediately and the owner loses dashboard access, with an email notification.
* Added a `subscription_updated` webhook that fires on subscription status transitions,
with guidance for reconciling out-of-order webhook delivery.
* Fixed a crash in the dashboard and store caused by third-party browser extensions
(e.g. translators) mutating the page.
* Fixed the embedded checkout element rendering an opaque background instead of a
transparent one.
## Workspace lifecycle emails and webhook reliability [#workspace-lifecycle-emails-and-webhook-reliability]
* Admins can now connect a new Stripe account to a workspace that hasn't taken a sale yet.
* Added progressive lifecycle emails (suspension, archived, purge-scheduled) that warn
sellers before each step.
* Fixed subscription creation failing when Stripe settled the payment server-side without
returning a client secret.
* Fixed outgoing webhook deliveries erroring when the target event or webhook no longer
exists — those deliveries are now dropped cleanly instead of failing.
## Workspace archiving and checkout fixes [#workspace-archiving-and-checkout-fixes]
* Admins can now archive any workspace to take it out of circulation regardless of
payment history, permanently protecting it from the automated inactivity sweep.
* Fixed `fngs-discount-code` and `fngs-quantity` URL parameters not being applied when
opening a checkout-element link.
* Fixed Apple Pay and Google Pay showing the pre-tax amount instead of the
tax-inclusive total.
## Paid workspaces are now permanently protected from cleanup [#paid-workspaces-are-now-permanently-protected-from-cleanup]
The inactive-workspace sweep now permanently protects any workspace that has ever taken
a paid order — it stays active as the system of record for its transaction data and is
never warned, suspended, archived, or purged.
## Safer inactive-workspace handling [#safer-inactive-workspace-handling]
Reworked the inactive-workspace sweep into a recoverable lifecycle (active → suspended →
archived → purge), fixing a bug where inactive workspaces could be permanently deleted at
suspension instead of being given a recovery window. Also fixed a checkout
billing-country mismatch between server and client render.
## API docs fix [#api-docs-fix]
Fixed the Swagger UI and downloadable OpenAPI spec serving an invalid file.
## Mobile checkout fix [#mobile-checkout-fix]
Fixed a high-volume checkout hydration error on mobile.
## New API filters and email fixes [#new-api-filters-and-email-fixes]
* Added `checkoutId` and custom-field filters to the orders and payments list endpoints.
* Fixed the store name and BCC address in transactional emails falling back incorrectly
when not explicitly configured.
* Fixed a crash when retrieving offer information for a missing offer.
## BCC copies of transactional emails [#bcc-copies-of-transactional-emails]
Merchants can now receive an exact copy of each transactional email type via a new
per-email "send a copy (BCC)" opt-in in Settings → Emails, with an optional address per
event. Also fixed iOS rubber-band overscroll on checkout and store pages.
# Discount (/core-resources/discount)
A Discount object represents a price reduction that can be applied to customer purchases. Discounts can be either coupon codes that customers enter at checkout, or automatic sales that apply based on configured rules.
Discounts support both fixed amounts and percentage-based reductions, with optional validity periods and usage limits. You can target discounts to specific [Offers](/core-resources/offer) or apply them store-wide. Applied discounts are visible on [Payment](/core-resources/payment) objects.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ------------------------------------ | ------------------------- |
| `GET` | `/v0/discounts/list` | List and filter discounts |
| `POST` | `/v0/discounts/create` | Create a new discount |
| `GET` | `/v0/discounts/{discountId}` | Retrieve a discount |
| `PATCH` | `/v0/discounts/{discountId}/update` | Update a discount |
| `PATCH` | `/v0/discounts/{discountId}/archive` | Archive a discount |
## The Discount object [#the-discount-object]
Object type identifier. Always `"discount"` for Discount objects.
Unique identifier (UUID) for this discount.
Discount type. Possible values: `code` (coupon code), `sale` (automatic discount)
Display name of the discount (e.g., "Summer Sale", "10% Off").
Discount amount. For `fixed`: amount in currency units (e.g., `10.00` for $10). For `percentage`: percentage value (e.g., `15` for 15% off).
How the amount is applied. Possible values: `fixed`, `percentage`
The coupon code customers enter. Only set when `type` is `"code"`.
Three-letter ISO 4217 currency code. Required for fixed-amount discounts.
Current discount status. Possible values: `active`, `inactive`
Unix timestamp (milliseconds) when the discount becomes valid. `null` means immediately valid.
Unix timestamp (milliseconds) when the discount expires. `null` means no expiration.
Maximum number of times this discount can be used. `null` means unlimited.
Number of times this discount has been used. Defaults to `0`.
Whether this discount applies to all [Offers](/core-resources/offer). Defaults to `false`.
Array of [Offer](/core-resources/offer) IDs that are excluded from this discount when `includesAllOffers` is `true`.
***
## Discount types [#discount-types]
### Coupon codes [#coupon-codes]
Coupon codes require customers to enter a code at checkout. They're ideal for:
* Marketing campaigns
* Influencer partnerships
* Customer retention offers
* Limited-time promotions
### Automatic sales [#automatic-sales]
Sales apply automatically without customer action. They're ideal for:
* Site-wide promotions
* Product launches
* Holiday sales
* Flash sales
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"discount": {
"object": "discount",
"id": "880e8400-e29b-41d4-a716-446655440003",
"type": "code",
"name": "Summer Sale 2024",
"amount": 15,
"amountType": "percentage",
"discountCode": "SUMMER15",
"currency": "USD",
"status": "active",
"validFrom": 1717200000000,
"validUntil": 1725148800000,
"purchaseLimit": 1000,
"timesUsed": 247,
"includesAllOffers": true,
"excludedOffers": []
}
}
}
```
***
## Related resources [#related-resources]
Offers that discounts apply to
Payments with discounts applied
Products discounted via offers
Orders with discounts
# Event (/core-resources/event)
Events are notifications that inform you when something significant happens in your Fungies account. When an event occurs - such as a successful [Payment](/core-resources/payment) or [Subscription](/core-resources/subscription) cancellation - we create an Event object and send it to your configured webhook endpoints.
You can use events to trigger workflows in your application, such as fulfilling [Orders](/core-resources/order), sending custom emails, or syncing data with your backend systems.
## Event delivery [#event-delivery]
Events are delivered to your webhook endpoints via HTTP POST requests. Each event includes:
* A unique `id` for deduplication
* An `idempotencyKey` to prevent duplicate processing
* The event `type` indicating what happened
* A `data` payload with relevant objects
Events may be delivered more than once. Use the `idempotencyKey` to ensure you process each event only once.
## Event types [#event-types]
These are the event types currently supported. We may add more at any time, so your code should handle unknown event types gracefully.
| Event Type | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payment_success` | A [Payment](/core-resources/payment) has been successfully processed. **Use this to fulfill orders and grant subscription access.** |
| `payment_refunded` | A [Payment](/core-resources/payment) has been refunded (full or partial) |
| `payment_failed` | A [Payment](/core-resources/payment) attempt has failed |
| `subscription_created` | A new [Subscription](/core-resources/subscription) record exists in Stripe/Fungies. May include `PENDING` payment data if sent before checkout finishes — not a paid signal by itself. |
| `subscription_interval` | A [Subscription](/core-resources/subscription)'s billing interval has been charged |
| `subscription_updated` | A [Subscription](/core-resources/subscription) has been modified |
| `subscription_cancelled` | A [Subscription](/core-resources/subscription) has been cancelled |
### Subscription signup events [#subscription-signup-events]
On the first subscription checkout, Fungies sends both events when your webhook is subscribed to both types. They are produced by different Stripe notifications and may arrive seconds apart or out of order.
* **`subscription_created`** — subscription object created; `data.lastPayment` and `data.order` may still be `PENDING`.
* **`payment_success`** — initial `subscription_initial` payment is `PAID`; safe to fulfill.
Treat `subscription_created` as informational (lifecycle, CRM, analytics). Treat `payment_success` as the fulfillment trigger for the first charge.
***
## The Event object [#the-event-object]
Unique identifier (UUID) for this event.
The type of event that occurred. See [Event types](#event-types) above.
Unique key (UUID) for idempotent event processing. Use this to prevent duplicate handling of the same event.
Whether this event occurred in test mode (`true`) or live mode (`false`).
The event payload containing related objects.
Array of line items involved in the transaction. See [Item object](#item-object) below.
The [Order object](/core-resources/order) associated with this event.
The [Payment object](/core-resources/payment) for the transaction.
The [User object](/core-resources/user) representing the customer.
***
## Item object [#item-object]
Each item in the `data.items` array represents a line item in the [Order](/core-resources/order).
Object type identifier. Always `"item"`.
Unique identifier (UUID) for this line item.
Display name of the item.
Item price in the smallest currency unit. Defaults to `0`.
Number of units purchased.
Three-letter ISO 4217 currency code.
The [Product object](/core-resources/product) this item belongs to.
The Variant object if applicable.
The [Offer object](/core-resources/offer) if this item was purchased through an offer.
The subscription Plan object (variants are used as plans for [Subscriptions](/core-resources/subscription)).
Your custom identifier for this item.
Key-value pairs of custom fields defined by you and filled by the customer during checkout. Only keys that exactly match a field's Key (configured in the [Dashboard](/developers/customer-data/setup)) are included — an unmatched or undefined key is silently omitted rather than sent as an empty value.
***
## Example event payload [#example-event-payload]
```json
{
"id": "evt_123e4567-e89b-12d3-a456-426614174000",
"type": "payment_success",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"testMode": false,
"data": {
"items": [
{
"object": "item",
"id": "itm_123e4567-e89b-12d3-a456-426614174001",
"name": "Pro Plan - Monthly",
"value": 2999,
"quantity": 1,
"currency": "USD",
"product": {
"id": "prod_abc123",
"name": "Pro Plan"
},
"variant": null,
"offer": null,
"plan": {
"id": "plan_monthly",
"name": "Monthly"
},
"internalId": "sku_pro_monthly",
"customFields": {
"license_type": "single"
}
}
],
"order": {
"object": "order",
"id": "550e8400-e29b-41d4-a716-446655440000",
"number": "#ABC123DEF456",
"status": "PAID",
"value": 2999,
"currency": "USD"
},
"payment": {
"object": "payment",
"id": "660e8400-e29b-41d4-a716-446655440001",
"number": "#PAY123DEF456",
"status": "PAID"
},
"customer": {
"object": "user",
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "customer@example.com",
"username": null,
"internalId": "usr_abc123"
}
}
}
```
***
## Related resources [#related-resources]
Configure webhook endpoints
Learn about webhooks
Order object reference
Payment object reference
# Offer (/core-resources/offer)
An Offer object represents a specific pricing configuration for a [Product](/core-resources/product) that customers can purchase. Offers define the price, currency, region availability, and subscription terms (if applicable).
A single [Product](/core-resources/product) can have multiple offers to support different regions, currencies, or pricing tiers. Offers can include product keys for digital goods that are automatically delivered upon purchase. [Discounts](/core-resources/discount) can be applied to offers to reduce the price.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| -------- | ----------------------------------- | ---------------------- |
| `GET` | `/v0/offers/list` | List and filter offers |
| `POST` | `/v0/offers/create` | Create a new offer |
| `GET` | `/v0/offers/{offerId}` | Retrieve an offer |
| `PATCH` | `/v0/offers/{offerId}/update` | Update an offer |
| `PATCH` | `/v0/offers/{offerId}/archive` | Archive an offer |
| `POST` | `/v0/offers/{offerId}/keys/add` | Add product keys |
| `POST` | `/v0/offers/{offerId}/keys/remove` | Remove unsold keys |
| `DELETE` | `/v0/offers/{offerId}/keys/{keyId}` | Remove a specific key |
## The Offer object [#the-offer-object]
Object type identifier. Always `"offer"` for Offer objects.
Unique identifier (UUID) for this offer.
Your custom identifier for this offer. Use to link to your own SKU system.
Display name of the offer (e.g., "Standard Edition", "Premium Bundle").
Description of what's included in this offer.
Price in the specified currency. For [Subscriptions](/core-resources/subscription), this is the recurring price.
Original price before any discount. Used to show strikethrough pricing.
Three-letter ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`).
Geographic region where this offer is available. Common values: `Global`, `Europe`, `United States`, `United Kingdom`, `North America`, `Latin America`, `Asia`, `EMEA`, `Rest of the World`
Platform for digital product activation. Common values: `Steam`, `Xbox Live`, `PSN`, `Nintendo`, `Epic Games`, `GOG.com`, `Origin`, `Battle.net`, `Ubisoft Connect`
Global Trade Item Number (barcode) for the product.
Warning or disclaimer message to display to customers.
Billing interval for [Subscriptions](/core-resources/subscription). Possible values: `day`, `week`, `month`, `year`
Number of intervals between billings. For example, `3` with `month` interval means billing every 3 months.
Trial period interval type. Possible values: `day`, `week`, `month`, `year`
Number of intervals for the trial period. For example, `14` with `day` interval means a 14-day trial.
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"offer": {
"object": "offer",
"id": "990e8400-e29b-41d4-a716-446655440004",
"internalId": "sku_standard_us",
"name": "Standard Edition",
"description": "Base game with all launch content",
"price": 59.99,
"originalPrice": 69.99,
"currency": "USD",
"region": "United States",
"platform": "Steam",
"gtin": "1234567890123",
"warningMessage": null,
"recurringInterval": null,
"recurringIntervalCount": null,
"trialInterval": null,
"trialIntervalCount": null
}
}
}
```
***
## Subscription offer example [#subscription-offer-example]
```json
{
"status": "success",
"data": {
"offer": {
"object": "offer",
"id": "aa0e8400-e29b-41d4-a716-446655440005",
"internalId": "plan_pro_monthly",
"name": "Pro Plan - Monthly",
"description": "Full access to all features",
"price": 29.99,
"originalPrice": null,
"currency": "USD",
"region": "Global",
"platform": null,
"gtin": null,
"warningMessage": null,
"recurringInterval": "month",
"recurringIntervalCount": 1,
"trialInterval": "day",
"trialIntervalCount": 14
}
}
}
```
***
## Related resources [#related-resources]
Products that offers belong to
Discounts that apply to offers
Subscription offers
Orders containing offers
# Order (/core-resources/order)
An Order object represents a purchase transaction made by a customer. Orders are created when a customer initiates checkout and contain information about what was purchased, the total value, tax details, and the current status of the transaction.
Orders can contain one or more items and are linked to [Payments](/core-resources/payment), [Users](/core-resources/user), and optionally [Subscriptions](/core-resources/subscription). Each order has a unique ID and a human-readable order number that can be used for customer-facing references.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ------------------------------ | --------------------------------- |
| `GET` | `/v0/orders/list` | List and filter orders |
| `GET` | `/v0/orders/{orderIdOrNumber}` | Retrieve an order by ID or number |
| `PATCH` | `/v0/orders/{orderId}/update` | Update order details |
| `PATCH` | `/v0/orders/{orderId}/cancel` | Cancel an order |
## The Order object [#the-order-object]
Object type identifier. Always `"order"` for Order objects.
Unique identifier (UUID) for this order.
Human-readable order number (e.g., `#ABC123DEF456`). Used for customer-facing references and support.
Current order status. Possible values: `PENDING`, `PAID`, `FAILED`, `UNPAID`, `CANCELLED`, `REFUNDED`, `PARTIALLY_REFUNDED`, `EXPIRED`
Total order value in the smallest currency unit (e.g., cents for USD). Defaults to `0`.
Tax amount in the smallest currency unit. Defaults to `0`.
Processing fee in the smallest currency unit. Defaults to `0`.
Three-letter ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`).
Number of decimal places for the currency. Used for formatting display values.
Two-letter ISO 3166-1 alpha-2 country code where the order originated.
Total number of items in the order. Defaults to `0`.
Unix timestamp in milliseconds when the order was created.
UUID of the [User](/core-resources/user) who placed the order.
Expanded [User object](/core-resources/user) with basic information about the customer.
Always `"user"`.
Unique identifier (UUID) for this user.
Username of the user.
UUID of the most recent [Payment](/core-resources/payment) associated with this order.
Human-readable number of the most recent payment.
Expanded [Payment object](/core-resources/payment) with details about the most recent payment.
Always `"payment"`.
Unique identifier (UUID) for this payment.
Payment type: `one_time`, `subscription_initial`, `subscription_update`, `subscription_interval`, `subscription_extra`, `claim_free`
Human-readable payment number.
Current payment status.
Identifier of the [Subscription](/core-resources/subscription) if this order is part of a recurring subscription.
Expanded [Subscription object](/core-resources/subscription) if this order is subscription-related.
Always `"subscription"`.
Subscription identifier.
Subscription status: `active`, `past_due`, `canceled`, `unpaid`, `incomplete`, `incomplete_expired`, `trialing`, `paused`
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"order": {
"object": "order",
"id": "550e8400-e29b-41d4-a716-446655440000",
"number": "#ABC123DEF456",
"status": "PAID",
"value": 2999,
"tax": 500,
"fee": 87,
"currency": "USD",
"currencyDecimals": 2,
"country": "US",
"totalItems": 1,
"createdAt": 1705590000000,
"userId": "123e4567-e89b-12d3-a456-426614174000",
"user": {
"object": "user",
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "johndoe"
},
"lastPaymentId": "660e8400-e29b-41d4-a716-446655440001",
"lastPaymentNumber": "#PAY123DEF456",
"lastPayment": {
"object": "payment",
"id": "660e8400-e29b-41d4-a716-446655440001",
"type": "one_time",
"number": "#PAY123DEF456",
"status": "PAID"
},
"subscriptionId": null,
"subscription": null
}
}
}
```
***
## Related resources [#related-resources]
Customer accounts linked to orders
Payment transactions for orders
Recurring order subscriptions
Webhook events for order changes
# Overview (/core-resources/overview)
This guide explains how the core API objects in Fungies work together to power your store. Understanding these relationships will help you build integrations and process webhook events effectively.
## Object Relationships [#object-relationships]
***
## Core Objects [#core-objects]
Customers who purchase from your store. Contains contact and billing information.
Items you sell - games, subscriptions, digital downloads, etc.
Pricing configurations for products with region/currency support.
Price reductions via coupon codes or automatic sales.
Purchase transactions containing items, totals, and status.
Financial transactions with charges and payment method details.
Recurring billing relationships for subscription products.
Webhook notifications about activity in your store.
***
## Common Flows [#common-flows]
### One-Time Purchase Flow [#one-time-purchase-flow]
### Subscription Flow [#subscription-flow]
### Refund Flow [#refund-flow]
***
## Subscription Lifecycle [#subscription-lifecycle]
***
## Object Hierarchy [#object-hierarchy]
### Catalog Structure [#catalog-structure]
A [Product](/core-resources/product) is the top-level catalog item. Products can have:
* **Variants** - Different versions (Standard, Deluxe, Ultimate editions)
* **Plans** - Subscription billing options (Monthly, Annual)
* **Offers** - Pricing per region/currency
### Transaction Hierarchy [#transaction-hierarchy]
Each customer action creates linked transaction objects:
***
## ID Formats [#id-formats]
| Object | Format | Example |
| ------------ | ------ | ------------------------------------------ |
| User | UUID | `123e4567-e89b-12d3-a456-426614174000` |
| Product | UUID | `bb0e8400-e29b-41d4-a716-446655440006` |
| Offer | UUID | `990e8400-e29b-41d4-a716-446655440004` |
| Discount | UUID | `880e8400-e29b-41d4-a716-446655440003` |
| Order | UUID | `550e8400-e29b-41d4-a716-446655440000` |
| Order Number | String | `#ABC123DEF456` |
| Payment | UUID | `660e8400-e29b-41d4-a716-446655440001` |
| Subscription | String | `#SUB123DEF456` (based on order number) |
| Event | UUID | `evt_123e4567-e89b-12d3-a456-426614174000` |
Order numbers and subscription IDs are human-readable strings prefixed with `#`. These can be used interchangeably with UUIDs in most API endpoints.
***
## Webhook Events by Object [#webhook-events-by-object]
| Object | Events |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| [Payment](/core-resources/payment) | `payment_success`, `payment_failed`, `payment_refunded` |
| [Subscription](/core-resources/subscription) | `subscription_created`, `subscription_updated`, `subscription_interval`, `subscription_cancelled` |
Each [Event](/core-resources/event) includes the related objects in its `data` payload:
* `data.order` - The [Order](/core-resources/order)
* `data.payment` - The [Payment](/core-resources/payment)
* `data.customer` - The [User](/core-resources/user)
* `data.items` - Array of purchased items with [Product](/core-resources/product) and [Offer](/core-resources/offer) references
***
## Next Steps [#next-steps]
Configure endpoints to receive events
Get your API keys and authenticate
# Payment (/core-resources/payment)
A Payment object represents a single financial transaction - either a one-time purchase or a recurring subscription charge. Payments track the monetary flow, including the amount, fees, tax, and processing status.
Each payment is associated with an [Order](/core-resources/order) and optionally with a [User](/core-resources/user) and [Subscription](/core-resources/subscription). Payments contain detailed information about charges, including payment method details and invoice data when applicable.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------ | -------------------------- | ------------------------ |
| `GET` | `/v0/payments/list` | List and filter payments |
| `GET` | `/v0/payments/{paymentId}` | Retrieve a payment by ID |
## The Payment object [#the-payment-object]
Object type identifier. Always `"payment"` for Payment objects.
Unique identifier (UUID) for this payment.
Human-readable payment number (e.g., `#ABC123DEF456`). Used for reference in invoices and support.
Type of payment. Possible values: `one_time`, `subscription_initial`, `subscription_update`, `subscription_interval`, `subscription_extra`, `claim_free`
Current payment status. Possible values: `PENDING`, `PAID`, `FAILED`, `UNPAID`, `CANCELLED`, `REFUNDED`, `PARTIALLY_REFUNDED`, `EXPIRED`
Payment amount in the smallest currency unit (e.g., cents for USD). Includes tax but excludes fees. Defaults to `0`.
Tax amount in the smallest currency unit. Included in the total value. Defaults to `0`.
Processing fee in the smallest currency unit. This is deducted from your payout. Defaults to `0`.
Three-letter ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`).
Number of decimal places for this currency (e.g., `2` for USD, `0` for JPY).
Unix timestamp (milliseconds) when the payment was created.
UUID of the [User](/core-resources/user) who made this payment.
Expanded [User object](/core-resources/user) with basic information about the payer.
Always `"user"`.
Unique identifier (UUID) for this user.
Username of the user.
UUID of the associated [Order](/core-resources/order). For subscription payments, this links to the initial order.
Human-readable order number of the associated order.
Expanded [Order object](/core-resources/order) with basic information.
Always `"order"`.
Unique identifier (UUID) for this order.
Human-readable order number.
Current order status.
[Subscription](/core-resources/subscription) identifier if this is a subscription-related payment.
Expanded [Subscription object](/core-resources/subscription) with status and ID.
Always `"subscription"`.
Subscription identifier.
Subscription status: `active`, `past_due`, `canceled`, `unpaid`, `incomplete`, `incomplete_expired`, `trialing`, `paused`
Applied [Discount object](/core-resources/discount) if a discount code or sale was used.
Always `"discount"`.
Unique identifier (UUID) for this discount.
Discount type: `code` or `sale`.
Display name of the discount.
Discount amount (fixed value or percentage).
Whether amount is `fixed` or `percentage`.
The coupon code if this is a code-type discount.
Invoice number for completed payments. Only available for `PAID`, `REFUNDED`, or `PARTIALLY_REFUNDED` statuses.
URL to download the invoice PDF. Only available for completed payments with generated invoices.
Array of charge attempts for this payment.
Always `"charge"`.
Unique identifier (UUID) for this charge.
Charge status: `succeeded`, `pending`, `failed`
Unix timestamp (milliseconds) when the charge was created.
IP address of the customer at time of charge.
Payment method details including `type`, `brand`, `last4`, and `card`.
Payment method type (e.g., card, paypal, bank\_transfer, klarna, affirm).
Card brand if payment method is a card (visa, mastercard, amex, discover, diners, jcb, unionpay).
Last 4 digits of the card or account number.
Card-specific payment details. Present when the payment method type is `card`.
Card brand (visa, mastercard, amex, discover, diners, jcb, unionpay).
Last 4 digits of the card number.
Two-letter ISO country code of the card issuer.
Card network (e.g., visa, mastercard).
Digital wallet details, if the card payment was made via a wallet (e.g., Apple Pay, Google Pay). Null when no wallet was used.
The type of digital wallet: `amex_express_checkout`, `apple_pay`, `google_pay`, `link`, `masterpass`, `samsung_pay`, or `visa_checkout`.
The last four digits of the device account number. May differ from the physical card's last4.
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"payment": {
"object": "payment",
"id": "660e8400-e29b-41d4-a716-446655440001",
"number": "#PAY123DEF456",
"type": "one_time",
"status": "PAID",
"value": 2999,
"tax": 500,
"fee": 87,
"currency": "USD",
"currencyDecimals": 2,
"createdAt": 1705590000000,
"userId": "123e4567-e89b-12d3-a456-426614174000",
"user": {
"object": "user",
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "johndoe"
},
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "#ABC123DEF456",
"order": {
"object": "order",
"id": "550e8400-e29b-41d4-a716-446655440000",
"number": "#ABC123DEF456",
"status": "PAID"
},
"subscriptionId": null,
"subscription": null,
"discount": null,
"invoiceNumber": "INV-2024-001234",
"invoiceUrl": "https://api.fungies.io/invoice/INV-2024-001234",
"charges": [
{
"object": "charge",
"id": "770e8400-e29b-41d4-a716-446655440002",
"status": "succeeded",
"createdAt": 1705590000000,
"ipAddress": "192.168.1.100",
"paymentMethod": {
"type": "card",
"brand": "visa",
"last4": "4242",
"card": {
"brand": "visa",
"last4": "4242",
"country": "US",
"network": "visa",
"wallet": {
"type": "apple_pay",
"dynamicLast4": "7279"
}
}
}
}
]
}
}
}
```
***
## Related resources [#related-resources]
Orders associated with payments
Customers who made payments
Recurring subscription payments
Discounts applied to payments
# Product (/core-resources/product)
A Product object represents something you sell in your store. Products contain metadata like name, description, and type, while pricing is managed through linked [Offers](/core-resources/offer). This separation allows a single product to have multiple pricing options for different regions or tiers.
Products support various types including digital downloads, games, gift cards, software keys, subscriptions, and one-time payments. Each product can have variants (different versions) and, for subscriptions, billing plans.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ------------------------------------------------------- | ------------------------ |
| `GET` | `/v0/products/list` | List and filter products |
| `POST` | `/v0/products/create` | Create a new product |
| `GET` | `/v0/products/{productId}` | Retrieve a product |
| `PATCH` | `/v0/products/{productId}/update` | Update a product |
| `PATCH` | `/v0/products/{productId}/archive` | Archive a product |
| `POST` | `/v0/products/{productId}/duplicate` | Duplicate a product |
| `POST` | `/v0/products/{productId}/variants/add` | Add a variant |
| `PATCH` | `/v0/products/{productId}/variants/{variantId}/update` | Update a variant |
| `PATCH` | `/v0/products/{productId}/variants/{variantId}/archive` | Archive a variant |
| `POST` | `/v0/products/{productId}/plans/add` | Add a subscription plan |
| `PATCH` | `/v0/products/{productId}/plans/{planId}/update` | Update a plan |
| `PATCH` | `/v0/products/{productId}/plans/{planId}/archive` | Archive a plan |
## The Product object [#the-product-object]
Object type identifier. Always `"product"` for Product objects.
Unique identifier (UUID) for this product.
Product type. Possible values: `DigitalDownload`, `Game`, `GiftCard`, `SoftwareKey`, `VirtualCurrency`, `VirtualItem`, `Subscription`, `OneTimePayment`
Display name of the product.
Product description. May contain HTML or markdown.
Your custom identifier for this product. Use to link to your own inventory system.
Product status. Possible values: `DRAFT`, `ACTIVE`, `ARCHIVED`
Game developer name (primarily for `Game` type products).
Game publisher name (primarily for `Game` type products).
Unix timestamp (milliseconds) of the game's release date.
PEGI age rating. Possible values: `3`, `7`, `12`, `16`, `18`, `!` (pending)
Supported platforms/systems. Possible values: `Windows`, `MacOs`, `Linux`, `PlayStation 4`, `PlayStation 5`, `Xbox One`, `Xbox Series X|S`, `iOS`, `Android`, `Nintendo Switch`, `Nintendo 3DS`
Game genres. Common values: `Action`, `Adventure`, `RPG`, `FPS`, `Strategy`, `Simulation`, `Sports`, `Racing`, `Puzzle`, `Horror`, `Indie`, `MMO`, `Open World`, `Multiplayer`
***
## Product hierarchy [#product-hierarchy]
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"product": {
"object": "product",
"id": "bb0e8400-e29b-41d4-a716-446655440006",
"type": "Game",
"name": "Epic Adventure Quest",
"description": "An immersive open-world RPG experience...",
"internalId": "game_epic_adventure",
"status": "ACTIVE",
"developer": "Awesome Games Studio",
"publisher": "Big Publisher Inc",
"releaseDate": 1735689600000,
"pegiRating": "16",
"systems": ["Windows", "PlayStation 5", "Xbox Series X|S"],
"genres": ["Action RPG", "Open World", "Adventure"]
}
}
}
```
***
## Subscription product example [#subscription-product-example]
```json
{
"status": "success",
"data": {
"product": {
"object": "product",
"id": "cc0e8400-e29b-41d4-a716-446655440007",
"type": "Subscription",
"name": "Pro Membership",
"description": "Unlock all premium features with our Pro plan",
"internalId": "membership_pro",
"status": "ACTIVE",
"developer": null,
"publisher": null,
"releaseDate": null,
"pegiRating": null,
"systems": null,
"genres": null
}
}
}
```
***
## Related resources [#related-resources]
Pricing options for products
Discounts applicable to products
Orders containing products
Subscription products
# Subscription (/core-resources/subscription)
A Subscription object represents an ongoing billing relationship where a customer is charged on a recurring basis. Subscriptions are created when a customer purchases a subscription [Product](/core-resources/product) and manage the lifecycle of recurring [Payments](/core-resources/payment).
Subscriptions track billing intervals, status changes, and link to all associated [Payments](/core-resources/payment) and [Orders](/core-resources/order). You can use the API to manage subscriptions including updating, pausing, and cancelling them.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ------------------------------------------------------------- | ---------------------------------------------- |
| `GET` | `/v0/subscriptions/list` | List and filter subscriptions |
| `POST` | `/v0/subscriptions/create` | Create a new subscription |
| `GET` | `/v0/subscriptions/{subscriptionIdOrNumber}` | Retrieve a subscription |
| `PATCH` | `/v0/subscriptions/{subscriptionIdOrNumber}/update` | Update a subscription |
| `POST` | `/v0/subscriptions/{subscriptionIdOrNumber}/charge` | Charge a subscription immediately |
| `PATCH` | `/v0/subscriptions/{subscriptionIdOrNumber}/cancel` | Cancel a subscription |
| `PATCH` | `/v0/subscriptions/{subscriptionIdOrNumber}/pause-collection` | Pause subscription billing |
| `PATCH` | `/v0/subscriptions/{subscriptionIdOrNumber}/endTrial` | End a trial immediately and start paid billing |
## Ending a trial early [#ending-a-trial-early]
A subscription in `trialing` status can be converted to a paid subscription right away with the
`endTrial` endpoint. The first invoice is created and charged immediately using the payment method
on file, and the outcome (emails, webhooks, orders) is identical to the trial ending naturally.
The request fails with a `400` if the subscription is not trialing or has no payment method saved.
## Multiple line items [#multiple-line-items]
The `create`, `update`, and `charge` endpoints accept an `items` array with **up to 20 line items**,
so a single subscription can bill several offers together (e.g. a package plus add-ons) while the
invoice and webhook payloads keep the full per-line breakdown.
Because one subscription maps to a single Stripe subscription, all items must be **on the same
cadence**:
* **Same currency** — every item resolves to the same currency (either explicitly or from its offer).
* **Same billing interval** — every offer shares the same `interval` and `interval_count`. Mixing,
for example, a monthly and a yearly offer is rejected with a `400` rather than failing at Stripe.
* **No trial** — a trial is only applied to single-item subscriptions; multi-item subscriptions start
billing immediately.
* **No discount codes** — discount codes are not supported when a subscription has more than one item.
Requests that violate these rules return a `400` with a descriptive message.
## The Subscription object [#the-subscription-object]
Object type identifier. Always `"subscription"` for Subscription objects.
Subscription identifier, based on the initial order number.
Current subscription status. Possible values: `active`, `past_due`, `canceled`, `unpaid`, `incomplete`, `incomplete_expired`, `trialing`, `paused`
Unix timestamp (milliseconds) when the subscription was created.
Unix timestamp (milliseconds) when the current billing interval started.
Unix timestamp (milliseconds) when the current billing interval ends.
Whether the subscription will cancel at the end of the current interval. Defaults to `false`.
Unix timestamp (milliseconds) when the subscription was cancelled, if applicable.
UUID of the subscribed [User](/core-resources/user).
Expanded [User object](/core-resources/user) with basic subscriber information.
Always `"user"`.
Unique identifier (UUID) for this user.
Username of the user.
UUID of the initial [Order](/core-resources/order) that created this subscription.
Human-readable number of the initial order.
Expanded [Order object](/core-resources/order) for the initial subscription order.
Always `"order"`.
Unique identifier (UUID) for this order.
Human-readable order number.
Current order status.
UUID of the most recent [Payment](/core-resources/payment) for this subscription.
Human-readable number of the most recent payment.
Expanded [Payment object](/core-resources/payment) for the most recent charge.
Always `"payment"`.
Unique identifier (UUID) for this payment.
Payment type: `subscription_initial`, `subscription_interval`, `subscription_update`, `subscription_extra`
Human-readable payment number.
Current payment status.
***
## Subscription lifecycle [#subscription-lifecycle]
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"subscription": {
"object": "subscription",
"id": "#SUB123DEF456",
"status": "active",
"createdAt": 1705590000000,
"currentIntervalStart": 1708268400000,
"currentIntervalEnd": 1710946800000,
"cancelAtIntervalEnd": false,
"canceledAt": null,
"userId": "123e4567-e89b-12d3-a456-426614174000",
"user": {
"object": "user",
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "johndoe"
},
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"orderNumber": "#ABC123DEF456",
"order": {
"object": "order",
"id": "550e8400-e29b-41d4-a716-446655440000",
"number": "#ABC123DEF456",
"status": "PAID"
},
"lastPaymentId": "660e8400-e29b-41d4-a716-446655440001",
"lastPaymentNumber": "#PAY789GHI012",
"lastPayment": {
"object": "payment",
"id": "660e8400-e29b-41d4-a716-446655440001",
"type": "subscription_interval",
"number": "#PAY789GHI012",
"status": "PAID"
}
}
}
}
```
***
## Webhook events [#webhook-events]
| Event | Typical use |
| ------------------------ | ---------------------------------------------------------- |
| `subscription_created` | Subscription exists; first payment may still be processing |
| `payment_success` | First or recurring charge succeeded — **fulfill here** |
| `subscription_interval` | New billing period charged (renewal) |
| `subscription_updated` | Plan change, cancel-at-period-end, trial ended, etc. |
| `subscription_cancelled` | Subscription ended |
See [Event types](/core-resources/event#subscription-signup-events) and [Webhooks overview](/developers/webhooks/overview#subscription-checkouts-and-fulfillment) for why `subscription_created` can include `PENDING` payments and why you should not rely on it alone for access grants.
***
## Related resources [#related-resources]
Payment transactions for subscriptions
Initial orders creating subscriptions
Subscribed customers
Subscription webhook events
# User (/core-resources/user)
A User object represents a customer who has made a purchase or registered in your store. Users store identity information including contact details, billing address, and custom identifiers. Each user is automatically created when a customer completes a checkout, or can be created manually via the API.
Users are central to the Fungies platform - they link to [Orders](/core-resources/order), [Payments](/core-resources/payment), [Subscriptions](/core-resources/subscription), and inventory items. You can use the API to create, retrieve, update, and manage user accounts.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ------------------------------ | -------------------------- |
| `GET` | `/v0/users/list` | List and filter users |
| `POST` | `/v0/users/create` | Create a new user |
| `GET` | `/v0/users/{userId}` | Retrieve a user |
| `PATCH` | `/v0/users/{userId}/update` | Update user details |
| `PATCH` | `/v0/users/{userId}/archive` | Archive a user |
| `PATCH` | `/v0/users/{userId}/unarchive` | Restore an archived user |
| `GET` | `/v0/users/{userId}/inventory` | Get user's inventory items |
## The User object [#the-user-object]
Object type identifier. Always `"user"` for User objects.
Unique identifier (UUID) for this user.
The user's email address. Used for transactional emails and account identification.
Username of the user, if set. Can be used as an alternative identifier.
Billing and contact details for the user.
The type of customer account. Possible values: `"individual"`, `"company"`
Customer's first name.
Customer's last name.
Customer's phone number (without country dial code).
International dial code for the phone number (e.g., `"+1"`, `"+44"`).
Primary street address line.
Secondary address line (apartment, suite, unit, etc.).
City or locality.
State, province, or region.
Postal or ZIP code.
Two-letter ISO 3166-1 alpha-2 country code (e.g., `"US"`, `"GB"`, `"DE"`).
Company or organization name. Typically set when `type` is `"company"`.
Tax identification type or label.
Tax identification number (VAT number, EIN, etc.).
Your custom identifier for this user. Use this to link Fungies users to your own system's user records.
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"user": {
"object": "user",
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "customer@example.com",
"username": "johndoe",
"details": {
"type": "individual",
"firstName": "John",
"lastName": "Doe",
"dialCode": "+1",
"phoneNumber": "5551234567",
"address": "123 Main Street",
"addressLine2": "Apt 4B",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"countryCode": "US",
"companyName": null,
"taxId": null,
"taxNumber": null
},
"internalId": "usr_abc123"
}
}
}
```
***
## Related resources [#related-resources]
View orders placed by users
Payment transactions by users
User subscription relationships
Webhook events for user activity
# Webhook (/core-resources/webhook)
A Webhook object represents an endpoint you've configured to receive [Event](/core-resources/event) notifications — payment successes, refunds, subscription changes, and more. Each webhook is subscribed to one or more event types and receives an HTTP POST for every matching event, signed with its `secret`.
A workspace may have a small, fixed number of webhooks configured at once; `create` returns an error once that limit is reached.
Webhook URLs must use `http://` or `https://` and resolve to a public address. Other schemes, and URLs that resolve to a loopback, private, or link-local address (including cloud metadata endpoints), are rejected — both at creation/update time and again at delivery time.
## Endpoints [#endpoints]
| Method | Endpoint | Description |
| ------- | ----------------------------------- | -------------------------------------------------- |
| `GET` | `/v0/webhooks/list` | List all configured webhooks |
| `POST` | `/v0/webhooks/create` | Create a new webhook |
| `POST` | `/v0/webhooks/sendTestEvent` | Send a synthetic test event to subscribed webhooks |
| `GET` | `/v0/webhooks/{webhookId}` | Retrieve a webhook |
| `PATCH` | `/v0/webhooks/{webhookId}/update` | Update a webhook |
| `PATCH` | `/v0/webhooks/{webhookId}/archive` | Archive a webhook |
| `GET` | `/v0/webhooks/{webhookId}/attempts` | List a webhook's delivery attempts |
## The Webhook object [#the-webhook-object]
Object type identifier. Always `"webhook"` for Webhook objects.
Unique identifier (UUID) for this webhook.
The endpoint that receives event deliveries.
Current webhook status. Possible values: `active`, `inactive`
Signing secret used to verify the `x-fngs-signature` header on each delivery.
[Event](/core-resources/event) types this webhook is subscribed to.
Unix timestamp (milliseconds) when the webhook was created.
Unix timestamp (milliseconds) when the webhook was archived. `null` if not archived.
***
## The Webhook Attempt object [#the-webhook-attempt-object]
Each delivery attempt — success or failure — is recorded and available via the `attempts` endpoint.
Object type identifier. Always `"webhook_attempt"` for Webhook Attempt objects.
Unique identifier (UUID) for this attempt.
The [Webhook](#the-webhook-object) this attempt belongs to.
The [Event](/core-resources/event) type that was delivered.
Retry sequence number for this delivery. `0` is the first try.
HTTP status code returned by the receiving endpoint. `null` if the request itself failed (timeout, connection error, blocked URL).
The request body that was sent.
The parsed JSON response body, if any.
Unix timestamp (milliseconds) when the attempt was made.
***
## Example response [#example-response]
```json
{
"status": "success",
"data": {
"webhook": {
"object": "webhook",
"id": "660e8400-e29b-41d4-a716-446655440001",
"url": "https://example.com/webhooks/fungies",
"status": "active",
"secret": "your-webhook-signing-secret",
"events": ["payment_success", "subscription_cancelled"],
"createdAt": 1717200000000,
"deletedAt": null
}
}
}
```
***
## Related resources [#related-resources]
The notification payloads webhooks deliver
Configuring and verifying webhook deliveries
# Workspace lifecycle (/guides/workspace-lifecycle)
Fungies never deletes a workspace the moment it goes quiet. Instead, an inactive
workspace moves through a series of **recoverable** stages — with plenty of
warning by email — and your data stays safe until the very last step. This page
explains exactly how that works and what keeps a workspace protected forever.
**The short version:** sign in every couple of months and you never have to
think about this. Any workspace that has ever taken a payment is kept
**active forever** and is never paused or deleted.
## What keeps a workspace active [#what-keeps-a-workspace-active]
A workspace stays active as long as any one of the following is true:
Signing in at least once every couple of months keeps a free workspace
active.
Any workspace that has ever taken a paid order is kept active **forever**.
Workspaces with a live subscription are always kept.
If your connected Stripe account can accept payments, the workspace is
always kept.
If none of these apply — typically an empty or abandoned workspace on the free
plan — it becomes eligible for the inactivity lifecycle below.
## The inactivity lifecycle [#the-inactivity-lifecycle]
If a free workspace stays inactive, it moves through these stages. **Every stage
before deletion is fully recoverable — just sign back in.**
We email the workspace owner about a week before anything happens, so you
have time to sign in and keep the workspace active.
The workspace and its store are taken **offline**, but nothing is deleted —
your data, settings, and store are fully intact. Sign in at any time to
bring it straight back online.
If the workspace stays paused for about a month, it's **archived**. It's
still recoverable and your data is retained — you'll get another email with
the date it's scheduled for removal.
About three months after archiving, if the workspace is still untouched, it
and its data are **permanently deleted**. We send a final reminder before
this happens.
Deletion is the only irreversible step. Up to that point you can restore
everything simply by signing in before the date in your reminder emails.
## How to bring a workspace back [#how-to-bring-a-workspace-back]
Go to [app.fungies.io](https://app.fungies.io) and sign in with the owner
account.
Selecting the paused or archived workspace reactivates it and brings your
store back online automatically.
If a workspace has already been permanently deleted, it can't be recovered —
please [contact support](mailto:support@fungies.io) and we'll help you get set
up again.
## Workspaces that are never swept [#workspaces-that-are-never-swept]
Some workspaces are exempt from the inactivity lifecycle entirely and are always
kept active:
| Kept forever when… | Why |
| ------------------------------------------------ | ----------------------------------------------------------------- |
| The workspace has ever taken a paid order | Fungies keeps your transaction records as your Merchant of Record |
| It has an active subscription | You're an active, paying customer |
| Its connected Stripe account can accept payments | The store is live and ready to sell |
Even after a workspace is deleted, Fungies retains the **invoices and
financial records** required by law and by our role as Merchant of Record.
Deletion removes your store and its content — not the tax/accounting trail for
past sales.
## Questions? [#questions]
If you're unsure about the status of a workspace or need help restoring one,
reach out at [support@fungies.io](mailto:support@fungies.io) — we're happy to
help.
# Authentication (/api-reference/authentication)
The Fungies API uses API keys to authenticate requests. Every request must include valid authentication headers.
## Getting Your API Keys [#getting-your-api-keys]
You can create and manage your API keys in the [Fungies Dashboard](https://app.fungies.io/devs/api-keys).
You'll receive two types of keys:
| Key Type | Prefix | Purpose |
| ---------- | ------ | --------------------------------------------------- |
| Public Key | `pub_` | Required for all API requests |
| Secret Key | `sec_` | Required for write operations (POST, PATCH, DELETE) |
## Authentication Headers [#authentication-headers]
Include your API keys in the request headers:
```bash
# Required for all requests
x-fngs-public-key: pub_your_public_key_here
# Required for write operations
x-fngs-secret-key: sec_your_secret_key_here
```
## Example Request [#example-request]
Here's an example of a properly authenticated request:
```bash
curl -X GET "https://api.fungies.io/v0/products" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key"
```
```javascript
const response = await fetch('https://api.fungies.io/v0/products', {
method: 'GET',
headers: {
'x-fngs-public-key': 'pub_your_public_key',
'x-fngs-secret-key': 'sec_your_secret_key'
}
});
```
```python
import requests
response = requests.get(
'https://api.fungies.io/v0/products',
headers={
'x-fngs-public-key': 'pub_your_public_key',
'x-fngs-secret-key': 'sec_your_secret_key'
}
)
```
## Security Best Practices [#security-best-practices]
Your API keys grant access to your Fungies account. Keep them secure and never expose them publicly.
Follow these guidelines to protect your keys:
* **Never commit keys to version control** - Use environment variables instead
* **Don't expose keys in client-side code** - Secret keys should only be used server-side
* **Rotate keys regularly** - Generate new keys periodically and revoke old ones
* **Use separate keys for different environments** - Keep production and development keys separate
## HTTPS Required [#https-required]
All API requests must be made over HTTPS. Requests made over plain HTTP will be rejected.
## Error Responses [#error-responses]
If authentication fails, you'll receive one of these responses:
| Status Code | Meaning |
| ------------------ | -------------------------------------- |
| `401 Unauthorized` | Missing or invalid API key |
| `403 Forbidden` | Valid key but insufficient permissions |
# Introduction (/api-reference/introduction)
Welcome to the Fungies API. This API lets you programmatically manage your digital commerce operations, from creating products to processing payments and managing customer subscriptions.
## Overview [#overview]
The Fungies API is built on REST principles:
* **Predictable URLs** - Resource-oriented endpoints that are easy to understand
* **JSON responses** - All responses are returned in JSON format
* **Standard HTTP** - Uses conventional HTTP methods (GET, POST, PATCH, DELETE)
* **Secure** - All requests must be made over HTTPS
## Base URL [#base-url]
All API requests should be made to:
```
https://api.fungies.io/v0
```
## Request Format [#request-format]
The API accepts JSON-encoded request bodies for POST and PATCH requests. Make sure to set the appropriate content type header:
```bash
Content-Type: application/json
```
## Rate Limits [#rate-limits]
The API implements rate limiting to ensure fair usage. If you exceed the limit, you'll receive a `429 Too Many Requests` response. Wait a moment before retrying.
## Working with the API [#working-with-the-api]
The Fungies API processes one object per request. Bulk operations are not currently supported, so you'll need to make separate API calls for each object you want to create or update.
## Need Help? [#need-help]
If you have questions or run into issues, reach out to our support team or check out the rest of this documentation for detailed endpoint references.
# Billing Data Prefill (/developers/checkout-elements/billing-data)
Prefilling billing data reduces friction in the checkout process by automatically populating form fields with customer information you already have.
## Available Fields [#available-fields]
| Field | SDK Property | HTML Attribute | Query Parameter |
| ---------- | ----------------------- | --------------------------------- | -------------------------- |
| Email | `billingData.email` | `data-fungies-billing-email` | `fngs-customer-email` |
| First Name | `billingData.firstName` | `data-fungies-billing-first-name` | `fngs-customer-first-name` |
| Last Name | `billingData.lastName` | `data-fungies-billing-last-name` | `fngs-customer-last-name` |
| Country | `billingData.country` | `data-fungies-billing-country` | `fngs-customer-country` |
| State | `billingData.state` | `data-fungies-billing-state` | `fngs-customer-state` |
| City | `billingData.city` | `data-fungies-billing-city` | `fngs-customer-city` |
| ZIP Code | `billingData.zipCode` | `data-fungies-billing-zip-code` | `fngs-customer-zip-code` |
The legacy `customerEmail` property and `data-fungies-customer-email` attribute are still supported but deprecated. Use `billingData.email` instead.
## Usage Examples [#usage-examples]
### JavaScript SDK [#javascript-sdk]
```javascript
Fungies.Checkout.open({
checkoutUrl: "https://yourstore.fungies.io/checkout-element/abc123",
settings: { mode: "overlay" },
billingData: {
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
country: "US",
state: "CA",
city: "Los Angeles",
zipCode: "90001"
}
});
```
### HTML Data Attributes [#html-data-attributes]
```html
```
### Query Parameters (Hosted Checkout) [#query-parameters-hosted-checkout]
```
https://yourstore.fungies.io/checkout/offer-id
?fngs-customer-email=john.doe@example.com
&fngs-customer-first-name=John
&fngs-customer-last-name=Doe
&fngs-customer-country=US
&fngs-customer-state=CA
&fngs-customer-city=Los%20Angeles
&fngs-customer-zip-code=90001
```
Remember to URL-encode values that contain special characters (spaces, `@`, etc.).
## Country Codes [#country-codes]
The `country` field accepts ISO 3166-1 alpha-2 country codes:
| Code | Country |
| ---- | ------------------------------------------------------------- |
| `US` | United States |
| `GB` | United Kingdom |
| `CA` | Canada |
| `AU` | Australia |
| `DE` | Germany |
| `FR` | France |
| `PL` | Poland |
| `JP` | Japan |
| ... | [Full list](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) |
## State/Province Codes [#stateprovince-codes]
For countries that require state/province selection (US, Canada, Australia, etc.), use the appropriate subdivision codes:
**United States:**
* `CA` - California
* `NY` - New York
* `TX` - Texas
* etc.
**Canada:**
* `ON` - Ontario
* `BC` - British Columbia
* `QC` - Quebec
* etc.
## Field Visibility [#field-visibility]
Billing fields are only visible in checkout when:
* Billing information is enabled in your checkout settings, **or**
* Your Stripe account is based in India (required by regulations)
If billing fields are hidden, the prefilled data will still be stored with the order.
## Priority Order [#priority-order]
When multiple sources provide billing data, this priority is used:
1. **Query parameters / SDK billingData** - Highest priority
2. **Persisted customer data** - From previous sessions (local storage)
3. **IP-based detection** - Automatic country detection
4. **Default values** - Fallback (e.g., United States)
This ensures that explicitly passed billing data always takes precedence over previously saved data.
## Additional Query Parameters [#additional-query-parameters]
Beyond billing data, hosted checkout supports these query parameters:
| Parameter | Description |
| -------------------- | ---------------------- |
| `fngs-quantity` | Product quantity |
| `fngs-discount-code` | Discount code to apply |
### Example with Discount [#example-with-discount]
```
https://yourstore.fungies.io/checkout/offer-id
?fngs-customer-email=john@example.com
&fngs-customer-country=US
&fngs-discount-code=SAVE20
&fngs-quantity=2
```
# HTML Data Attributes (/developers/checkout-elements/html-attributes)
HTML data attributes provide a no-code way to add Fungies checkout buttons to any website. Simply add attributes to your HTML elements and include the SDK script.
## Quick Start [#quick-start]
```html
```
## Script Attributes [#script-attributes]
Add these attributes to the `
```
## Button Attributes [#button-attributes]
Add these attributes to any clickable element (button, anchor, div, etc.):
### Required [#required]
| Attribute | Description |
| --------------------------- | -------------------------------------------- |
| `data-fungies-checkout-url` | The checkout URL from your Fungies dashboard |
### Display Options [#display-options]
| Attribute | Values | Description |
| --------------------------- | ------------------ | ------------------------------------------------ |
| `data-fungies-mode` | `overlay`, `embed` | How to display the checkout (default: `overlay`) |
| `data-fungies-frame-target` | Element ID | Target element for embed mode |
### Billing Data [#billing-data]
| Attribute | Description |
| --------------------------------- | ------------------------------------- |
| `data-fungies-billing-email` | Customer email |
| `data-fungies-billing-first-name` | Customer first name |
| `data-fungies-billing-last-name` | Customer last name |
| `data-fungies-billing-country` | Country code (e.g., `US`, `GB`, `PL`) |
| `data-fungies-billing-state` | State/province |
| `data-fungies-billing-city` | City |
| `data-fungies-billing-zip-code` | Postal/ZIP code |
### Other Options [#other-options]
| Attribute | Description |
| ---------------------------- | --------------------------------------- |
| `data-fungies-discount-code` | Discount code to apply |
| `data-fungies-quantity` | Product quantity |
| `data-fungies-items` | JSON array of items (for multi-product) |
| `data-fungies-custom-fields` | JSON object of custom fields |
### Legacy (Deprecated) [#legacy-deprecated]
| Attribute | Description |
| ----------------------------- | ---------------------------------------------------------- |
| `data-fungies-customer-email` | Use `data-fungies-billing-email` instead |
| `data-fungies-button` | Legacy URL format, use `data-fungies-checkout-url` instead |
## Complete Examples [#complete-examples]
### Basic Overlay Button [#basic-overlay-button]
```html
```
### With Prefilled Billing Data [#with-prefilled-billing-data]
```html
```
### With Discount Code [#with-discount-code]
```html
```
### With Quantity [#with-quantity]
```html
```
### Multi-Product Checkout [#multi-product-checkout]
```html
```
### With Custom Fields [#with-custom-fields]
```html
```
### Embedded Checkout [#embedded-checkout]
```html
```
## Styling [#styling]
The SDK doesn't add any styles to your buttons. Style them however you want:
```html
```
## Dynamic Elements [#dynamic-elements]
If you add checkout buttons dynamically (e.g., via JavaScript or a framework), call `Fungies.ScanDOM()` to attach click handlers:
```javascript
// After adding new elements to the DOM
Fungies.ScanDOM();
```
Or import and call directly:
```javascript
import { Fungies } from "@fungies/fungies-js";
// After dynamically adding elements
Fungies.ScanDOM();
```
# Checkout Elements Overview (/developers/checkout-elements/overview)
Checkout Elements allow you to integrate Fungies checkout into your website in two ways:
1. **Embedded Checkout** - Display the checkout directly within your page using an iframe
2. **Hosted Checkout** - Redirect customers to a Fungies-hosted checkout page
Both methods support prefilling customer billing data to streamline the checkout experience.
## Integration Methods [#integration-methods]
### JavaScript SDK (Embedded) [#javascript-sdk-embedded]
Use the Fungies JavaScript SDK to embed checkout directly in your website:
```javascript
import { Fungies } from "@fungies/fungies-js";
Fungies.Initialize();
Fungies.Checkout.open({
checkoutUrl: "https://yourstore.fungies.io/checkout-element/your-checkout-id",
settings: {
mode: "embed", // or "overlay"
frameTarget: "checkout-container" // DOM element ID for embed mode
},
billingData: {
email: "customer@example.com",
firstName: "John",
lastName: "Doe",
country: "US",
state: "CA",
city: "Los Angeles",
zipCode: "90001"
}
});
```
### HTML Data Attributes (Embedded) [#html-data-attributes-embedded]
For simpler integrations, use HTML data attributes:
```html
```
### Query Parameters (Hosted Checkout) [#query-parameters-hosted-checkout]
For hosted checkout pages, pass customer data via URL query parameters:
```
https://yourstore.fungies.io/checkout/offer-id?fngs-customer-email=customer@example.com&fngs-customer-first-name=John&fngs-customer-country=US
```
## When to Use Each Method [#when-to-use-each-method]
| Method | Best For |
| -------------------- | ------------------------------------------------------- |
| **SDK (Embed)** | Single-page apps, custom UX, keeping users on your site |
| **SDK (Overlay)** | Quick integration, modal checkout experience |
| **HTML Attributes** | Static sites, no-code solutions |
| **Query Parameters** | Email campaigns, external links, redirects |
## Browser Support [#browser-support]
Checkout Elements are built and tested against modern, up-to-date browsers and WebViews — Chrome,
Safari, Firefox, and Edge, and the Android WebView and iOS WKWebView components mobile apps embed
them in.
This covers the large majority of in-app browsers and WebViews used by mobile apps that embed
the checkout. If a buyer's browser falls below this floor, the checkout shows a message asking
them to open the link in an up-to-date browser instead of failing silently.
## Next Steps [#next-steps]
Complete SDK options and methods
All available billing prefill fields
# JavaScript SDK (/developers/checkout-elements/sdk)
The Fungies JavaScript SDK provides a programmatic way to open checkout experiences in your web application.
## Installation [#installation]
```bash
npm install @fungies/fungies-js
```
```bash
yarn add @fungies/fungies-js
```
```bash
pnpm add @fungies/fungies-js
```
```html
```
## Initialization [#initialization]
```javascript
import { Fungies } from "@fungies/fungies-js";
// Initialize the SDK
Fungies.Initialize({
enableDataAttributes: true, // default: true
});
```
## Opening Checkout [#opening-checkout]
### Overlay Mode [#overlay-mode]
Opens checkout in a full-screen modal overlay:
```javascript
Fungies.Checkout.open({
checkoutUrl: "https://yourstore.fungies.io/checkout-element/your-checkout-id",
settings: {
mode: "overlay",
},
});
```
### Embed Mode [#embed-mode]
Renders checkout inside a target DOM element:
```javascript
Fungies.Checkout.open({
checkoutUrl: "https://yourstore.fungies.io/checkout-element/your-checkout-id",
settings: {
mode: "embed",
frameTarget: "checkout-container", // ID of the target element
},
});
```
```html
```
## Checkout Options [#checkout-options]
| Option | Type | Description |
| ---------------------- | ------------------------ | ---------------------------------------------------------- |
| `checkoutUrl` | `string` | **Required.** The checkout URL from your Fungies dashboard |
| `settings.mode` | `"overlay"` \| `"embed"` | Display mode for the checkout |
| `settings.frameTarget` | `string` | DOM element ID for embed mode |
| `customerEmail` | `string` | Customer email (deprecated, use `billingData.email`) |
| `billingData` | `object` | Prefilled billing information (see below) |
| `discountCode` | `string` | Discount code to apply |
| `quantity` | `number` | Product quantity |
| `items` | `array` | Items for multi-product checkout |
| `customFields` | `object` | Custom field values |
## Billing Data Object [#billing-data-object]
Prefill customer billing information:
```javascript
Fungies.Checkout.open({
checkoutUrl: "...",
settings: { mode: "overlay" },
billingData: {
email: "customer@example.com",
firstName: "John",
lastName: "Doe",
country: "US", // ISO 3166-1 alpha-2 code
state: "CA", // State/province code
city: "Los Angeles",
zipCode: "90001",
},
});
```
| Field | Type | Description |
| ----------- | -------- | --------------------------------------------------------- |
| `email` | `string` | Customer email address |
| `firstName` | `string` | Customer first name |
| `lastName` | `string` | Customer last name |
| `country` | `string` | Country code (ISO 3166-1 alpha-2, e.g., `US`, `GB`, `PL`) |
| `state` | `string` | State or province |
| `city` | `string` | City name |
| `zipCode` | `string` | Postal/ZIP code |
## Multi-Product Checkout [#multi-product-checkout]
Specify quantities for multiple offers:
```javascript
Fungies.Checkout.open({
checkoutUrl: "...",
settings: { mode: "overlay" },
items: [
{ offerId: "offer-id-1", quantity: 2 },
{ offerId: "offer-id-2", quantity: 1 },
],
});
```
## Custom Fields [#custom-fields]
Pass custom field values to the checkout:
```javascript
Fungies.Checkout.open({
checkoutUrl: "...",
settings: { mode: "overlay" },
customFields: {
user_id: "usr_123",
server_region: "US-West",
},
});
```
## Closing Checkout [#closing-checkout]
Programmatically close the checkout:
```javascript
Fungies.Checkout.close();
```
## Listening to checkout events [#listening-to-checkout-events]
The SDK listens for messages from the checkout iframe and dispatches DOM events on `document`. This happens automatically when you call `Fungies.Initialize()` or use the `data-auto-init` script attribute — no extra setup is required.
Use these events to run your own logic after a purchase (redirect to a thank-you page, refresh entitlements, show a confirmation modal, etc.).
| Event | When it fires |
| --------------------------- | ------------------------------------------------------------- |
| `fungies:checkout:complete` | The customer completed checkout successfully |
| `fungies:checkout:close` | The checkout was closed (by the customer or after completion) |
Import event name constants from the package (recommended):
```javascript
import { DOM_CHECKOUT_EVENTS, Fungies } from "@fungies/fungies-js";
Fungies.Initialize();
document.addEventListener(DOM_CHECKOUT_EVENTS.COMPLETE, () => {
window.location.href = "/thank-you";
});
document.addEventListener(DOM_CHECKOUT_EVENTS.CLOSE, () => {
console.log("Checkout closed");
});
```
With the CDN script tag, register listeners after the SDK loads:
```html
```
In React, attach listeners in a `useEffect` and remove them on unmount:
```tsx
import { useEffect } from "react";
import { DOM_CHECKOUT_EVENTS, Fungies } from "@fungies/fungies-js";
function App() {
useEffect(() => {
Fungies.Initialize();
const onComplete = () => {
window.location.href = "/thank-you";
};
document.addEventListener(DOM_CHECKOUT_EVENTS.COMPLETE, onComplete);
return () => {
document.removeEventListener(DOM_CHECKOUT_EVENTS.COMPLETE, onComplete);
};
}, []);
return (
);
}
```
## DOM Scanning [#dom-scanning]
If you dynamically add checkout buttons after page load, trigger a manual scan:
```javascript
Fungies.ScanDOM();
```
## Complete Example [#complete-example]
```javascript
import { Fungies } from "@fungies/fungies-js";
// Initialize
Fungies.Initialize();
// Open checkout with all options
document.getElementById("buy-button").addEventListener("click", () => {
Fungies.Checkout.open({
checkoutUrl: "https://yourstore.fungies.io/checkout-element/abc123",
settings: {
mode: "overlay",
},
billingData: {
email: currentUser.email,
firstName: currentUser.firstName,
lastName: currentUser.lastName,
country: currentUser.country,
},
discountCode: "SUMMER20",
quantity: 1,
customFields: {
user_id: currentUser.id,
},
});
});
```
# Webhooks Overview (/developers/webhooks/overview)
Webhooks let your application receive real-time notifications when events occur in your Fungies account. Instead of polling the API, Fungies pushes event data directly to your server.
## Why Use Webhooks? [#why-use-webhooks]
Webhooks are essential for building reactive integrations:
* **Fulfill orders automatically** - Deliver digital products when a payment succeeds
* **Sync with your database** - Keep your user records up-to-date with subscription changes
* **Trigger workflows** - Send confirmation emails, update inventory, or notify your team
* **Handle async events** - Respond to events that happen outside of direct API calls
## How It Works [#how-it-works]
1. An event occurs (e.g., `payment_success`)
2. Fungies sends an HTTP POST request to your webhook URL
3. Your server processes the event and returns a `2xx` response
4. Fungies marks the delivery as successful
## Event Payload [#event-payload]
Each webhook delivers a JSON payload containing an [Event object](/core-resources/event):
```json
{
"id": "evt_abc123",
"type": "payment_success",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"testMode": false,
"data": {
"order": {
"object": "order",
"id": "ord_xyz789"
// ... order details
}
}
}
```
## Subscription checkouts and fulfillment [#subscription-checkouts-and-fulfillment]
New subscriptions emit **separate events** from different stages of checkout. They can arrive in any order.
| Event | When it fires | Use for fulfillment? |
| ---------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `subscription_created` | Stripe creates the subscription object (often while the first payment is still `PENDING`) | **No** — informational only unless `data.lastPayment.status` is `PAID` |
| `payment_success` | The initial or renewal payment is confirmed (`PAID`) | **Yes** — grant access, deliver goods, sync your database |
| `subscription_updated` | The subscription status changes (e.g. `incomplete` → `active` once the first payment confirms) | **Reconcile** — treat it as a prompt to re-check state, not as an ordered source of truth |
Do not fulfill subscription purchases on `subscription_created` alone when `data.lastPayment.status`, `data.order.status`, or `data.subscription.status` is `PENDING` or `incomplete`. Wait for `payment_success`, or ignore the event until payment fields show `PAID` / `active`.
**Webhook events can arrive out of order, and you cannot reorder them from the payload.** Delivery is asynchronous, retried, and batched, and the event payload carries no timestamp to sort by. Because `subscription_created` (`incomplete`) and `payment_success` (`active`) race, a stateless consumer can receive `active` and then `incomplete` for the same subscription.
Handle this explicitly — do **not** derive the current state from whichever event happened to arrive last:
* **Don't regress on stale events.** Never downgrade a subscription you have already seen as `active` / `PAID` because a later-delivered `subscription_created` or `subscription_updated` reports `incomplete`.
* **Reconcile from the API when it matters.** A `subscription_updated` event tells you the status *changed*; to get the authoritative current status, retrieve the subscription with `GET /v0/subscriptions/{id}` rather than trusting the status on the last event you received.
Enable **`payment_success`** on every webhook endpoint that fulfills orders. If only `subscription_created` is selected, you will not receive a paid signal when checkout completes.
For renewals, use `payment_success` (initial and interval charges) or `subscription_interval` depending on your integration; the initial signup should always key off `payment_success` for the first charge.
## Delivery Guarantees [#delivery-guarantees]
Fungies guarantees **at-least-once delivery** for all webhook events. Your endpoint should handle potential duplicate events idempotently.
| Behavior | Details |
| ----------------- | ------------------------------- |
| Protocol | HTTPS (required for production) |
| Method | POST |
| Expected response | `2xx` status code |
| Retry attempts | 5 times on failure |
| Timeout | 30 seconds |
## Securing Your Webhooks [#securing-your-webhooks]
Every webhook request includes an `x-fngs-signature` header containing an HMAC-SHA256 signature. Use your webhook secret to verify that requests actually came from Fungies.
```
x-fngs-signature: sha256_6808ed5be1262b60818359fa586145810d0793e8a677f1326520d3844e21b640
```
Always verify webhook signatures in production. This prevents attackers from sending fake events to your endpoint.
## Next Steps [#next-steps]
Create your webhook endpoint and register it with Fungies
Test your integration locally with ngrok or webhook.site
# Set Up Webhooks (/developers/webhooks/setup)
This guide walks you through setting up a webhook endpoint to receive events from Fungies.
## Quick Start [#quick-start]
Build an HTTP endpoint that accepts POST requests
Add your endpoint URL in the Fungies Dashboard
Validate that requests are from Fungies
## 1. Create Your Endpoint Handler [#1-create-your-endpoint-handler]
Your webhook endpoint needs to:
* Accept `POST` requests with a JSON body
* Return a `2xx` status code quickly (before any heavy processing)
* Handle the [Event object](/core-resources/event) payload
Return your `2xx` response immediately, then process the event asynchronously. This prevents timeouts on long-running operations.
### Basic Handler Example [#basic-handler-example]
```typescript
// This example uses Express to receive webhooks
import express from "express";
const app = express();
app.post('/webhook', express.json({type: 'application/json'}), (request, response) => {
const event = request.body;
// Handle the event
switch (event.type) {
case 'payment_success':
// handlePaymentSuccess(event.data);
break;
case 'payment_refund':
// handlePaymentRefund(event.data);
break;
// ... handle other event types
default:
console.log(`Unhandled event type ${event.type}`);
}
response.json({success: true});
});
app.listen(8081, () => console.log('Running on port 8081'));
```
## 2. Register in the Fungies Dashboard [#2-register-in-the-fungies-dashboard]
Once your endpoint is ready (or you're using a tool like [ngrok](https://ngrok.com) for local development):
1. Go to [Fungies Dashboard](https://app.fungies.io) → **Developers** → **Webhooks**
2. Click **Create a webhook**
3. Configure your webhook:
| Field | Description |
| ---------- | --------------------------------------------------- |
| **URL** | Your endpoint URL (must be HTTPS in production) |
| **Secret** | A random string to sign events (save this securely) |
| **Events** | Select which event types to receive |
4. Click **Save**
You can create multiple webhooks for different event types, or use a single endpoint that handles all events.
For subscription products, include **`payment_success`** if your handler fulfills orders. `subscription_created` alone is not enough — see [Subscription checkouts and fulfillment](/developers/webhooks/overview#subscription-checkouts-and-fulfillment).
## 3. Verify Webhook Signatures [#3-verify-webhook-signatures]
Never skip signature verification in production. Without it, anyone could send fake events to your endpoint.
Fungies signs every webhook payload using HMAC-SHA256 with your webhook secret. The signature is included in the `x-fngs-signature` header:
```
x-fngs-signature: sha256_6808ed5be1262b60818359fa586145810d0793e8a677f1326520d3844e21b640
```
### How to Verify [#how-to-verify]
1. Get the raw request body (before any JSON parsing)
2. Compute HMAC-SHA256 using your webhook secret
3. Compare with the signature in the header
### Verification Function [#verification-function]
```typescript
// This example shows standalone function to verify webhook event signature
import crypto from "crypto";
const verifyWebhookEventSignature = (payload: Buffer, signature: string, secret: string) => {
const hmac = crypto.createHmac("sha256", secret);
return `sha256_${hmac.update(payload.toString()).digest("hex")}` === signature;
};
```
### Express Middleware Example [#express-middleware-example]
```typescript
// This example uses Express to receive webhooks
import express, { Response, Request } from "express";
import crypto from "crypto";
const WEBHOOK_SECRET = "";
const app = express();
const verifyWebhookEventSignature = (request: Request, response: Response, buf: Buffer, encoding: string) => {
const signature = request.header('x-fngs-signature');
const hmac = crypto.createHmac("sha256", WEBHOOK_SECRET);
const calcSignature = `sha256_${hmac.update(buf.toString(encoding)).digest("hex")}`;
request["signature_match"] = (calcSignature === signature);
};
app.post('/webhook', express.json({
type: 'application/json',
verify: verifyWebhookEventSignature
}), (request, response) => {
if(!request["signature_match"]) {
return response.status(403).send('signature mismatch');
}
// Do something with the event
// ...
});
app.listen(8081, () => console.log('Running on port 8081'));
```
## Common Patterns [#common-patterns]
### Idempotent Event Handling [#idempotent-event-handling]
Since webhooks guarantee at-least-once delivery, your handler might receive the same event multiple times. Use the event ID to prevent duplicate processing:
```javascript
app.post('/webhook', async (req, res) => {
const event = req.body;
// Check if we've already processed this event
const processed = await db.events.findById(event.id);
if (processed) {
return res.status(200).send('Already processed');
}
// Process the event
await handleEvent(event);
// Mark as processed
await db.events.create({ id: event.id, processedAt: new Date() });
res.status(200).send('OK');
});
```
### Async Processing with Queues [#async-processing-with-queues]
For complex operations, queue events for background processing:
```javascript
app.post('/webhook', async (req, res) => {
// Respond immediately
res.status(200).send('Received');
// Queue for async processing
await queue.add('process-webhook', req.body);
});
```
## Troubleshooting [#troubleshooting]
| Issue | Solution |
| -------------------- | ------------------------------------------------------------ |
| Not receiving events | Check your endpoint is publicly accessible and returns `2xx` |
| Signature mismatch | Ensure you're using the raw body, not parsed JSON |
| Timeout errors | Return `2xx` faster, process events asynchronously |
| Duplicate events | Implement idempotent handling using event IDs |
## Next Steps [#next-steps]
Learn how to test webhooks locally before deploying to production
# Test Webhooks (/developers/webhooks/test)
Before deploying your webhook handler to production, you'll want to verify it works correctly. This guide covers two approaches: using webhook.site for quick testing, and ngrok for testing your actual code.
## Option 1: Quick Test with webhook.site [#option-1-quick-test-with-webhooksite]
[webhook.site](https://webhook.site/) is a free tool that gives you a temporary URL to receive and inspect webhook payloads. It's perfect for understanding the event structure before writing any code.
Go to [webhook.site](https://webhook.site/) and copy your unique URL
Navigate to [Fungies Dashboard](https://app.fungies.io) → **Developers** → **Webhooks** → **Create a webhook**
Open your webhook and click the **Test** button. Select an event type and click **Send**.
Check webhook.site to see the full event payload and headers.
Test events include `"testMode": true` in the payload and don't affect any real data in your account.
## Option 2: Test Your Code with ngrok [#option-2-test-your-code-with-ngrok]
[ngrok](https://ngrok.com) creates a secure tunnel from the internet to your local machine. This lets you test your actual webhook handler code with real Fungies events.
### Install ngrok [#install-ngrok]
```bash
# macOS (Homebrew)
brew install ngrok
# Or download from https://ngrok.com/download
```
### Start the Tunnel [#start-the-tunnel]
Run your local server, then create a tunnel to it:
```bash
# Start your local server (example: running on port 3000)
npm run dev
# In another terminal, create the tunnel
ngrok http 3000
```
You'll see output like this:
```bash
Session Status online
Forwarding https://abc123.ngrok.io -> localhost:3000
```
### Register Your Tunnel URL [#register-your-tunnel-url]
1. Copy the `https://...ngrok.io` URL
2. Go to [Fungies Dashboard](https://app.fungies.io) → **Developers** → **Webhooks**
3. Create a webhook with your ngrok URL + your endpoint path (e.g., `https://abc123.ngrok.io/webhooks/fungies`)
### Send Test Events [#send-test-events]
Use the **Test** button in the Dashboard to send events to your local server. Check your terminal logs to see the events being received and processed.
ngrok provides a local web interface at `http://localhost:4040` where you can inspect all requests and replay them for debugging.
## Testing Best Practices [#testing-best-practices]
### Use a Separate Webhook for Testing [#use-a-separate-webhook-for-testing]
Create a dedicated webhook endpoint for development to avoid mixing test events with production data.
### Verify Your Error Handling [#verify-your-error-handling]
Test how your handler responds to:
* Invalid payloads
* Missing required fields
* Duplicate events (same event ID sent twice)
* Network timeouts
### Log Everything During Development [#log-everything-during-development]
```javascript
app.post('/webhooks/fungies', (req, res) => {
console.log('Received webhook:', {
headers: req.headers,
body: JSON.stringify(req.body, null, 2)
});
// ... your handler logic
res.status(200).send('OK');
});
```
## Checklist Before Going Live [#checklist-before-going-live]
Endpoint returns
`2xx`
within 30 seconds
Signature verification is implemented and working
Handler is idempotent (handles duplicate events)
Errors are logged for debugging
Endpoint uses HTTPS
## Next Steps [#next-steps]
Review webhook concepts and delivery guarantees
See all available event types and their payloads
# Custom Domain Setup (/developers/custom-domain/overview)
Custom domains let you serve your Fungies store from your own domain (e.g., `store.yourgame.com`) instead of the default Fungies subdomain.
## Benefits [#benefits]
* **Brand consistency** - Keep customers on your domain throughout the purchase flow
* **Trust** - Customers feel more confident buying from a domain they recognize
* **SEO** - Build domain authority for your own properties
## Prerequisites [#prerequisites]
Before you begin, make sure you have:
Access to your domain's DNS settings (through your registrar or DNS provider)
A domain or subdomain you want to use (e.g.,
`store.yourdomain.com`
)
## Setup Process [#setup-process]
Go to **Settings** → **Domain** in the Fungies Dashboard and enter your custom domain
Add the provided DNS records to your domain's DNS settings
Fungies will automatically validate your domain and issue SSL certificates
## Step 1: Enter Your Domain [#step-1-enter-your-domain]
1. Go to [Fungies Dashboard](https://app.fungies.io) → **Settings** → **Domain**
2. In the **Custom domain** section, enter your domain (e.g., `store.yourgame.com`)
3. Click **Submit** to start the setup process
We recommend using a subdomain like `store.` or `shop.` rather than your root domain for easier DNS configuration.
## Step 2: Add DNS Records [#step-2-add-dns-records]
After submitting your domain, you'll see a table of DNS records to add. There are typically three types:
### Certificate Validation Record [#certificate-validation-record]
| Type | Name | Value | Proxy |
| ----- | ------------------------ | ------------------------------ | ----- |
| CNAME | `_abc123.yourdomain.com` | `_xyz789.acm-validations.aws.` | OFF |
This record proves you own the domain and allows AWS to issue an SSL certificate.
### Hostname Validation Record [#hostname-validation-record]
| Type | Name | Value | Proxy |
| ----- | ---------------------- | ------------------ | ----- |
| CNAME | `store.yourdomain.com` | `cname.fungies.io` | OFF |
This record points your domain to the Fungies servers.
### Certificate Renewal Record (DCV delegation) [#certificate-renewal-record-dcv-delegation]
| Type | Name | Value | Proxy |
| ----- | -------------------------------------- | ------------------------------------- | ----- |
| CNAME | `_acme-challenge.store.yourdomain.com` | `store.yourdomain.com.dcv.fungies.io` | OFF |
This `_acme-challenge` CNAME delegates certificate control validation (DCV) to Fungies. Once it's in place, Fungies handles **both the initial SSL issuance and automatic renewal** on your behalf — you don't need to add or manage any other certificate record for this domain.
Add the `_acme-challenge` record as a **CNAME** exactly as shown. Do **not** also add a `_acme-challenge` **TXT** record — a CNAME cannot coexist with another record type at the same name (RFC 1034/2181), and the extra TXT will block validation and renewal.
**Manual TXT fallback:** if the Dashboard shows a `_acme-challenge` **TXT** record instead of the delegation CNAME, DCV delegation isn't available for your setup. In that case add the TXT record as shown and do not add the `_acme-challenge` CNAME. The Dashboard only ever asks for one of the two.
If you're using Cloudflare, make sure the **Proxy** toggle is set to **OFF** (DNS only, grey cloud) for all records. Proxied records will prevent validation.
## Step 3: Wait for Validation [#step-3-wait-for-validation]
The setup status shows three stages:
| Stage | Description | Typical Time |
| ------------------------- | ----------------------------------------- | ------------ |
| **Certificates Issued** | Fungies requests SSL certificates | Instant |
| **Certificate Validated** | DNS records verified, certificates issued | 5-30 minutes |
| **Domain Setup** | Domain is fully configured and live | 1-5 minutes |
DNS changes can take up to 24 hours to propagate globally, though most complete within 30 minutes. Click **Refresh** in the Dashboard to check the current status.
## Root Domain Setup (Apex Domains) [#root-domain-setup-apex-domains]
If you want to use a root domain (e.g., `yourgame.com` without `www` or any subdomain), additional configuration is required.
Root domains require CNAME flattening or ALIAS/ANAME record support, which is only available on certain DNS providers like **Cloudflare**, **Namecheap**, **DNSimple**, and some others.
### Recommended Approach [#recommended-approach]
We strongly recommend one of these options:
1. **Use a subdomain** - Set up `www.yourdomain.com` or `store.yourdomain.com` instead
2. **Move DNS to Cloudflare** - [Migrate your DNS to Cloudflare](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) for CNAME flattening support
### Cloudflare Configuration [#cloudflare-configuration]
If using Cloudflare for a root domain:
1. Add the CNAME record as provided
2. Set the proxy status to **DNS only** (grey cloud)
3. Cloudflare will automatically flatten the CNAME at the root
## Troubleshooting [#troubleshooting]
### Certificate Validation Taking Too Long [#certificate-validation-taking-too-long]
If certificate validation is stuck for more than 30 minutes, you may need to add a CAA record:
| Type | Name | Value |
| ---- | ---------------- | ---------------------- |
| CAA | `yourdomain.com` | `0 issue "amazon.com"` |
This authorizes Amazon (AWS) to issue certificates for your domain.
CAA records are typically only needed if you have existing CAA records that restrict certificate issuance. If you don't have any CAA records, you probably don't need to add one.
### Domain Still Not Working [#domain-still-not-working]
Check these common issues:
| Issue | Solution |
| -------------------------------------- | -------------------------------------------------------------------------- |
| Proxy enabled on Cloudflare | Disable proxy (set to DNS only / grey cloud) |
| Wrong CNAME value | Ensure you copied the exact value from the Dashboard |
| TTL too high | Lower TTL to 300 (5 minutes) for faster propagation |
| Cached DNS | Try from a different network or use [DNS Checker](https://dnschecker.org/) |
| Can't add domain — removal in progress | Wait for Delete to finish, or press **Retry removal** |
| "Domain already in use" | Another workspace already uses this host — remove it there first |
### Need Help? [#need-help]
If you're still having issues after 24 hours, contact us:
* **Discord**: [discord.gg/yfH5ZyTZH4](https://discord.gg/yfH5ZyTZH4)
* **Email**: [support@fungies.io](mailto:support@fungies.io)
## Removing a Custom Domain [#removing-a-custom-domain]
To remove your custom domain and revert to the default Fungies subdomain:
1. Go to **Settings** → **Domain**
2. Click **Delete** next to your custom domain
3. Confirm the action
The removal process takes up to 10 minutes. Your store will remain accessible via the default Fungies domain.
While removal is in progress you cannot add a new custom domain. If removal stalls, press **Retry removal** — it is safe to run again.
### Can't re-add after deleting? [#cant-re-add-after-deleting]
| Issue | Solution |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| "Domain removal is still in progress" | Wait for removal to finish, or press **Retry removal** |
| "Domain already in use" | Another Fungies workspace is using this domain — remove it there first, or pick a different host |
| "Duplicate custom hostname found" (legacy) | Rare leftover from an interrupted removal — press **Retry removal**, wait a few minutes, then add again |
# Custom Fields Overview (/developers/customer-data/overview)
Custom fields let you collect additional information from customers during checkout. This is essential for integrating Fungies with your application, especially for in-game purchases or user-specific products.
## Use Cases [#use-cases]
Custom fields are commonly used to:
* **Identify players** - Collect a `playerId` or `username` to deliver in-game items
* **Server selection** - Let customers choose which game server to receive items on
* **Account linking** - Capture external account IDs to sync purchases with your system
* **Order customization** - Collect preferences or configuration options
## How It Works [#how-it-works]
1. You define custom fields in the Fungies Dashboard, each with a **Key** (e.g. `playerId`)
2. Customers fill in the fields during checkout
3. The data is included in webhook event payloads, keyed by that same **Key**
4. Your application uses the data to fulfill the order
The **Key** is the exact property name used in the payload (`data.items[].customFields.`). Only checkout values whose key matches a defined field's Key exactly are forwarded — a mismatched or undefined key is dropped silently rather than sent through empty. This is the most common cause of an empty-looking `customFields` object.
## Example Event Payload [#example-event-payload]
When a customer completes a purchase, the custom field data is included under the `customFields` key:
```json
{
"id": "6f97c060-570d-46e8-81af-033539210df3",
"data": {
"items": [
{
"id": "a0bf335d-25b9-4eda-bf92-f45f7bd2ed18",
"name": "Legendary Sword",
"quantity": "1",
"internalId": null,
"customFields": {
"playerId": "123",
"serverName": "US-1"
}
},
{
"id": "adb745b7-b23a-4960-ab21-7f21b7312840",
"name": "Legendary Shield",
"quantity": "1",
"internalId": null,
"customFields": {
"playerId": "123",
"serverName": "US-1"
}
}
]
// ...
}
```
## Key Concepts [#key-concepts]
Custom fields are defined at the project level and apply to all products in that project
All custom fields are required—customers must fill them out to complete checkout
Fields can be validated with regex patterns or your own validation API
Support for text inputs and dropdown selections
## Next Steps [#next-steps]
Create custom fields in the Fungies Dashboard
Add validation rules to ensure data quality
# Set Up Custom Fields (/developers/customer-data/setup)
This guide walks you through creating custom fields to collect additional information from customers during checkout.
## Prerequisites [#prerequisites]
You have
[webhooks set up](/developers/webhooks/setup)
to receive custom field data
## Create a Custom Field [#create-a-custom-field]
Go to [Fungies Dashboard](https://app.fungies.io) → **Products** → **Game Assets**
Click **Add project** to create a new one, or edit an existing project
In the project form, scroll to **Custom Fields** and click **Add field**
Choose a field type and configure its settings (see below)
Click **Save** to apply your changes
Custom fields apply to all products within a project. Customers must fill in all custom fields to complete checkout.
## Field Types [#field-types]
### Text Field [#text-field]
A free-form text input for collecting strings like usernames, IDs, or notes.
| Setting | Description |
| ------------------ | --------------------------------------------------------- |
| **Label** | Field label shown to customers (e.g., "Player ID") |
| **Placeholder** | Hint text shown in the empty field |
| **Regex** | Optional pattern validation (e.g., `^[a-zA-Z0-9]{3,20}$`) |
| **Validation URL** | Optional API endpoint for custom validation |
**Common uses:**
* Player IDs / Usernames
* Email addresses
* External account identifiers
* Custom notes or messages
### Selection Field [#selection-field]
A dropdown menu where customers choose from predefined options.
| Setting | Description |
| ----------- | ------------------------------------------------------ |
| **Label** | Field label shown to customers (e.g., "Select Server") |
| **Options** | List of choices (minimum 1 required) |
**Common uses:**
* Game server selection
* Region preference
* Product variants or configurations
## Example Configurations [#example-configurations]
### Player ID Field [#player-id-field]
```
Label: Player ID
Placeholder: Enter your in-game player ID
Regex: ^[a-zA-Z0-9_]{3,32}$
```
This accepts alphanumeric player IDs between 3 and 32 characters.
### Server Selection Field [#server-selection-field]
```
Label: Select Your Server
Options:
- US East
- US West
- EU Central
- Asia Pacific
```
Customers select which server should receive their purchased items.
### Discord Username Field [#discord-username-field]
```
Label: Discord Username
Placeholder: username#1234
Regex: ^.{3,32}#[0-9]{4}$
```
Validates the classic Discord username format.
## Receiving Custom Field Data [#receiving-custom-field-data]
When a customer completes a purchase, the custom field data is included **on each line item**, under `data.items[].customFields` — not at the top level of the event:
```json
{
"id": "evt_123e4567-e89b-12d3-a456-426614174000",
"type": "payment_success",
"data": {
"items": [
{
"object": "item",
"id": "itm_abc123",
"name": "Legendary Sword",
"customFields": {
"playerId": "Player_12345",
"server": "US East"
}
}
]
}
}
```
Your webhook handler can extract this data from each item and use it to fulfill the order in your system. See the [Event object reference](/core-resources/event#item-object) for the full payload shape.
**`customFields` is only populated for keys that exactly match a field's Key.** The **Key** you set in [Add a Custom Field](#add-a-custom-field) above is the literal property name that shows up under `customFields` in the webhook payload. Any value submitted at checkout whose key doesn't match a defined field's Key character-for-character (case-sensitive) is silently dropped — no error is raised, on either your side or ours.
If `customFields` shows up empty or missing values:
* Confirm the field is actually defined on the product (or project) being purchased — fields don't apply store-wide, only to the project/product they're configured on.
* Confirm the key your checkout flow sends matches the field's **Key** exactly.
* Double-check you're reading `data.items[].customFields`, not `data.customFields` — it's nested per line item.
## Best Practices [#best-practices]
**Keep it minimal.** Only ask for information you actually need. More fields = more friction = lower conversion rates.
* Use clear, descriptive labels that customers understand
* Add placeholder text showing the expected format
* Use regex validation to catch errors early
* Consider using selection fields when there's a fixed set of options
## Next Steps [#next-steps]
Learn how to validate customer input with regex patterns or your own API
# Validate Customer Data (/developers/customer-data/validate)
Validation helps ensure customers enter correct information, reducing fulfillment errors and support requests. Fungies supports two validation methods: regex patterns and custom validation URLs.
## Regex Validation [#regex-validation]
Use regular expressions to validate text field input directly in the browser. Invalid input is rejected before the customer can submit.
### How to Add Regex Validation [#how-to-add-regex-validation]
1. Edit your custom field in the Dashboard
2. Enter a regex pattern in the **Regex** field
3. Save your changes
Regex validation happens client-side, providing instant feedback to customers.
### Common Patterns [#common-patterns]
| Use Case | Pattern | Matches |
| --------------------- | ---------------------------------------------------------------- | ------------------- |
| Numbers only | `^[0-9]+$` | `12345` |
| Letters only | `^[a-zA-Z]+$` | `PlayerOne` |
| Alphanumeric | `^[a-zA-Z0-9]+$` | `Player123` |
| Username (3-20 chars) | `^[a-zA-Z0-9_]{3,20}$` | `cool_player_99` |
| Email | `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` | `user@example.com` |
| UUID | `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` | `550e8400-e29b-...` |
| Date (YYYY-MM-DD) | `^\d{4}-\d{2}-\d{2}$` | `2024-01-15` |
### Regex Tips [#regex-tips]
```javascript
// Start (^) and end ($) anchors ensure the entire input matches
^[a-zA-Z0-9]+$ // ✓ Matches: "abc123"
// ✗ Rejects: "abc 123" (space not allowed)
// Character classes define allowed characters
[a-zA-Z] // Any letter
[0-9] or \d // Any digit
[a-zA-Z0-9_] // Letters, digits, or underscore
// Quantifiers specify length
{3,20} // Between 3 and 20 characters
+ // One or more
* // Zero or more
```
Test your regex patterns at [regex101.com](https://regex101.com/) before adding them to Fungies.
## Custom Validation URL [#custom-validation-url]
For complex validation that can't be expressed as regex—like checking if a player ID exists in your database—use a custom validation endpoint.
### How It Works [#how-it-works]
1. Customer enters a value
2. Fungies sends a POST request to your validation URL
3. Your API checks if the value is valid
4. Return `200 OK` for valid, any other status for invalid
### Request Format [#request-format]
Fungies sends a POST request with the field value:
```json
{
"playerId": "Player_12345"
}
```
The key is your custom field's identifier, and the value is what the customer entered.
### Response [#response]
| Status | Meaning |
| ------------- | -------------------------------- |
| `200` | Valid - customer can proceed |
| `4xx` / `5xx` | Invalid - customer sees an error |
### Example Endpoint [#example-endpoint]
```javascript
// Express.js example
app.post('/validate/player-id', async (req, res) => {
const { playerId } = req.body;
// Check if player exists in your database
const player = await db.players.findById(playerId);
if (player) {
return res.status(200).json({ valid: true });
}
return res.status(400).json({
valid: false,
message: 'Player not found'
});
});
```
## Securing Your Validation Endpoint [#securing-your-validation-endpoint]
Without signature verification, anyone could call your validation endpoint. Always verify the signature in production.
Fungies signs validation requests the same way as webhooks. The signature is in the `x-fngs-signature` header:
```
x-fngs-signature: sha256_6808ed5be1262b60818359fa586145810d0793e8a677f1326520d3844e21b640
```
### Verify the Signature [#verify-the-signature]
Use your validation URL secret (set in the Dashboard) to verify requests:
```javascript
import crypto from 'crypto';
function verifySignature(payload, signature, secret) {
const expectedSignature = 'sha256_' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/validate/player-id', (req, res) => {
const signature = req.headers['x-fngs-signature'];
const rawBody = req.rawBody; // Must be raw, unparsed body
if (!verifySignature(rawBody, signature, process.env.VALIDATION_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// ... validation logic
});
```
See the [webhook signature verification guide](/developers/webhooks/setup#3-verify-webhook-signatures) for more details.
## Combining Validation Methods [#combining-validation-methods]
You can use both regex and URL validation on the same field:
1. **Regex runs first** - Catches format errors instantly (client-side)
2. **URL validation runs second** - Checks business logic (server-side)
This gives customers fast feedback on format errors while still validating against your backend.
## Next Steps [#next-steps]
Create and configure custom fields
Receive custom field data in your application
# Archive a discount (/api-reference/discounts/archive-a-discount)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new discount (/api-reference/discounts/create-a-new-discount)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get discount details (/api-reference/discounts/get-discount-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List discounts (/api-reference/discounts/list-discounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a discount (/api-reference/discounts/update-a-discount)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new Checkout Element (/api-reference/elements/create-a-new-checkout-element)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List checkout elements (/api-reference/elements/list-checkout-elements)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel order (/api-reference/orders/cancel-order)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get order details (/api-reference/orders/get-order-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List orders (/api-reference/orders/list-orders)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Refund order payment (/api-reference/orders/refund-order-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update order (/api-reference/orders/update-order)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a pending payment (/api-reference/payments/cancel-a-pending-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payment details (/api-reference/payments/get-payment-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payments (/api-reference/payments/list-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a plan to a product (/api-reference/products/add-a-plan-to-a-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a variant to a product (/api-reference/products/add-a-variant-to-a-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a plan (/api-reference/products/archive-a-plan)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a product (/api-reference/products/archive-a-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a variant (/api-reference/products/archive-a-variant)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new product (/api-reference/products/create-a-new-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Duplicate a product (/api-reference/products/duplicate-a-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get product details (/api-reference/products/get-product-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List products (/api-reference/products/list-products)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a plan (/api-reference/products/update-a-plan)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a product (/api-reference/products/update-a-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a variant (/api-reference/products/update-a-variant)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add product keys (/api-reference/offers/add-product-keys)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive an offer (/api-reference/offers/archive-an-offer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new offer (/api-reference/offers/create-a-new-offer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get offer details (/api-reference/offers/get-offer-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List offers (/api-reference/offers/list-offers)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove specific unsold key (/api-reference/offers/remove-specific-unsold-key)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove unsold keys (/api-reference/offers/remove-unsold-keys)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an offer (/api-reference/offers/update-an-offer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Calculate product price including tax (/api-reference/tax/calculate-product-price-including-tax)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel Subscription (/api-reference/subscriptions/cancel-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Charge Subscription (/api-reference/subscriptions/charge-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create Subscription (/api-reference/subscriptions/create-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# End Subscription Trial (/api-reference/subscriptions/end-subscription-trial)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Subscription (/api-reference/subscriptions/get-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Upcoming Subscription Payment (/api-reference/subscriptions/get-upcoming-subscription-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List Subscriptions (/api-reference/subscriptions/list-subscriptions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pause Subscription Collection (/api-reference/subscriptions/pause-subscription-collection)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update Subscription (/api-reference/subscriptions/update-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive User (/api-reference/users/archive-user)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new User account (/api-reference/users/create-a-new-user-account)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get User object (/api-reference/users/get-user-object)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get User's inventory items (/api-reference/users/get-users-inventory-items)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List and filter Users (/api-reference/users/list-and-filter-users)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Unarchive User (/api-reference/users/unarchive-user)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update User details (/api-reference/users/update-user-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a webhook (/api-reference/webhooks/archive-a-webhook)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a webhook (/api-reference/webhooks/create-a-webhook)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get webhook details (/api-reference/webhooks/get-webhook-details)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List a webhook's delivery attempts (/api-reference/webhooks/list-a-webhooks-delivery-attempts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List webhooks (/api-reference/webhooks/list-webhooks)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a test event (/api-reference/webhooks/send-a-test-event)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a webhook (/api-reference/webhooks/update-a-webhook)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}