# What is Pandabase? (/) Pandabase is the merchant of record (MoR) for your store. It handles payments, tax, disputes, and payouts for your digital products. ## Get started [#get-started] Create your store and accept your first payment. ## Learn more [#learn-more] Find a guide: How payments, fees, and settlements work. Product types, fulfillment modes, variants, and pricing. How checkout sessions and the payment flow work. Creating stores, storefronts, and managing your team. Supported countries, fees, and payout timelines. What happens when a customer files a chargeback. Get notified when events happen in your store. Integrate with the Pandabase API programmatically. # Pricing (/pricing) Pandabase is pay-as-you-go. There are no monthly fees, setup fees, or minimum commitments. You only pay when you make a sale. ## Included with pricing [#included-with-pricing] Display and accept all major card brands and local payment methods to increase revenue and support customers across multiple regions. The payments you receive are automatically settled in **USD**, regardless of the currency your customer paid in. ## Transaction fees [#transaction-fees] Two fees are deducted from each successful payment before the remainder is added to your available balance. | Fee | Amount | What it covers | | ---------------------- | --------------------- | ---------------------------------------------------------------------------- | | Platform fee | 5.9% + $0.20 | Tax calculation, fraud monitoring, order fulfillment, and merchant of record | | Payment processing fee | Variable rate + $0.30 | Card network interchange and acquirer processing | The processing rate varies by card type, region, and currency. The exact rate for each transaction is shown on the payment detail page in your dashboard. ## Dispute fee [#dispute-fee] **$20.00** per dispute, regardless of outcome. When a customer files a chargeback, the dispute fee is deducted from your balance along with the disputed amount. The fee is not refunded even if you [win the dispute](/store/manage-payments/disputes). ## Fraud alerts [#fraud-alerts] Fraud alerts from **Verifi** and **Ethoca** are charged at **$29.00** per alert and automatically refunded to help prevent disputes. ## Prevented disputes [#prevented-disputes] When Verifi or Ethoca resolves a chargeback with the issuing bank before it becomes a formal dispute, the disputed amount is returned to the cardholder and a **$30.00** prevention fee is deducted from your balance. Prevented disputes don't count toward your dispute rate. See [disputes](/store/manage-payments/disputes) for details. ## Payouts [#payouts] The minimum payout amount is **$50.00** in all countries. Each payout has a base fee of **$1.50**. Payouts to accounts outside the **United States** also incur cross-border and currency conversion fees. You can preview exact fees before requesting a payout. See the [payout guide](/store/finances/payouts) for more details. ## Refunds [#refunds] Issuing a refund is free. The refunded amount is deducted from your store's available balance, but the original transaction fees are **not** returned. ## Worked example [#worked-example] A customer pays **$50.00** for a product (illustrative processing rate of 2.9%): | | Amount | | ------------------------------------- | ---------- | | Sale | $50.00 | | Payment processing fee (2.9% + $0.30) | -$1.75 | | Platform fee (5.9% + $0.20) | -$3.15 | | **Added to balance** | **$45.10** | When you request a payout in the US: | | Amount | | --------------- | ---------- | | Payout amount | $45.10 | | Payout fee | -$1.50 | | **You receive** | **$43.60** | # Start selling (/quickstart) You must be **at least 18 years old**, wherever you live, and register and verify your merchant account using your own identity and ID. Using someone else's ID, including a parent's, violates our [age and identity requirements](/account#age-and-identity-requirements), even with their permission. ### Create your account [#create-your-account] Sign up at [merchant.pandabase.io](https://merchant.pandabase.io/auth/signup) with your email. We'll send you a magic link to verify your account. ### Complete onboarding [#complete-onboarding] Click the magic link and finish your profile (username, first and last name, phone number, country), then accept our legal policies. ### Launch your store [#launch-your-store] Tell us about your store: name, URL, statement descriptor, support email, country of operation, category, description, and what you're selling. Your storefront goes live as soon as you submit. Store creation form | Country | Region | | ------------------------- | ------------ | | ๐Ÿ‡จ๐Ÿ‡ฆ Canada | Americas | | ๐Ÿ‡บ๐Ÿ‡ธ United States | Americas | | ๐Ÿ‡ฆ๐Ÿ‡น Austria | Europe | | ๐Ÿ‡ง๐Ÿ‡ช Belgium | Europe | | ๐Ÿ‡ญ๐Ÿ‡ท Croatia | Europe | | ๐Ÿ‡จ๐Ÿ‡พ Cyprus | Europe | | ๐Ÿ‡จ๐Ÿ‡ฟ Czech Republic | Europe | | ๐Ÿ‡ฉ๐Ÿ‡ฐ Denmark | Europe | | ๐Ÿ‡ช๐Ÿ‡ช Estonia | Europe | | ๐Ÿ‡ซ๐Ÿ‡ฎ Finland | Europe | | ๐Ÿ‡ซ๐Ÿ‡ท France | Europe | | ๐Ÿ‡ฉ๐Ÿ‡ช Germany | Europe | | ๐Ÿ‡ฌ๐Ÿ‡ง Great Britain | Europe | | ๐Ÿ‡ฌ๐Ÿ‡ท Greece | Europe | | ๐Ÿ‡ญ๐Ÿ‡บ Hungary | Europe | | ๐Ÿ‡ฎ๐Ÿ‡ช Ireland | Europe | | ๐Ÿ‡ฎ๐Ÿ‡น Italy | Europe | | ๐Ÿ‡ฑ๐Ÿ‡ป Latvia | Europe | | ๐Ÿ‡ฑ๐Ÿ‡น Lithuania | Europe | | ๐Ÿ‡ฑ๐Ÿ‡บ Luxembourg | Europe | | ๐Ÿ‡ฒ๐Ÿ‡น Malta | Europe | | ๐Ÿ‡ณ๐Ÿ‡ฑ Netherlands | Europe | | ๐Ÿ‡ณ๐Ÿ‡ด Norway | Europe | | ๐Ÿ‡ต๐Ÿ‡ฑ Poland | Europe | | ๐Ÿ‡ต๐Ÿ‡น Portugal | Europe | | ๐Ÿ‡ท๐Ÿ‡ด Romania | Europe | | ๐Ÿ‡ธ๐Ÿ‡ฐ Slovakia | Europe | | ๐Ÿ‡ธ๐Ÿ‡ฎ Slovenia | Europe | | ๐Ÿ‡ช๐Ÿ‡ธ Spain | Europe | | ๐Ÿ‡ธ๐Ÿ‡ช Sweden | Europe | | ๐Ÿ‡จ๐Ÿ‡ญ Switzerland | Europe | | ๐Ÿ‡น๐Ÿ‡ท Turkiye | Europe | | ๐Ÿ‡ฆ๐Ÿ‡บ Australia | Asia-Pacific | | ๐Ÿ‡ญ๐Ÿ‡ฐ Hong Kong | Asia-Pacific | | ๐Ÿ‡ฎ๐Ÿ‡ณ India | Asia-Pacific | | ๐Ÿ‡ณ๐Ÿ‡ฟ New Zealand | Asia-Pacific | | ๐Ÿ‡ต๐Ÿ‡ญ Philippines | Asia-Pacific | | ๐Ÿ‡ธ๐Ÿ‡ฌ Singapore | Asia-Pacific | | ๐Ÿ‡น๐Ÿ‡ผ Taiwan | Asia-Pacific | | ๐Ÿ‡น๐Ÿ‡ญ Thailand | Asia-Pacific | | ๐Ÿ‡ฎ๐Ÿ‡ฑ Israel | Middle East | | ๐Ÿ‡ถ๐Ÿ‡ฆ Qatar | Middle East | | ๐Ÿ‡ฆ๐Ÿ‡ช United Arab Emirates | Middle East | | ๐Ÿ‡ฒ๐Ÿ‡ฆ Morocco | Africa | ### Verify your identity [#verify-your-identity] Submit your ID (handled by Sumsub; Pandabase never stores it) and verify your phone number with 2FA. You can start accepting payments immediately, but KYC is required before we release payouts. Identity verification screen **Why do we need your ID?** Your ID lets us confirm who we're sending money to, as required by anti-money-laundering and know-your-customer (KYC) laws in every country we operate in (in the US, that's the Bank Secrecy Act and USA PATRIOT Act). We don't store your ID ourselves. It's processed by Sumsub, a third-party identity verification platform we use to combat fraud and stay compliant. It isn't shared or used beyond that. ### Complete the store questionnaire [#complete-the-store-questionnaire] Open the questionnaire from the banner at the top of your dashboard and answer the questions about your business model. Store questionnaire banner ### Start accepting payments [#start-accepting-payments] Share your storefront link or integrate the [Checkout API](/developers/learn/hosted-checkout) to take your first payment. We handle tax, disputes, and payouts automatically. Funds land in your bank account on your chosen schedule. # Manage your account (/account) ## Creating an account [#creating-an-account] ### Age and identity requirements [#age-and-identity-requirements] You must be **at least 18 years old** to open or use a Pandabase merchant account. This requirement applies worldwide, with no exceptions for parental or guardian permission. You must register using your own legal identity and verify your account with your own government-issued ID. Opening or verifying an account using someone else's identity or ID, including a parent's or guardian's, violates this policy even if they give permission. **Example:** If you're 16, you cannot open or verify an account using your parent's ID, even with their permission. ### Financial and risk reviews [#financial-and-risk-reviews] We may assess your financial health and, where applicable, your company's financial health before approving your account or during ongoing reviews. We may request supporting information such as bank statements, evidence of income or source of funds, financial statements, outstanding debts or liabilities, and information about your business activity and ability to cover refunds, disputes, and other payment obligations. These reviews help us assess potential financial exposure. We may request additional information relevant to that assessment and decline, restrict, or suspend service if we cannot verify the information provided or determine that the risk is unacceptable. ### Sign up [#sign-up] Sign up with your email address. Pandabase uses **magic link authentication** โ€” no passwords. Every time you log in, we send a one-time link to your email that expires after 10 minutes. ## Verification requirements [#verification-requirements] Before your store can accept payments, you need to complete these steps: ### Email verification [#email-verification] Confirmed automatically when you sign up via magic link. ### Profile information [#profile-information] Add your first name, last name, and country. Your country determines which payout methods are available. ### Phone verification [#phone-verification] Verify your phone number via SMS code. Your phone number's country must match your account country. ### Identity verification (KYC) [#identity-verification-kyc] Complete identity verification through our KYC provider. This typically involves uploading a government-issued ID and a selfie. Verification usually takes a few minutes. Once all steps are complete, you can create stores and start accepting payments. ## Account settings [#account-settings] Open **Account Settings** from the avatar menu in the dashboard to manage your profile, KYC documents, and security. Account Settings page showing My Profile tab with profile information The page is split into three tabs: * **My Profile** โ€” your legal name, username, email, phone number, country, and account ID. Most fields are read-only since they're tied to your verified identity. Contact support if you need to change anything. * **Compliance** โ€” outstanding requirements, KYC documents on file, and current verification status. * **Security** โ€” 2FA setup, active sessions, and recent login activity. ### Compliance [#compliance] Compliance tab listing phone verification, 2FA, identity verification, and default store status Each requirement shows its current state โ€” **Required**, **Complete**, or a direct action button (e.g., **Verify Phone**). Until every item is marked Complete, your account can't receive payouts. ### Security [#security] Security tab with 2FA toggle, active sessions list, and Delete Account in the danger zone The Security tab covers three things: * **Two-Factor Authentication** โ€” enable or disable TOTP-based 2FA. * **Active Sessions** โ€” every device currently signed in to your account. Click **Revoke** on any row to sign that session out immediately, or **Revoke All** to sign out everything except your current session. * **Delete Account** โ€” request account closure. Deletion isn't a self-service action; clicking it shows a dialog directing you to email [support@pandabase.io](mailto:support@pandabase.io) so we can confirm and process the removal manually. ## Two-factor authentication [#two-factor-authentication] Enable **TOTP-based 2FA** with an authenticator app such as Google Authenticator, Authy, or 1Password. When enabled, you'll need to enter a 6-digit code from your authenticator app each time you log in. We strongly recommend enabling 2FA on all accounts, especially those with active stores and balances. ## Sessions [#sessions] Each login creates a session tied to your device and IP address. You can view all active sessions and revoke any session from the dashboard. Sessions track: * Device type and operating system * Browser * IP address * Last active time ## Account limits [#account-limits] | Limit | Value | | ------------------ | --------- | | Stores per account | 10 | | Team memberships | Unlimited | ## Supported countries [#supported-countries] Pandabase is available to merchants in the following countries: **Americas**: Canada, United States **Europe**: Austria, Belgium, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Great Britain, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden, Switzerland, Turkiye **Asia-Pacific**: Australia, Hong Kong, India, New Zealand, Philippines, Singapore, Taiwan, Thailand **Middle East**: Israel, Qatar, United Arab Emirates **Africa**: Morocco # Create checkout session (/developers/api/billing-api-reference/checkouts/createcheckoutsession) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Estimate checkout (/developers/api/billing-api-reference/checkouts/estimatecheckout) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Introduction (/developers/api/introduction) Use the Pandabase HTTP API to manage your store from any language that can send HTTP requests. See [API tokens](/developers/learn/api-tokens) for authentication. ## API Basics [#api-basics] | Environment | URL | Protocols | | ----------- | -------------------------- | ------------------- | | Production | `https://api.pandabase.io` | HTTP/1.1-2 over TLS | ## Additional Information [#additional-information] * All endpoints are accessible under the specified URLs. * The API follows the REST architecture principles. * Both production and sandbox environments support HTTP/1.1 and HTTP/2 protocols. * All communications are secured using TLS (Transport Layer Security). ### Domains and IP addresses [#domains-and-ip-addresses] Only `HTTPS` requests are allowed, any requests made with `HTTP` will be redirected. ### Content Type [#content-type] All requests must be encoded as JSON with the Content-Type: `application/json` header. If not otherwise specified, responses from the Pandabase API, including errors, are encoded exclusively as JSON as well. ### Versioning [#versioning] The API uses major versions for breaking changes and dated updates for backward-compatible changes. **How It Works** 1. **Major Versions**: Released when there are breaking changes that require code updates * Breaking changes include endpoint restructuring, renamed fields, or removed functionality * Each major version is maintained separately to give you time to migrate * Current version: **V2** (released March 7, 2026) 2. **Date-Based Increments** (YYYY/MM/DD): Added to major versions for non-breaking updates * Non-breaking changes include new features, additional fields, bug fixes, and performance improvements * Format: `YYYY/MM/DD-v2` (e.g., `2026/03/09-v2`) * These updates are backward compatible within the same major version **Version Header** The `X-Version` response header identifies the API version used for the request. ### Request IDs [#request-ids] Every API response includes the following headers: | Header | Description | | -------------- | ---------------------------------------------------------- | | `X-Request-ID` | Unique request identifier for tracking and troubleshooting | | `X-Version` | Current API version in `YYYY/MM/DD-v2` format | Include the `X-Request-ID` when contacting support so we can find the request. # Pagination (/developers/api/pagination) All list endpoints use page-based pagination with consistent request parameters and response structure. ## Request Parameters [#request-parameters] Include these query parameters on any list endpoint: ## Response Structure [#response-structure] List endpoints return an object with `items` and `pagination`: ```json { "ok": true, "data": { "items": [], "pagination": { "page": 1, "limit": 25, "total": 142, "totalPages": 6 } } } ``` ## Pagination Object [#pagination-object] ## Best Practices [#best-practices] 1. Use `page` and `limit` query parameters when making requests to list endpoints. 2. Respect the maximum `limit` of 100 items per page. 3. Check `totalPages` to know when you've reached the last page. 4. An empty `items` array with `total: 0` means no results matched your query. ## Limitations [#limitations] * The maximum allowed `limit` is 100 items per page. * If a `limit` greater than 100 is requested, the API will return a validation error. * Default `limit` varies by resource (20 or 25) but all cap at 100. # Rate limits (/developers/api/rate-limits) ## Overview [#overview] Pandabase limits request rates by IP address, account, and store. ### Rate Limiter Types [#rate-limiter-types] Pandabase uses three types of rate limiters, depending on the endpoint and resource being accessed: 1. **Global IP-based limiter** (`GlobalResource`): Applies to all requests from a specific IP address 2. **Account-based limiter** (`AccountResource`): Applies to requests authenticated with your API key 3. **Store-based limiter** (`StoreResource`): Applies to requests specific to a storefront/shop ### How It Works [#how-it-works] Account limits are generally stricter and change more often than IP limits. Rate limits can change. Implement retry logic and respect the rate limit headers in API responses. ## Rate Limit Headers [#rate-limit-headers] All API responses include the following headers to help you manage your request rate: | Header | Description | | ----------------------- | ------------------------------------------------------------------------------------------- | | `X-RateLimit-Limit` | The maximum number of requests permitted within the current rate limit window. | | `X-RateLimit-Remaining` | The number of requests remaining in the current rate limit window. | | `X-RateLimit-Reset` | The time at which the current rate limit window resets (UTC epoch seconds). | | `X-RateLimit-Type` | The type of rate limiter applied (`GlobalResource`, `AccountResource`, or `StoreResource`). | ## Handling Rate Limits [#handling-rate-limits] When you exceed the rate limit, the API returns a `429 Too Many Requests` response: ```json { "ok": false, "error": "Rate limit exceeded. Please retry after the reset time." } ``` ### Best Practices [#best-practices] 1. **Monitor headers**: Check `X-RateLimit-Remaining` to track available requests 2. **Implement exponential backoff**: When you receive a `429` response, wait before retrying 3. **Respect reset times**: Use `X-RateLimit-Reset` to determine when to retry 4. **Cache responses**: Reduce API calls by caching data when appropriate 5. **Batch requests**: Where possible, combine multiple operations into single requests ### Example Retry Logic [#example-retry-logic] ```javascript async function makeRequestWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); const data = await response.json(); if (response.status === 429 && !data.ok) { const resetTime = response.headers.get("X-RateLimit-Reset"); const waitTime = resetTime * 1000 - Date.now(); if (i < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, waitTime)); continue; } } return { response, data }; } } // Usage const { response, data } = await makeRequestWithRetry( "https://api.pandabase.io/stores/[storeId]", { method: "GET", headers: { Authorization: "Bearer sk", "Content-Type": "application/json", }, }, ); if (data.ok) { console.log("Success:", data.data); } else { console.error("Error:", data.error); } ``` ## Rate Limit Variations [#rate-limit-variations] Limits vary by endpoint and plan: * Endpoints have different limits based on the resources they use * Limits can change over time * Higher-tier plans may have increased rate limits Always rely on the headers returned in each API response to determine your current rate limit status rather than hardcoding values. # Response Format (/developers/api/responses) ## Response Structure [#response-structure] API responses include an `ok` field that indicates whether the request succeeded. ### Success Response [#success-response] When a request succeeds, the API returns `ok: true` along with the requested data: ```json { "ok": true, "data": { // Your requested data here } } ``` **Response Fields:** ### Error Response [#error-response] When a request fails, the API returns `ok: false` along with error details: ```json { "ok": false, "error": "Human-readable error message" } ``` **Response Fields:** Error messages are designed to not contain sensitive information and can be safely displayed in user interfaces. ## HTTP Status Codes [#http-status-codes] The API uses standard HTTP status codes to indicate the general category of response: | Status Code | Description | | ----------- | -------------------------------------------------------- | | `200` | **OK** - Request succeeded | | `201` | **Created** - Resource created successfully | | `400` | **Bad Request** - Invalid request parameters | | `401` | **Unauthorized** - Missing or invalid authentication | | `403` | **Forbidden** - Authenticated but lacks permission | | `404` | **Not Found** - Resource doesn't exist | | `409` | **Conflict** - Request conflicts with current state | | `422` | **Unprocessable Entity** - Validation error | | `429` | **Too Many Requests** - Rate limit exceeded | | `500` | **Internal Server Error** - Server error | | `503` | **Service Unavailable** - Temporary service interruption | ## Handling Responses [#handling-responses] Always check the `ok` field to determine if a request succeeded: ```javascript const response = await fetch("https://api.pandabase.io/stores/{storeId}", { method: "GET", headers: { Authorization: "Bearer sk_xxx", "Content-Type": "application/json", }, }); const result = await response.json(); if (result.ok) { // Success - use result.data console.log("data:", result.data); } else { // Error - handle result.error console.error(`error ${result.error.code}: ${result.error.message}`); } ``` Include the `X-Request-Id` response header when contacting support so we can find the request. # Get gross volume (/developers/api/store-api-reference/analytics/get-stores-storeid-analytics-gross-volume) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get orders by country (/developers/api/store-api-reference/analytics/get-stores-storeid-analytics-orders-by-country) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get analytics overview (/developers/api/store-api-reference/analytics/get-stores-storeid-analytics-overview) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List case messages (/developers/api/store-api-reference/cases/get-stores-storeid-cases-caseid-messages) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get case (/developers/api/store-api-reference/cases/get-stores-storeid-cases-caseid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List cases (/developers/api/store-api-reference/cases/get-stores-storeid-cases) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update case (/developers/api/store-api-reference/cases/patch-stores-storeid-cases-caseid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Send case message (/developers/api/store-api-reference/cases/post-stores-storeid-cases-caseid-messages) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Mark case as read (/developers/api/store-api-reference/cases/post-stores-storeid-cases-caseid-read) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete category (/developers/api/store-api-reference/categories/delete-stores-storeid-categories-categoryid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get category (/developers/api/store-api-reference/categories/get-stores-storeid-categories-categoryid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List categories (/developers/api/store-api-reference/categories/get-stores-storeid-categories) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update category (/developers/api/store-api-reference/categories/patch-stores-storeid-categories-categoryid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create category (/developers/api/store-api-reference/categories/post-stores-storeid-categories) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete coupon (/developers/api/store-api-reference/coupons/delete-stores-storeid-coupons-couponid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get coupon (/developers/api/store-api-reference/coupons/get-stores-storeid-coupons-couponid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List coupons (/developers/api/store-api-reference/coupons/get-stores-storeid-coupons) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update coupon (/developers/api/store-api-reference/coupons/patch-stores-storeid-coupons-couponid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create coupon (/developers/api/store-api-reference/coupons/post-stores-storeid-coupons) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get customer (/developers/api/store-api-reference/customers/get-stores-storeid-customers-customerid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List customers (/developers/api/store-api-reference/customers/get-stores-storeid-customers) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get dispute (/developers/api/store-api-reference/disputes/get-stores-storeid-disputes-disputeid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List disputes (/developers/api/store-api-reference/disputes/get-stores-storeid-disputes) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Submit dispute evidence (/developers/api/store-api-reference/disputes/post-stores-storeid-disputes-disputeid-evidence) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get license (/developers/api/store-api-reference/licenses/get-stores-storeid-licenses-licenseid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List licenses (/developers/api/store-api-reference/licenses/get-stores-storeid-licenses) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Verify license (/developers/api/store-api-reference/licenses/post-stores-storeid-licenses-verify) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete meter (/developers/api/store-api-reference/meters/delete-stores-storeid-products-productid-meters-meterid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get meter (/developers/api/store-api-reference/meters/get-stores-storeid-products-productid-meters-meterid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List meters (/developers/api/store-api-reference/meters/get-stores-storeid-products-productid-meters) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update meter (/developers/api/store-api-reference/meters/patch-stores-storeid-products-productid-meters-meterid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create meter (/developers/api/store-api-reference/meters/post-stores-storeid-products-productid-meters) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List order refunds (/developers/api/store-api-reference/orders/get-stores-storeid-orders-orderid-refunds) {/* 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 (/developers/api/store-api-reference/orders/get-stores-storeid-orders-orderid) {/* 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 (/developers/api/store-api-reference/orders/get-stores-storeid-orders) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retry order fulfillment (/developers/api/store-api-reference/orders/post-stores-storeid-orders-orderid-retry-fulfillment) {/* 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 (/developers/api/store-api-reference/payments/get-stores-storeid-orders-payments-paymentid) {/* 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 (/developers/api/store-api-reference/payments/get-stores-storeid-orders-payments) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List bank accounts (/developers/api/store-api-reference/payouts/get-stores-storeid-payouts-bank-accounts) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List balance ledger (/developers/api/store-api-reference/payouts/get-stores-storeid-payouts-ledger) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get payout (/developers/api/store-api-reference/payouts/get-stores-storeid-payouts-payoutid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List payouts (/developers/api/store-api-reference/payouts/get-stores-storeid-payouts) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete product (/developers/api/store-api-reference/products/delete-stores-storeid-products-productid) {/* 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 (/developers/api/store-api-reference/products/get-stores-storeid-products-productid) {/* 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 (/developers/api/store-api-reference/products/get-stores-storeid-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 product (/developers/api/store-api-reference/products/patch-stores-storeid-products-productid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create product (/developers/api/store-api-reference/products/post-stores-storeid-products) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get refund (/developers/api/store-api-reference/refunds/get-stores-storeid-refunds-refundid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List refunds (/developers/api/store-api-reference/refunds/get-stores-storeid-refunds) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create refund (/developers/api/store-api-reference/refunds/post-stores-storeid-refunds-orders-orderid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get download URL (/developers/api/store-api-reference/reports/get-stores-storeid-reports-reportid-download-format) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get report (/developers/api/store-api-reference/reports/get-stores-storeid-reports-reportid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List reports (/developers/api/store-api-reference/reports/get-stores-storeid-reports) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create report (/developers/api/store-api-reference/reports/post-stores-storeid-reports) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get store (/developers/api/store-api-reference/store/get-stores-storeid) {/* 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 invoice (/developers/api/store-api-reference/subscriptions/get-stores-storeid-subscriptions-subscriptionid-upcoming-invoice) {/* 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 usage (/developers/api/store-api-reference/subscriptions/get-stores-storeid-subscriptions-subscriptionid-usage) {/* 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 (/developers/api/store-api-reference/subscriptions/get-stores-storeid-subscriptions-subscriptionid) {/* 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 (/developers/api/store-api-reference/subscriptions/get-stores-storeid-subscriptions) {/* 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 (/developers/api/store-api-reference/subscriptions/post-stores-storeid-subscriptions-subscriptionid-cancel) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Change subscription plan (/developers/api/store-api-reference/subscriptions/post-stores-storeid-subscriptions-subscriptionid-change) {/* 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 (/developers/api/store-api-reference/subscriptions/post-stores-storeid-subscriptions-subscriptionid-pause) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Reactivate subscription (/developers/api/store-api-reference/subscriptions/post-stores-storeid-subscriptions-subscriptionid-reactivate) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Resume subscription (/developers/api/store-api-reference/subscriptions/post-stores-storeid-subscriptions-subscriptionid-resume) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List usage events (/developers/api/store-api-reference/usage/get-stores-storeid-usage) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Report usage events (/developers/api/store-api-reference/usage/post-stores-storeid-usage) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete webhook (/developers/api/store-api-reference/webhooks/delete-stores-storeid-webhooks-webhookid) {/* 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 (/developers/api/store-api-reference/webhooks/get-stores-storeid-webhooks-webhookid) {/* 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 (/developers/api/store-api-reference/webhooks/get-stores-storeid-webhooks) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update webhook (/developers/api/store-api-reference/webhooks/patch-stores-storeid-webhooks-webhookid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create webhook (/developers/api/store-api-reference/webhooks/post-stores-storeid-webhooks) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get category (/developers/api/storefront-api-reference/categories/getstorefrontcategory) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List categories (/developers/api/storefront-api-reference/categories/liststorefrontcategories) {/* 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 (/developers/api/storefront-api-reference/products/getstorefrontproduct) {/* 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 (/developers/api/storefront-api-reference/products/liststorefrontproducts) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get store (/developers/api/storefront-api-reference/store/getstorefront) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Changelog (/developers/changelog) ## 2026-06-07 [#2026-06-07] `Feature` ยท `Fix` ยท `Improvement` **Subscription plan changes, proration & analytics** ### Features [#features] * **Change a subscription's plan**: New endpoint to upgrade, downgrade, or re-price an active subscription โ€” switch the product or variant, change the billing interval, or adjust the quantity (seats). * `POST /v2/stores/:storeId/subscriptions/:subscriptionId/change` * Control how it applies with `proration_behavior` (or it follows the product's billing anchor): **`IMMEDIATELY`** โ€” an upgrade charges a prorated difference for the remainder of the period and resets the billing date, a downgrade applies now; **`END_OF_PERIOD`** โ€” the change is scheduled for the next renewal with no charge now. * Available on the Store API with the `SUBSCRIPTIONS_WRITE` scope. * **Reactivate a subscription**: `POST .../subscriptions/:subscriptionId/reactivate` clears a scheduled cancellation while the subscription is still within its current period. * **Pause & resume over the Store API**: The pause and resume actions are now available programmatically (`SUBSCRIPTIONS_WRITE`), alongside the existing cancel. * **Upcoming invoice preview**: `GET .../subscriptions/:subscriptionId/upcoming-invoice` returns the next charge โ€” base, usage, tax, and total โ€” matching what the renewal will bill. Available on the merchant API, the Store API (`SUBSCRIPTIONS_READ`), and the customer portal. * **Subscription analytics**: New metrics in the dashboard โ€” MRR, ARR, ARPU, active/trialing/past-due counts, churn rate, and trial-conversion rate โ€” plus a monthly new-vs-churned MRR chart. * `GET .../analytics/subscriptions/overview` * `GET .../analytics/subscriptions/mrr` * **New webhook events**: `SUBSCRIPTION_UPDATED` (plan, quantity, or payment-method change), `SUBSCRIPTION_TRIAL_ENDING`, and `SUBSCRIPTION_RENEWING`. Add them to an endpoint's `eventTypes` to subscribe; all are available from the webhook test endpoint. * **Renewal & trial reminders**: `SUBSCRIPTION_TRIAL_ENDING` and `SUBSCRIPTION_RENEWING` webhooks fire a few days ahead, and customers are emailed before a trial ends. Customers are also notified when a plan changes and when a scheduled cancellation is reversed. * **Quantity on subscriptions**: Subscription objects now include a `quantity` field. * **Manage saved cards**: Customers can now add, remove, and set a default card from the portal at mypandabase.com. A billing address is required when adding a card. * **Update billing details**: Edit the cardholder name and billing address on a saved card. * **Change a subscription's card**: Choose which saved card a subscription bills. * **Recover a past-due subscription**: Retry a failed renewal after updating the card โ€” if the bank requires authentication, the customer is guided through it. * **Card notifications**: Customers receive a confirmation when a card is added or removed and when a subscription's card changes, and a reminder before a saved card expires. ### Improvements [#improvements] * **Current-session indicator**: The merchant and customer session lists now flag the session you're currently signed in with and sort it first, so it's clear which one not to revoke. * **Richer merchant sessions**: New merchant sign-ins now record the device, IP address, and approximate location, shown on the security page. Sessions created before this show these details once you sign in again. ### Fixes [#fixes] * Resuming a subscription now returns the correct next charge date in the response. * **Payment method on customer orders**: The customer order detail now shows how the order was paid (for example, `Visa โ€ขโ€ขโ€ขโ€ข 4242`) โ€” both in the API under `payment.method` and on the order page in the portal. * Customers are now returned to the merchantโ€™s `return_url` based on the latest order status, making redirects more reliable and preventing customers from getting stuck on the checkout confirmation page. ## 2026-05-29 [#2026-05-29] `Feature` **Payment method on payments & exports** ### Features [#features-1] * **Payment method on orders and payments**: Order and payment responses now include a `paymentMethod` object describing how the customer paid: `{ type, cardBrand, cardLastFour }`. For card payments, `type` is `"card"` and the brand and last four digits are populated (for example `"visa"` and `"4242"`). For other methods, `type` is the method used, such as `"link"`, `"cashapp"`, `"sepa_debit"`, or `"us_bank_account"`, with the card fields left `null`. Available on order detail, payment detail, and the payments list. The fields are `null` on payments that have not completed yet. * **Payment method in Payment Activity reports**: The Payment Activity export now includes `payment_method`, `card_brand`, and `card_last_four` columns in both CSV and JSON formats. ## 2026-05-25 [#2026-05-25] `Feature` **Order metadata in exports** ### Features [#features-2] * **Order metadata in Payment Activity reports**: The Payment Activity report now includes an `order.metadata` column in both CSV and JSON formats. Appended as the last column so existing column order is preserved. ## 2026-05-24 [#2026-05-24] `Improvement` **Clearer ledger notes** ### Improvements [#improvements-1] * **Friendlier balance ledger notes**: Entry descriptions across payments, refunds, chargebacks, payouts, and subscription renewals have been rewritten for clarity. Existing entries are unchanged; the new wording applies to entries written from this date forward. ## 2026-05-23 [#2026-05-23] `Feature` **Store search endpoint** ### Features [#features-3] * **Search**: New `GET /v2/stores/:storeId/search?q=` resolves any Pandabase resource ID, a customer email, or an order number to a uniform result `{ kind, id, label, sublabel, link, createdAt }`. Accepts up to 10 comma-separated inputs per request and is rate-limited to 30 requests per minute. ## 2026-05-22 [#2026-05-22] `Feature` ยท `Fix` **Reports API, Idempotency Keys & Email References** ### Features [#features-4] * **Reports API**: Generate financial exports asynchronously, delivered as a signed download URL and notification email. * `POST /v2/stores/:storeId/reports` queues a report. * `GET /v2/stores/:storeId/reports` lists reports, filterable by `status` and `type`. * `GET /v2/stores/:storeId/reports/:reportId` returns status and metadata. * `GET /v2/stores/:storeId/reports/:reportId/download/:format` returns a 5-minute pre-signed download URL. * Types: `PAYMENT_ACTIVITY`, `PAYOUT_RECONCILIATION`, `BALANCE_LEDGER`, `REFUNDS_DISPUTES`. Formats: `CSV`, `JSON`. * Period window is capped at 90 days per request. At most 10 reports per store may be `PENDING` or `PROCESSING` concurrently; further requests return `429`. * Statuses: `PENDING โ†’ PROCESSING โ†’ SUCCEEDED | FAILED`. Generated files are retained for 30 days. * Available on the Store API at `/v2/core/stores/:storeId/reports` behind two new token scopes: `REPORTS_READ` and `REPORTS_WRITE`. Existing tokens are unaffected. * New notification preference `notifications.reportReady` (default `true`) controls the ready and failure emails. * **Idempotency keys**: Three endpoints now honor an `Idempotency-Key` request header: * `POST /v2/stores/:storeId/orders/:orderId/refund` * `POST /v2/stores/:storeId/payouts` * `POST /v2/stores/:storeId/checkouts/:sessionId/pay` Keys must be 1 to 255 characters of `[A-Za-z0-9_-]`. A replayed request with the same key and matching body returns the original response with header `Idempotent-Replay: true`. A reused key with a different body returns `422`. A request that arrives while the original is still in flight returns `409`. Stored results live for 24 hours. `5xx` responses are not stored, so the next attempt re-executes. * **Email reference numbers**: Every outbound email now ends with a `Ref: ` line in the footer. Quote this id when contacting support to locate any message. ### Fixes [#fixes-1] * The webhook test endpoint (`POST /v2/stores/:storeId/webhooks/:webhookId/test`) now accepts every `SUBSCRIPTION_*` event type in addition to `PAYMENT_*`. ## 2026-05-21 [#2026-05-21] `Feature` **Usage-based billing** ### Features [#features-5] * **Manage meters from the Store API**: List, create, fetch, update, and delete usage meters on a product without leaving your codebase. Gated by new `METERS_READ` / `METERS_WRITE` token scopes. * **Subscription usage endpoint**: `GET /v2/core/stores/:storeId/subscriptions/:subscriptionId/usage` returns a live projection of the current billing period together with summaries of past periods. Useful for building your own usage and billing reports. Gated by `USAGE_READ`. * **Clearer OpenAPI reference**: Every endpoint description in the Store API has been rewritten for consistency and to match the rest of the docs. See the [usage-based billing guide](/developers/learn/usage-based-billing) for the meter model, event ingestion, and renewal math. ## 2026-05-17 [#2026-05-17] `Feature` **Prevented disputes** ### Features [#features-6] * **`PAYMENT_DISPUTE_PREVENTED`**: New webhook event fired when Verifi (Visa RDR) or Ethoca prevents a chargeback before it becomes a formal dispute. * **$30 prevention fee**: Disputed amount plus the prevention fee is deducted from your balance. Prevented disputes do not count toward your dispute rate. The `Dispute` resource exposes the new `PREVENTED` status, and the dispute-list endpoint accepts it as a filter. You can opt in by adding `PAYMENT_DISPUTE_PREVENTED` to your endpoint's `eventTypes`. See [events](/developers/webhooks/events) and the [disputes guide](/store/manage-payments/disputes). ## 2026-05-15 [#2026-05-15] `Feature` **Standard Webhooks support** ### Features [#features-7] * **Standard Webhooks**: Endpoints now run in **V2** ([Standard Webhooks](https://www.standardwebhooks.com) v1, SDK-verifiable). New endpoints default to V2; **V1 is deprecated** and will be removed after a 60-day notice โ€” migrate now. * **`signatureVersion` field**: Set the mode on `POST` / `PATCH` webhook endpoints; returned on every response. * **Test events match live mode**: Test deliveries send the same headers a real delivery would. * **V2 drops `X-Pandabase-*` headers**; V1 endpoints keep both. * **Retries now span \~75 hours** (9 attempts, 5s โ†’ 24h, ยฑ10% jitter). Was: 5 attempts within \~30s. See the [webhook overview](/developers/webhooks/overview) and [migration guide](/developers/webhooks/migrate-signatures). ## 2026-05-14 [#2026-05-14] `Feature` **Refunds, Disputes, Webhook Signatures & New Payment Methods** ### Features [#features-8] * **Refunds & Disputes in the Store API**: Programmatic endpoints to list refunds, fetch a refund, issue a refund, list disputes, fetch a dispute, and submit dispute evidence. Gated by new `REFUNDS_READ` / `REFUNDS_WRITE` / `DISPUTES_READ` / `DISPUTES_WRITE` token scopes. * **`Webhook-Signature` header on outgoing webhooks**: New, replay-resistant signature alongside the existing `X-Pandabase-Signature`. We recommend migrating verification logic; the legacy header stays for backwards compatibility for now. Verification details and migration guide in the docs. * **Failed-webhook email alerts**: Store owners now get an email when an endpoint stops accepting deliveries, so they can investigate failed deliveries. * **New payment methods**: Bizum (EUR) and Pay by Bank (GBP) are now available at checkout in their respective regions. * **More resilient subscription renewals**: Same renewal can no longer fire twice within a billing cycle, and transient errors (e.g., temporary payment provider error) reschedule a quick retry without counting against the retry budget. ## 2026-05-12 [#2026-05-12] `Fix` **Dynamic Line Items** ### Fixes [#fixes-2] * Dynamic line items in a checkout no longer count against the per-store catalog cap. ## 2026-05-10 [#2026-05-10] `Feature` **Credit/Debit Payouts & Verification** ### Features [#features-9] * **Credit / debit payouts**: New routes for non-bank payout rails. * **Improved bank account verification flow**: New accounts go through a verification window before becoming eligible for payouts. ## 2026-05-07 [#2026-05-07] `Feature` **Case Auto-Reply & Auto-Close** ### Features [#features-10] * **Case auto-reply and auto-close**: Configurable auto-reply on first customer message; idle cases close automatically after a period of no activity. ## 2026-05-06 [#2026-05-06] `Feature` **Tax Details Endpoint** ### Features [#features-11] * **Tax details endpoint**: Returns the tax breakdown that will apply to a checkout given items and billing country. ## 2026-05-05 [#2026-05-05] `Feature` **Monthly Revenue Analytics** ### Features [#features-12] * **Monthly revenue endpoint**: Month-over-month gross/net revenue analytics. ## 2026-05-01 [#2026-05-01] `Feature` **Paylinks & Email Reliability** ### Features [#features-13] * **Paylinks**: Static, shareable checkout URLs. Supports usage caps, expiry, and a mix of catalog products plus dynamic line items. `POST /v2/storefront/paylinks/:paylinkId/checkout` to open a session โ€” no auth needed. * **More reliable transactional emails**: Receipts, refund notices, and KYC results now retry longer before giving up. ## 2026-04-29 [#2026-04-29] `Fix` **Payout Fee Country Fix** ### Fixes [#fixes-3] * **Payout fees now use the bank account's country**: Fixes incorrect cross-border fee estimates for stores whose bank account is in a different country than the store itself. ## 2026-04-28 [#2026-04-28] `Feature` **Shield (Beta)** ### Features [#features-14] * **Shield (beta)**: Rules engine for blocklists / allowlists across emails, IP ranges, BIN ranges, and country codes. Auto-decline matching payments at checkout. ## 2026-04-25 [#2026-04-25] `Feature` **Cases API & Live Payout Fees** ### Features [#features-15] * **Cases API for merchants**: Programmatic access to support tickets โ€” list, get, reply, mark read โ€” over the Store API and via the MCP server. * **Payout fees are now quoted live**: Replaces the previous static fee table. Quote breakdown (`standard_payout_fee`, `cross_border_payout_fee`, `foreign_exchange_fee`) is exposed on payout request responses. ## 2026-04-19 [#2026-04-19] `Feature` **Support Cases** ### Features [#features-16] * **Support cases**: Customers can now open support tickets on their completed orders directly from the Pandabase portal. Merchants get real-time email notifications and can reply via the Store API or the customer portal. * **Pandabase support intervention**: When a case needs platform oversight, our team can join the conversation. Staff replies are visible to both parties and clearly branded as Pandabase Support โ€” no back-channels. * **Attachments**: Up to 5 files per message, allowlist (`image/*`, `application/pdf`, `text/plain`, `application/zip`). All URLs are short-lived. * **Customer emails now route through the portal**: Payment, order, subscription, and refund emails to customers no longer link to external support addresses โ€” they direct users to sign in at `mypandabase.com` and open a ticket against the relevant order. ## 2026-04-19 [#2026-04-19-1] `Feature` **5 New Supported Countries** ### Features [#features-17] * **5 new merchant countries**: Pandabase is now available to merchants in Israel, Romania, Hong Kong, Philippines, and Morocco โ€” bringing total coverage to **43 countries**. ## 2026-04-18 [#2026-04-18] `Feature` **Payment Methods in Checkout Estimate** ### Features [#features-18] * **`available_payment_methods` in checkout estimate**: The `POST /v2/stores/:storeId/checkouts/estimate` endpoint now returns a list of supported payment methods based on the customer's country, alongside the existing presentment currency resolution. Global methods (Card, Apple Pay, Google Pay) are always included, with country-specific methods added on top: * **US**: Cash App, Amazon Pay, WeChat Pay, Alipay * **UK**: Cash App, Amazon Pay (plus EU methods) * **EU/EEA** (Eurozone, Czechia, Denmark, Hungary, Poland, Sweden, Norway, Switzerland): iDEAL/Wero, Bancontact, Multibanco, MB WAY, EPS, Przelewy24, BLIK, Amazon Pay * **Brazil**: PIX * **India**: UPI * **South Korea**: Naver Pay, Kakao Pay, PAYCO, Samsung Pay ## 2026-04-03 [#2026-04-03] `Feature` **New Supported Countries** ### Features [#features-19] * **9 new supported countries**: Pandabase is now available to merchants in Australia, New Zealand, India, United Arab Emirates, Qatar, Taiwan, Vietnam, Thailand, and Switzerland. Local bank transfers are supported for Australia (BECS), New Zealand, India (NEFT/IMPS), Thailand, and Switzerland (SEPA). Wire (SWIFT) payouts are available for Thailand, United Arab Emirates, Qatar, and Taiwan. ## 2026-03-29 [#2026-03-29] `Feature` ยท `Fix` **Inclusive Tax, Coupon Restrictions & Async Payment Support** ### Features [#features-20] * **Inclusive tax support**: Stores can now set their tax behavior to `INCLUSIVE` via store settings. When enabled, product prices are treated as tax-inclusive and the tax portion is back-calculated for invoices and accounting. Configure via `PATCH /v2/stores/:storeId` with `taxBehavior: "INCLUSIVE"`. * **Async payment method support**: Payments via ACH, SEPA, and other bank transfer methods now show a `PROCESSING` status while the payment clears. Customers receive an email confirming their payment is in progress. * **Coupon customer restrictions enforced**: `firstPurchaseOnly` and `maxUsesPerCustomer` coupon settings are now validated at checkout. Previously these were stored but not enforced. * **Max coupons per store**: Increased from 100 to 1,000. ### Fixes [#fixes-4] * Checkout now rejects email addresses in the customer name field. ## 2026-03-26 [#2026-03-26] `Feature` **Product-Scoped Coupons** ### Features [#features-21] * **Product-scoped coupons**: Coupons can now be restricted to specific products. When a coupon has products assigned, the discount only applies to those items in the cart. Coupons without product scoping continue to apply to the entire cart. * Set `productIds` when creating or updating a coupon to scope it to specific products. * Pass `null` or an empty array to remove the product scope. * Available across all coupon endpoints, including the Store API. ## 2026-03-20 [#2026-03-20] `Feature` **Subscriptions & Recurring Billing (Beta)** ### Features [#features-22] * **Recurring billing**: Merchants can now sell subscription products with automatic recurring charges. Supports weekly, monthly, and yearly billing intervals. * **Free trials**: Subscription products can offer a free trial period. Customers save their card during checkout and are charged automatically when the trial ends. * **Subscription management**: New endpoints to list, view, cancel, pause, and resume subscriptions. Available via the Store API and the customer portal. * `GET /v2/stores/:storeId/subscriptions` * `GET /v2/stores/:storeId/subscriptions/:subscriptionId` * `POST /v2/stores/:storeId/subscriptions/:subscriptionId/cancel` * `POST /v2/stores/:storeId/subscriptions/:subscriptionId/pause` * `POST /v2/stores/:storeId/subscriptions/:subscriptionId/resume` * **Customer portal subscriptions**: Customers can view and cancel their subscriptions across all stores from the customer portal at mypandabase.com. * **Subscription invoices**: Customers receive a PDF invoice via email on every successful renewal charge. * **3D Secure on renewals**: When a renewal payment requires additional authentication, customers receive an email with a secure link to verify the payment. * **Subscription webhooks**: Six new webhook events โ€” `SUBSCRIPTION_CREATED`, `SUBSCRIPTION_RENEWED`, `SUBSCRIPTION_PAST_DUE`, `SUBSCRIPTION_CANCELLED`, `SUBSCRIPTION_PAUSED`, `SUBSCRIPTION_RESUMED`. * **Store API support**: Subscription endpoints available via token-authenticated Store API with `SUBSCRIPTIONS_READ` and `SUBSCRIPTIONS_WRITE` permissions. ## 2026-03-16 [#2026-03-16] `Feature` **Checkout Customization & Store Onboarding** ### Features [#features-23] * **Checkout title & description**: Customize the heading and description shown on the checkout page. Title defaults to your store name. Description is only shown when explicitly set. * **Store questionnaire**: New onboarding questionnaire for stores. Required before accepting payments โ€” collects business type, description, country, and expected volume. Companies and nonprofits also provide legal entity name, tax ID, and registered address. * **Localized payment toggle**: Merchants can now enable or disable local currency payments per store via `PATCH /v2/stores/:storeId`. * **Improved payout error messages**: Payout requests now show a detailed breakdown when balance is insufficient, including the total needed and fee breakdown. * **Receipt support email**: Order receipt emails now show the merchant's support email instead of a generic address. * **Max stores increased**: Accounts can now create up to 10 stores (previously 3). ## 2026-03-12 [#2026-03-12] `Feature` **Local Currency Payments & Balance Ledger** ### Features [#features-24] * **Local currency payments**: Customers are now charged in their local currency when checking out from a supported country. This enables payment methods like PIX, iDEAL, SEPA Debit, Bancontact, Naver Pay, Kakao Pay, and more. Settlements remain in USD. * **Balance ledger**: New `GET /v2/stores/:storeId/payouts/ledger` endpoint for a full audit trail of balance changes โ€” payments, refunds, disputes, and payouts. * **Presentment data on orders**: Order and payment responses now include `presentmentCurrency`, `presentmentAmount`, and `exchangeRate` when a local currency was used. ## 2026-03-11 [#2026-03-11] `Feature` ยท `Fix` **Payouts Expansion & New Countries** ### Features [#features-25] * **New payout countries**: Added Canada, Singapore, Turkiye, Hungary, Sweden, Denmark, and Norway. * **SWIFT transfers**: Merchants in countries without local rail support can now receive payouts via SWIFT. * **Country-specific bank accounts**: Sort codes (GB), transit/institution numbers (CA), bank/branch codes (SG), SWIFT/BIC codes (TR), and IBAN (EU/EEA). * **Bank account verification**: UK bank accounts are now verified against the bank's records before being accepted. * **Payout emails**: Merchants now receive an email when a payout is requested with estimated processing timeline. ### Fixes [#fixes-5] * Platform fee adjusted from 6% to 5.9% per transaction. ## 2026-03-10 [#2026-03-10] `Feature` ยท `Fix` **Checkouts, Storefront, Fraud Protection & Teams** ### Features [#features-26] * **Early fraud protection**: Payments flagged with early fraud warnings are now automatically refunded for low-value orders or flagged for manual review. Merchants are notified via email. * **Dispute status tracking**: Disputes now transition to `UNDER_REVIEW` when evidence is submitted to the card network. * **Team notifications**: All team actions (add, remove, role update) now send email notifications to both the affected member and the store owner. * **Role update notifications**: Changing a team member's role now sends a notification showing the previous and new role with permission details. * **Improved order timeline**: Added fulfillment started and payment failed events to the order timeline. * **Partial customer prefill**: Create checkout sessions with just `name` and `email` โ€” billing address can be added later via `PATCH`. Tax is calculated once billing is provided. * **Lower item minimums**: Dynamic checkout items can now be as low as $0.01 per unit. The total checkout amount must still be at least $1.00. * **Storefront store resolution**: New `GET /v2/storefront/resolve/:handle` endpoint to look up a store by its handle โ€” useful for subdomain-based routing. * **Storefront product by handle**: New `GET /v2/storefront/stores/:storeId/products/by-handle/:handle` endpoint to fetch a product by its URL-friendly handle. ### Fixes [#fixes-6] * Fixed refund response returning incorrect amount. * Fixed dispute won incorrectly restoring the non-refundable dispute fee to merchant balance. * Fixed coupon usage not rolling back when refunds are initiated externally. * Fixed refund confirmation emails not being sent on certain refund paths. * Total minimum is now enforced unconditionally on all checkout endpoints (estimate, create, update). Previously, the check was skipped when no billing address was provided. * Fixed storefront routes returning `500` errors instead of proper JSON error responses. * Fixed storefront resolve endpoint failing when `storeId` was not present in the URL. ## 2026-03-09 [#2026-03-09] `Feature` ยท `Fix` **Metadata & Docs** ### Features [#features-27] * **Checkout metadata**: Attach arbitrary key-value string pairs (`metadata`) when creating checkout sessions. Metadata flows through orders, webhook payloads, and fulfillment webhooks. Max 20 keys, key max 40 chars, value max 500 chars. * **Product soft delete**: Products are now soft-deleted via `deleted_at` field. Soft-deleted products are excluded from storefront listings, checkout item resolution, and all product queries. Order history is preserved. ### Fixes [#fixes-7] * Fixed deleted products still appearing in checkout item resolution. * Added GB (United Kingdom) as a supported merchant country. ## 2026-03-07 [#2026-03-07] `Feature` ยท `Deprecation` **V2 API Launch** ### V2 API [#v2-api] The V2 API is a complete rewrite of the Pandabase API with breaking changes. You cannot migrate from V1 endpoints to V2 directly. **Key changes from V1:** * New response format: `{ ok: true, data }` / `{ ok: false, error }` * New pagination format: `{ items, pagination: { page, limit, total, totalPages } }` * Stricter request validation with better error messages * Magic link authentication (no passwords) * All monetary values in cents (integers, no floats) #### New Modules [#new-modules] * **Checkouts**: Stateful checkout sessions with custom fields (text, numeric, dropdown), coupon/tax support, and pay-what-you-want pricing * **Storefront**: Public read-only API for store info, products (with variants/options), and categories โ€” no authentication required * **Store API**: Token-authenticated programmatic API with granular permissions (Bearer or HMAC authentication) * **Licenses**: Encrypted license key management with verification endpoint and activation tracking * **API Logs**: Request audit logging with 30-day retention for Store API requests * **Customers**: Read-only customer management (customers created automatically during checkout) #### New Webhook Events [#new-webhook-events] Webhook event names are now uppercase enum values: | V1 Event | V2 Event | | ------------------ | ---------------------- | | `payment.pending` | `PAYMENT_PENDING` | | `payment.success` | `PAYMENT_COMPLETED` | | `payment.failed` | `PAYMENT_FAILED` | | `payment.refunded` | `PAYMENT_REFUNDED` | | `payment.disputed` | `PAYMENT_DISPUTED` | | โ€” | `PAYMENT_DISPUTE_WON` | | โ€” | `PAYMENT_DISPUTE_LOST` | #### New Webhook Payload [#new-webhook-payload] V2 webhook payloads include `order`, `customer`, and `geo` data in a structured format. See the [webhook events reference](/developers/webhooks/events) for full details. #### Products [#products] * Variants with options, slug, description, SKU, stock tracking, and positioning * Product options (e.g. Size, Color) with values array * Fulfillment modes: `MANAGED_LICENSE`, `LICENSE_POOL`, `LICENSE_WEBHOOK`, `INSTANT_DOWNLOAD`, `REDIRECT`, `MANUAL` * Pricing models: `STANDARD`, `PAY_WHAT_YOU_WANT`, `FREE` * Product status: `DRAFT` / `ACTIVE` * Availability windows (`available_from`, `available_until`) * Max per customer limits * Unique handles per store (auto-generated from title) #### Orders [#orders] * Timeline events for full payment/refund/dispute/fulfillment audit trail * Dispute evidence submission with auto-filled MoR data * Fulfillment retry for failed webhook deliveries * Order receipts with license key delivery via email #### Payouts [#payouts] * Bank account management via global payouts * Payout requests with minimum $5 threshold * Safe concurrent payout processing #### Teams [#teams] * Role-based access control: `VIEWER`, `MODERATOR`, `DEVELOPER`, `ADMINSTRATOR` * Team member CRUD (admin only) #### Authentication [#authentication] * Magic link auth (passwordless) with 10-minute TTL * Session management with IP binding, device/browser/OS tracking * TOTP-based 2FA * Phone verification via Twilio * KYC verification via SumSub #### Supported Countries [#supported-countries] Merchants can now onboard from: US, GB, AT, BE, CY, CZ, DE, EE, ES, FI, FR, GR, HR, IE, IT, LT, LU, LV, MT, NL, PL, PT, SI, SK. ### Deprecations [#deprecations] * V1 API endpoints are deprecated and will be removed on **April 1st, 2026** * V1 webhook event format (`payment.success`, `payment.paid`, etc.) replaced by V2 uppercase events * V1 `Transaction` model replaced by `InboundPayment` in V2 * V1 `Recipient` and `Payout` models replaced by `OutboundPayoutMethod` and `OutboundPayout` ## 2024-11-30 [#2024-11-30] `Feature` ยท `Fix` **Features and Fixes** ### Features [#features-28] * Added support for applying coupons during checkout. * Introduced support for additional currencies (EUR, GBP). * Added guest and authenticated customer management. * Improved session management and user lifecycle handling. ### Fixes [#fixes-8] * Resolved issues with file validation during uploads. * Improved category and slug validations. * Fixed bugs in session retrieval and pagination. ## 2024-10-22 [#2024-10-22] `Feature` **API Schema Updates** ### New Properties [#new-properties] * `tax_calculation_enabled`, `localized_pricing_enabled`, `regional_pricing_enabled` on shop flags * `checkout_collect_phone_enabled`, `checkout_verify_email_enabled` on shop flags * `images[]` and `categories[]` on product objects * `icon_url` on shop objects ### Pagination [#pagination] List responses now include a `meta` object with `page_size` (default 10) and `page` (default 1). ### Filtering and Sorting [#filtering-and-sorting] Added optional `filter`, `sort`, and `search` query parameters to list endpoints. ## 2024-09-20 [#2024-09-20] `Feature` **Major Features Release** ### New Features [#new-features] * **MoR Model**: Pandabase as Merchant of Record. * **Advanced Analytics**: Store analytics endpoints. * **Dynamic Payments**: Custom fields during checkout. * **Idempotency**: Support for `POST`, `PUT`, and `PATCH` requests. * **File API**: Intent-based file uploads at `/files`. ## 2024-08-30 [#2024-08-30] `Feature` **Public Endpoints** * Added `/public` endpoints with filtering support. * Added `/onboarding` and `/login` endpoints. * Added `flags` to shops. ## 2024-06-06 [#2024-06-06] `Feature` **Categories Support** * Added `/categories` route with full CRUD support. * Added `categories` field to product model. ## 2024-05-13 [#2024-05-13] `Feature` **Payouts & KYC** * Added `payout` and `recipients` routes. * Added KYC verification support. * Added customer auth tokens. ## 2024-04-03 [#2024-04-03] `Feature` **Event Changes** * Renamed `transaction` event types to `order`. * Added `payment.pending`, `payment.paid`, `payment.success`, `payment.failed` event types. ## 2024-03-12 [#2024-03-12] `Feature` **Property Additions** * Added analytics properties to shop objects (`total_sales`, `total_earned`, `pending_balance`, `dispute_rate`). * Added feature flags (`pmp_enabled`, `chargeback_protection_enabled`, `storefront_enabled`, etc.). * Added `fee` and `early_fraud_warning` to transaction objects. * Added `discounted` and `discount_code` to order objects. * Added `reason` and `status` to refund and dispute objects. ## 2024-02-15 [#2024-02-15] `Feature` **Initial Release** * Initial V1 API release with stores, products, orders, coupons, webhooks, and payments. * Sort and filter support for List Orders route. # Quickstart (/developers/connected-accounts/quickstart) You'll need a platform store to create and manage connected accounts. For access and eligibility, contact support at [support@pandabase.io](mailto:support@pandabase.io). This feature is intended for platforms processing payments on behalf of other users. ## Integration overview [#integration-overview] If you already have a platform store, you can start integrating using the Platform API. All connected account operations are handled through a single endpoint: ``` https://api.pandabase.io/v3/stores/[store_id]/connected-accounts ``` ## Creating a connected account [#creating-a-connected-account] Use this endpoint to create a connected account under your platform. Before you start: * The user must not already have a Pandabase account * You can request supported capabilities during creation (for example: payments and payouts) ### Request [#request] **Endpoint:** ``` POST https://api.pandabase.io/v3/stores/[store_id]/connected-accounts ``` **Example cURL:** ```sh curl -X POST 'https://api.pandabase.io/v3/stores/[store_id]/connected-accounts' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -d '{ "email": "their-email@example.com", "capabilities": ["payments", "payouts"], "metadata": { "partner_id": "optional-partner-id" } }' ``` ### Response [#response] ```json { "ok": true, "data": { "type": "connected_account", "id": "c_acc_123456", "capabilities": [ { "type": "payments", "eligible": true }, { "type": "payouts", "eligible": true } ], "onboarding_link": "https://self.pandabase.io/connected-accounts/onboarding" } } ``` ## Notes [#notes] * The `onboarding_link` should be shared with the user to complete setup. * Capability eligibility may depend on compliance checks. * Keep your API key on your server. Never expose it in client-side code. # API Tokens (/developers/learn/api-tokens) ## Overview [#overview] API tokens authenticate requests to the [Store API](/developers/api/introduction) at `/v2/core/stores/:storeId/*`. Each token has a set of permissions and uses one of two authentication modes: **Bearer** or **HMAC**. Create tokens in your store's dashboard under **Settings โ†’ Developers โ†’ API Tokens**, or via the tokens API. ## Bearer mode [#bearer-mode] Send the token secret directly in the `Authorization` header. The secret starts with `sk_`. ``` Authorization: Bearer sk_live_abc123... ``` ### Example (TypeScript) [#example-typescript] ```typescript const STORE_ID = process.env.PANDABASE_STORE_ID!; const API_TOKEN = process.env.PANDABASE_API_TOKEN!; // sk_live_... // list products const res = await fetch( `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`, { headers: { Authorization: `Bearer ${API_TOKEN}`, }, }, ); const { data } = await res.json(); console.log(data.items); ``` ### Example (cURL) [#example-curl] ```bash curl https://api.pandabase.io/v2/core/stores/shp_xxx/products \ -H "Authorization: Bearer sk_live_abc123..." ``` Bearer mode is simple and works well for server-side integrations where the token is stored securely. ## HMAC mode [#hmac-mode] In HMAC mode, you sign the request body with a key derived from your secret. Send the token ID and signature in the headers; the secret stays on your server. When you create an HMAC token, you receive: * **Token ID** โ€” starts with `stk_`, sent in the `Authorization` header * **Secret** โ€” starts with `sk_`, used to derive the signing key (never sent in requests) ### How signing works [#how-signing-works] The HMAC key is **not** your raw secret. You must first hash your secret with SHA-256 to derive the signing key, then use that key to compute the HMAC-SHA256 signature of the request body. ``` signing_key = SHA-256(your_secret) signature = HMAC-SHA256(signing_key, request_body) ``` Use the derived key to sign the request body. ### Headers [#headers] | Header | Value | | --------------- | ------------------------------------------------------------------------------ | | `Authorization` | `Bearer stk_your_token_id` | | `X-Signature` | HMAC-SHA256 hex digest of the request body, keyed with the derived signing key | For requests with no body (`GET`, `DELETE`), sign an empty string. ### Example (TypeScript) [#example-typescript-1] ```typescript import crypto from "crypto"; const STORE_ID = process.env.PANDABASE_STORE_ID!; const TOKEN_ID = process.env.PANDABASE_TOKEN_ID!; // stk_... const TOKEN_SECRET = process.env.PANDABASE_TOKEN_SECRET!; // sk_... // derive the signing key by hashing the secret const SIGNING_KEY = crypto .createHash("sha256") .update(TOKEN_SECRET) .digest("hex"); function sign(body: string): string { return crypto .createHmac("sha256", SIGNING_KEY) .update(body) .digest("hex"); } // GET request (sign empty string) async function listProducts() { const signature = sign(""); const res = await fetch( `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`, { headers: { Authorization: `Bearer ${TOKEN_ID}`, "X-Signature": signature, }, }, ); return res.json(); } // POST request (sign the JSON body) async function createProduct() { const body = JSON.stringify({ name: "New Product", amount: 2999, type: "ONE_TIME", }); const signature = sign(body); const res = await fetch( `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN_ID}`, "X-Signature": signature, }, body, }, ); return res.json(); } ``` ### Example (Python) [#example-python] ```python import hmac import hashlib import json import requests import os STORE_ID = os.environ["PANDABASE_STORE_ID"] TOKEN_ID = os.environ["PANDABASE_TOKEN_ID"] # stk_... TOKEN_SECRET = os.environ["PANDABASE_TOKEN_SECRET"] # sk_... # derive the signing key by hashing the secret SIGNING_KEY = hashlib.sha256(TOKEN_SECRET.encode()).hexdigest() def sign(body: str) -> str: return hmac.new( SIGNING_KEY.encode(), body.encode(), hashlib.sha256 ).hexdigest() # GET request def list_products(): signature = sign("") res = requests.get( f"https://api.pandabase.io/v2/core/stores/{STORE_ID}/products", headers={ "Authorization": f"Bearer {TOKEN_ID}", "X-Signature": signature, }, ) return res.json() # POST request def create_product(): body = json.dumps({"name": "New Product", "amount": 2999, "type": "ONE_TIME"}) signature = sign(body) res = requests.post( f"https://api.pandabase.io/v2/core/stores/{STORE_ID}/products", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {TOKEN_ID}", "X-Signature": signature, }, data=body, ) return res.json() ``` ### Example (cURL) [#example-curl-1] ```bash # derive signing key: SHA-256 of your secret SIGNING_KEY=$(echo -n "$TOKEN_SECRET" | openssl dgst -sha256 | awk '{print $2}') # GET โ€” sign empty string SIGNATURE=$(echo -n "" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}') curl https://api.pandabase.io/v2/core/stores/shp_xxx/products \ -H "Authorization: Bearer stk_xxx" \ -H "X-Signature: $SIGNATURE" ``` ```bash # POST โ€” sign the JSON body BODY='{"name":"New Product","amount":2999,"type":"ONE_TIME"}' SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}') curl -X POST https://api.pandabase.io/v2/core/stores/shp_xxx/products \ -H "Content-Type: application/json" \ -H "Authorization: Bearer stk_xxx" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ## Bearer vs HMAC [#bearer-vs-hmac] | | Bearer | HMAC | | ----------------------- | --------------------------------- | ------------------------------------------------------------------ | | **How it works** | Secret sent in every request | Secret never leaves your server โ€” only a derived signature is sent | | **Setup** | Simple โ€” one header | More complex โ€” derive signing key + compute signature per request | | **Security** | Relies on TLS | Verifies request integrity + TLS | | **Best for** | Server-side integrations, scripts | High-security environments, multi-party systems | | **Risk if intercepted** | Token can be reused | Signature is only valid for that specific request body | Both modes use constant-time comparison to prevent timing attacks. HMAC signatures are always 64-character hex strings (SHA-256 output). ## Permissions [#permissions] Tokens use granular permissions. Only grant what your integration needs. | Resource | Read | Write | | ------------- | -------------------- | --------------------- | | Products | `PRODUCTS_READ` | `PRODUCTS_WRITE` | | Orders | `ORDERS_READ` | `ORDERS_WRITE` | | Customers | `CUSTOMERS_READ` | `CUSTOMERS_WRITE` | | Categories | `CATEGORIES_READ` | `CATEGORIES_WRITE` | | Coupons | `COUPONS_READ` | `COUPONS_WRITE` | | Webhooks | `WEBHOOKS_READ` | `WEBHOOKS_WRITE` | | Analytics | `ANALYTICS_READ` | โ€” | | Payouts | `PAYOUTS_READ` | `PAYOUTS_WRITE` | | Licenses | `LICENSES_READ` | `LICENSES_WRITE` | | Refunds | `REFUNDS_READ` | `REFUNDS_WRITE` | | Subscriptions | `SUBSCRIPTIONS_READ` | `SUBSCRIPTIONS_WRITE` | | Usage | `USAGE_READ` | `USAGE_WRITE` | | Meters | `METERS_READ` | `METERS_WRITE` | | Reports | `REPORTS_READ` | `REPORTS_WRITE` | | Store | `STORE_READ` | `STORE_WRITE` | If a request requires a permission the token doesn't have, the API returns `403 Forbidden` with the missing permissions listed in the error message. ## Secret rotation [#secret-rotation] Rotate a token's secret at any time via the dashboard or `POST /v2/stores/:storeId/tokens/:tokenId/roll-secret`. The old secret is immediately invalidated โ€” update your integration before rotating. ## Request logging [#request-logging] All Store API requests are automatically logged with 30-day retention. Each log entry captures the method, path, status code, response time, and IP address. View logs in the dashboard or via `GET /v2/stores/:storeId/api-logs`. Treat API tokens like passwords. Never expose them in client-side code, public repositories, or logs. If a token is compromised, rotate or delete it immediately. # Dynamic Payments (/developers/learn/dynamic-payments) ## Custom Fields [#custom-fields] Custom fields let you collect structured data from customers during the checkout process. You can define up to **3 fields** per checkout session, supporting text inputs, numeric inputs, and dropdowns. For example, collect a `discord_username` at checkout, then use its value from the `PAYMENT_COMPLETED` webhook to assign a Discord role. Other examples include collecting server preferences, referral codes, or any data your fulfillment logic needs. ## Metadata [#metadata] Metadata lets you attach arbitrary key-value string pairs to a checkout session. Unlike custom fields (which are filled by the customer), metadata is set by your application when creating the session and is immutable after creation. Metadata is included in each stage: checkout session โ†’ order โ†’ webhook payloads โ†’ fulfillment webhooks. **Constraints:** * Maximum 20 keys per session * Key: max 40 characters * Value: max 500 characters * Keys and values must be strings Use metadata for internal tracking like campaign IDs, referral sources, or any context your backend needs when processing webhooks. ### Get your webhook secret [#get-your-webhook-secret] In your store, create a new webhook. Copy and securely store the webhook secret. You'll also need your Store ID. ```dotenv title=".env" PANDABASE_WEBHOOK_SECRET="wh_sk_xxx" PANDABASE_STORE_ID="shp_xxxx" ``` ### Set up your backend [#set-up-your-backend] Set up an Express server with TypeScript: ```bash npm init npm add express dotenv npm add -D typescript @types/express @types/node ts-node npx tsc --init ``` Create a `src` folder and add an `index.ts` file: ```typescript title="src/index.ts" import express from "express"; import crypto from "crypto"; import dotenv from "dotenv"; dotenv.config(); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); ``` ### Create a checkout session with custom fields and metadata [#create-a-checkout-session-with-custom-fields-and-metadata] When creating a checkout session via the V2 API, pass `display.fields` for custom fields and `metadata` for developer key-value pairs. There are three field types: | Type | Description | Config options | | ---------- | -------------------- | ----------------------------------------------------------- | | `text` | Free-form text input | `default_value`, `minimum_length`, `maximum_length` | | `numeric` | Digits-only input | `default_value`, `minimum_length`, `maximum_length` | | `dropdown` | Select from a list | `default_value`, `options` (array of `label`/`value` pairs) | ```typescript const STORE_ID = process.env.PANDABASE_STORE_ID!; async function createCheckoutSession(userId: string): Promise { const response = await fetch( `https://api.pandabase.io/v2/stores/${STORE_ID}/checkouts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { name: "Premium Plan", amount: 2999, quantity: 1, }, ], display: { fields: [ { key: "discord_username", label: { type: "custom", custom: "Discord Username" }, type: "text", optional: false, text: { minimum_length: 2, maximum_length: 50 }, }, { key: "server_region", label: { type: "custom", custom: "Server Region" }, type: "dropdown", optional: false, dropdown: { default_value: "us_east", options: [ { label: "US East", value: "us_east" }, { label: "US West", value: "us_west" }, { label: "EU West", value: "eu_west" }, ], }, }, ], }, metadata: { user_id: userId, campaign: "spring_2026", source: "website", }, }), }, ); const result = await response.json(); // result.data contains the checkout session with session_id return result.data.session_id; } ``` The response includes a `session_id`. Use it to build the checkout URL or redirect the customer. ### Add a webhook receiver [#add-a-webhook-receiver] Add a route to receive webhooks and verify the signature: ```typescript function validateSignature( req: express.Request, res: express.Response, next: express.NextFunction, ) { const signature = crypto .createHmac("sha256", process.env.PANDABASE_WEBHOOK_SECRET!) .update(JSON.stringify(req.body)) .digest("hex"); if ( crypto.timingSafeEqual( Buffer.from(req.headers["x-pandabase-signature"] as string), Buffer.from(signature), ) ) { next(); } else { res.status(401).send("Invalid signature"); } } ``` ### Handle PAYMENT\_COMPLETED with custom fields and metadata [#handle-payment_completed-with-custom-fields-and-metadata] When a payment succeeds, the `PAYMENT_COMPLETED` webhook fires. The `data.order` object contains `customFields` (customer-submitted values) and `metadata` (your developer-defined key-value pairs). ```typescript app.post("/webhook", validateSignature, (req, res) => { const event = req.body; if (event.event === "PAYMENT_COMPLETED") { const order = event.data.order; // Access metadata (set by your app at session creation) if (order.metadata) { console.log("User ID:", order.metadata.user_id); console.log("Campaign:", order.metadata.campaign); } // Access custom fields (submitted by the customer) if (order.customFields) { const discordUsername = order.customFields.discord_username; if (discordUsername) { grantDiscordRole(discordUsername); } } } res.sendStatus(200); }); function grantDiscordRole(username: string) { console.log(`Granting role to: ${username}`); } ``` ### Send the checkout link [#send-the-checkout-link] Create an endpoint to initiate the payment process: ```typescript app.post("/create-payment", async (req, res) => { try { const { userId } = req.body; const sessionId = await createCheckoutSession(userId); res.json({ sessionId }); } catch (error) { res.status(500).json({ error: "Failed to create payment session" }); } }); ``` ## Custom Field Reference [#custom-field-reference] | Property | Type | Required | Description | | ------------------------ | ------- | -------- | -------------------------------------------------------------- | | `key` | string | Yes | Unique identifier, alphanumeric and underscores, max 200 chars | | `label.type` | string | Yes | Must be `"custom"` | | `label.custom` | string | Yes | Display label shown to customer, max 50 chars | | `type` | string | Yes | `"text"`, `"numeric"`, or `"dropdown"` | | `optional` | boolean | No | Default: `false` | | `text.default_value` | string | No | Default value for text fields | | `text.minimum_length` | integer | No | Minimum character length (min: 0) | | `text.maximum_length` | integer | No | Maximum character length (max: 255) | | `numeric.default_value` | string | No | Default value for numeric fields | | `numeric.minimum_length` | integer | No | Minimum digit length (min: 0) | | `numeric.maximum_length` | integer | No | Maximum digit length (max: 255) | | `dropdown.default_value` | string | No | Pre-selected option value | | `dropdown.options` | array | Yes | 1-200 items, each with `{ label, value }` | ## Metadata vs Custom Fields [#metadata-vs-custom-fields] | | Custom Fields | Metadata | | --------------- | --------------------------------------- | --------------------------------------- | | **Set by** | Customer (at checkout) | Developer (at session creation) | | **Purpose** | Collect user input (Discord name, etc.) | Internal tracking (user IDs, campaigns) | | **Max count** | 3 fields | 20 keys | | **Mutable** | Submitted at payment confirmation | Immutable after session creation | | **In webhooks** | `data.order.customFields` | `data.order.metadata` | | **Types** | text, numeric, dropdown | string key-value pairs only | ## Validation Rules [#validation-rules] The server strictly validates submitted custom field values: * Required fields must have a non-empty value * **Text**: enforces `minimum_length` and `maximum_length` constraints * **Numeric**: value must contain only digits, enforces length constraints * **Dropdown**: value must match one of the defined `options[].value` entries * Duplicate keys are rejected * Unknown keys not defined in the session are rejected * Submitting `custom_fields` when the session has none defined returns an error * Maximum of 3 custom fields per session ## Webhook Headers [#webhook-headers] Every webhook delivery includes these headers: | Header | Description | | ------------------------- | ---------------------------------------------------------------- | | `X-Pandabase-Signature` | HMAC-SHA256 signature of the JSON body using your webhook secret | | `X-Pandabase-Timestamp` | Unix timestamp of the delivery | | `X-Pandabase-Idempotency` | Unique delivery ID for deduplication | Failed deliveries are retried up to 5 times with exponential backoff. # Hosted Checkout (/developers/learn/hosted-checkout) ## Overview [#overview] Every checkout session returns a `checkout_url` that points to Pandabase's hosted checkout page. There are three ways to use it: 1. **Redirect**: send the customer to the checkout URL 2. **SDK embed**: use the Pandabase Checkout SDK to render checkout as a modal, drawer, overlay, or inline element 3. **Direct link**: share the checkout URL directly The checkout page is hosted at `checkout.pandabase.io` with two routes: | Route | Purpose | | ------------------------------------------ | ------------------------------------------ | | `/pay/stores/{storeId}/sids/{sessionId}` | Full-page checkout (used for redirects) | | `/embed/stores/{storeId}/sids/{sessionId}` | Embed-optimized checkout (used by the SDK) | ## Redirect [#redirect] Create a session on your backend and redirect the customer to its `checkout_url`. ```typescript const STORE_ID = process.env.PANDABASE_STORE_ID!; app.post("/buy", async (req, res) => { const response = await fetch( `https://api.pandabase.io/v2/stores/${STORE_ID}/checkouts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [{ product_id: "prd_xxx", quantity: 1 }], return_url: "https://yoursite.com/thank-you", }), }, ); const { data } = await response.json(); res.redirect(data.checkout_url); }); ``` After payment, the customer is redirected to your `return_url`. If no `return_url` is set, they see a confirmation screen on the checkout page. The redirect only means the customer completed the flow. It does not guarantee payment success. Always use [webhooks](/developers/webhooks/overview) to confirm payments on your backend. ## Checkout SDK [#checkout-sdk] SDK v2 is in **public preview**. The API may change before general availability. Add the Checkout SDK to your site with a script tag. It renders checkout in a sandboxed iframe that keeps card data separate from your page. You can customize its colors, fonts, and layout. * **No framework required.** A vanilla ` ``` This exposes a global `Pandabase` with one method, `Pandabase.checkout(options)`, which returns an instance with `open()`, `close()`, and `destroy()`. Always load the SDK from `https://secure.pandabase.io/v2/sdk.js`. **Never** self-host, bundle, vendor, or proxy this script. We push security and PCI-scope updates to the hosted version continuously, so a self-hosted copy will go stale, break the iframe's origin checks, and is unsupported. ### Quick start [#quick-start] Open checkout in a modal, which is the default presentation: ```html ``` ### Presentation modes [#presentation-modes] Set `mode` to choose how checkout appears. Each mode has its own layout. | Mode | Appearance | Open with | | ----------------------------- | ------------------------------------------------------------ | ----------- | | `modal` *(default)* | Compact centered card over a dimmed backdrop | `.open()` | | `drawer` | Panel that slides in from the right, full height | `.open()` | | `overlay` | Full-page blurred backdrop, wide two-column (summary + form) | `.open()` | | `inline` | Mounts directly into an element on your page | auto-mounts | #### Modal, drawer & overlay [#modal-drawer--overlay] Call `open()` to show checkout and `close()` to dismiss it. ```js const checkout = Pandabase.checkout({ storeId: "shp_abc123", sessionId: "cs_xyz789", mode: "overlay", // or "modal" | "drawer" }); checkout.open(); // later: checkout.close(); ``` #### Inline [#inline] Inline mounts immediately into the `container` you provide, so no `open()` is needed. ```html
``` ### Theming [#theming] Pass an `appearance` object to match your brand. All colors accept any CSS color string; `radius` and `fontSizeBase` are numbers (px). The same tokens style both the checkout UI and the payment fields, so everything stays consistent. ```js Pandabase.checkout({ storeId, sessionId, theme: "auto", // "light" | "dark" | "auto" (default) appearance: { accent: "#6d28d9", accentForeground: "#ffffff", background: "#ffffff", foreground: "#0a0a0a", muted: "#f4f4f5", border: "#e5e5e5", summaryBackground: "#f5f3ff", radius: 10, fontFamily: '"Inter", ui-sans-serif, system-ui, sans-serif', summaryPosition: "left", // overlay only: "left" | "right" | "top" // Optional: ship a distinct dark palette, applied when the resolved // scheme is dark (system preference, `theme: "dark"`, or colorScheme). dark: { background: "#0b0b12", foreground: "#e0e7ff", muted: "#17171f", border: "#262633", summaryBackground: "#101019", }, }, }); ``` #### Appearance tokens [#appearance-tokens] | Token | Type | Description | | --------------------- | ---------------------------- | ---------------------------------------------------------------------------------- | | `colorScheme` | `"light" \| "dark"` | Force a scheme regardless of system/`theme`. | | `accent` | color | Primary action color (buttons, focus, selected). | | `accentForeground` | color | Text/icon on top of `accent`. | | `background` | color | Page/surface background. | | `foreground` | color | Primary text. | | `muted` | color | Subtle/secondary surface. | | `mutedForeground` | color | Secondary text. | | `secondary` | color | Secondary button/surface. | | `secondaryForeground` | color | Text on `secondary`. | | `card` | color | Card surface. | | `cardForeground` | color | Text on cards. | | `popover` | color | Dropdown/popover surface. | | `popoverForeground` | color | Text in dropdowns/popovers. | | `ring` | color | Focus-ring color. | | `border` | color | Borders, dividers, input outlines. | | `input` | color | Input field background. | | `danger` | color | Error/danger color. | | `summaryBackground` | color | Order-summary panel background. | | `radius` | number (px) | Corner radius for inputs/buttons/cards. | | `fontFamily` | string | Sans font stack. | | `fontMono` | string | Monospace font stack. | | `fontSizeBase` | number (px) | Base font size for payment fields. | | `summaryPosition` | `"left" \| "right" \| "top"` | Summary placement (overlay only). | | `logo` | string (URL) | Override the merchant logo in the summary. | | `dark` | object | Any of the above (except `colorScheme`/`summaryPosition`), applied in dark scheme. | Scheme precedence: `appearance.colorScheme` โ†’ `theme` option โ†’ system preference. Tokens you omit fall back to the default light/dark theme. ### Handling results & redirects [#handling-results--redirects] For inline, modal, and drawer, handle `payment_success` to send the buyer onward. Card payments that complete in-place do **not** auto-redirect. ```js Pandabase.checkout({ storeId, sessionId, on: { payment_success: (e) => { window.location.href = e.returnUrl ?? "/thank-you"; }, }, }); ``` Behavior depends on the payment type: * **Redirect-based methods** (Cash App, bank debits, some 3-D Secure) send the customer to another site and back; the embed auto-redirects them to your `return_url`. * **In-page card success** fires `payment_success` (with `returnUrl`) and leaves the navigation to you. The SDK never takes over the merchant's page. ### SDK options [#sdk-options] ```js Pandabase.checkout(options) โ†’ instance ``` | Option | Type | Default | Notes | | ------------ | ---------------------------------------------- | --------- | ---------------------------------------------------------- | | `storeId` | string | โ€” | **Required.** | | `sessionId` | string | โ€” | **Required.** | | `mode` | `"modal" \| "drawer" \| "overlay" \| "inline"` | `"modal"` | | | `container` | string \| HTMLElement | โ€” | **Required for `inline`.** CSS selector or element. | | `theme` | `"light" \| "dark" \| "auto"` | `"auto"` | | | `appearance` | object | โ€” | See [Theming](#theming). | | `locale` | string | โ€” | BCP-47 (e.g. `"en"`, `"fr-FR"`); localizes payment fields. | | `on` | object | โ€” | Event handlers, see [Events](#events). | ### SDK methods [#sdk-methods] | Method | Description | | -------------------- | ----------------------------------------------------- | | `checkout.open()` | Open (modal/drawer/overlay). No-op for inline. | | `checkout.close()` | Close (modal/drawer/overlay). No-op for inline. | | `checkout.destroy()` | Remove listeners and all DOM created by the instance. | ### Events [#events] Supply event handlers as `on: { โ€ฆ }`. | Event | Payload | Fires when | | -------------------- | -------------------------- | ------------------------------------------------- | | `loaded` | `{ sessionId }` | The checkout iframe loaded. | | `ready` | โ€” | The checkout UI is mounted. | | `payment_success` | `{ orderId?, returnUrl? }` | Payment confirmed. | | `payment_processing` | โ€” | Async payment is pending. | | `payment_failed` | `{ error? }` | Payment failed. | | `error` | `{ error }` | Session/load error (incl. iframe failed to load). | | `close` | โ€” | The buyer closed the checkout. | ```js Pandabase.checkout({ storeId, sessionId, on: { loaded: (e) => console.log("loaded", e.sessionId), ready: () => console.log("ready"), payment_success: (e) => (window.location.href = e.returnUrl ?? "/done"), payment_processing: () => console.log("processing"), payment_failed: (e) => console.warn("failed:", e.error), error: (e) => console.error(e.error), close: () => console.log("closed"), }, }); ``` Legacy flat callbacks (`onPaymentSuccess`, `onClose`, โ€ฆ) are still accepted for backwards compatibility, but the grouped `on` object is preferred. ### Full example [#full-example] ```html ``` ### React [#react] Load the SDK script once, then use a hook to manage the checkout lifecycle. ```tsx // hooks/use-checkout.ts import { useEffect, useRef, useCallback } from "react"; declare global { interface Window { Pandabase?: { checkout: (opts: PandabaseCheckoutOptions) => PandabaseCheckout; }; } } type CheckoutMode = "modal" | "drawer" | "overlay" | "inline"; interface PandabaseCheckoutOptions { storeId: string; sessionId: string; mode?: CheckoutMode; container?: string | HTMLElement; theme?: "light" | "dark" | "auto"; appearance?: Record; locale?: string; on?: { loaded?: (e: { sessionId: string }) => void; ready?: () => void; payment_success?: (e: { orderId?: string; returnUrl?: string }) => void; payment_processing?: () => void; payment_failed?: (e: { error?: string }) => void; error?: (e: { error: unknown }) => void; close?: () => void; }; } interface PandabaseCheckout { open: () => void; close: () => void; destroy: () => void; } const SDK_URL = "https://secure.pandabase.io/v2/sdk.js"; let sdkLoaded = false; let sdkPromise: Promise | null = null; function loadSdk(): Promise { if (sdkLoaded) return Promise.resolve(); if (sdkPromise) return sdkPromise; sdkPromise = new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = SDK_URL; script.onload = () => { sdkLoaded = true; resolve(); }; script.onerror = () => reject(new Error("Failed to load Pandabase SDK")); document.head.appendChild(script); }); return sdkPromise; } export function useCheckout() { const checkoutRef = useRef(null); const open = useCallback(async (opts: PandabaseCheckoutOptions) => { await loadSdk(); if (checkoutRef.current) { checkoutRef.current.destroy(); } checkoutRef.current = window.Pandabase!.checkout({ mode: "modal", theme: "auto", ...opts, }); checkoutRef.current.open(); }, []); const close = useCallback(() => { checkoutRef.current?.close(); }, []); useEffect(() => { return () => { checkoutRef.current?.destroy(); }; }, []); return { open, close }; } ``` ```tsx // components/buy-button.tsx import { useState } from "react"; import { useCheckout } from "@/hooks/use-checkout"; export function BuyButton() { const [loading, setLoading] = useState(false); const checkout = useCheckout(); async function handleClick() { setLoading(true); try { const res = await fetch("/api/create-checkout", { method: "POST" }); const { storeId, sessionId } = await res.json(); checkout.open({ storeId, sessionId, mode: "overlay", on: { payment_success: (e) => { window.location.href = e.returnUrl ?? "/thank-you"; }, payment_failed: (e) => console.error("failed:", e.error), }, }); } finally { setLoading(false); } } return ( ); } ``` For inline mode in React, pass a ref as the container: ```tsx import { useRef, useEffect } from "react"; import { useCheckout } from "@/hooks/use-checkout"; interface InlineCheckoutProps { storeId: string; sessionId: string; } export function InlineCheckout({ storeId, sessionId }: InlineCheckoutProps) { const containerRef = useRef(null); const checkout = useCheckout(); useEffect(() => { if (!containerRef.current) return; checkout.open({ storeId, sessionId, mode: "inline", container: containerRef.current, on: { payment_success: (e) => console.log("paid:", e.orderId), }, }); }, [storeId, sessionId]); return
; } ``` ## Notes & limits [#notes--limits] * **`summaryPosition`** (`left`/`right`) only applies to the **overlay** mode, which is wide enough for two columns; other modes stack the summary on top. * **Cleanup:** call `destroy()` if you remove the checkout (e.g. on an SPA route change) to detach listeners and DOM. * **Multiple instances:** each `checkout()` call is independent; don't open two modals at once. ## Return URL [#return-url] Set a `return_url` when creating the session to redirect customers back to your site after payment. It applies to the full-page checkout (`/pay/` route) and to redirect-based methods in the embed. For in-page card success, the SDK hands the `returnUrl` to your `payment_success` handler so you control the navigation. ```json { "items": [{ "product_id": "prd_xxx", "quantity": 1 }], "return_url": "https://yoursite.com/thank-you" } ``` # Payment lifecycle (/developers/learn/lifecycle) Order states track a purchase from checkout through fulfillment, refunds, and disputes. Use webhook events to respond to state changes. ## Overview [#overview] ## Order statuses [#order-statuses] | Status | Description | | ------------ | --------------------------------------------- | | `PENDING` | Order created, awaiting payment confirmation. | | `PROCESSING` | Payment received, fulfillment in progress. | | `COMPLETED` | Order fulfilled successfully. | | `CANCELLED` | Order was cancelled before payment confirmed. | | `FAILED` | Payment was declined. | | `REFUNDED` | A refund has been issued against the order. | | `CHARGEBACK` | A dispute is open or the dispute was lost. | ## Payment statuses [#payment-statuses] Every order has exactly one `InboundPayment` record that tracks the payment side of the lifecycle. | Status | Description | | ------------ | --------------------------------------------------------------------------------------------------------- | | `PENDING` | Payment intent created, awaiting customer action. | | `PROCESSING` | Payment is being processed by the network. Common for bank-transfer methods that do not settle instantly. | | `COMPLETED` | Payment successfully collected. | | `FAILED` | Payment was declined or expired. | | `REFUNDED` | The payment has been refunded. | | `DISPUTED` | A dispute is open on the payment. | ## Lifecycle flow [#lifecycle-flow] ### 1. Payment initiated [#1-payment-initiated] When a customer submits the checkout, an `Order` and `InboundPayment` are created with status `PENDING`. A `PAYMENT_PENDING` webhook event fires. ```json { "event": "PAYMENT_PENDING", "id": "evt_abc123", "timestamp": "2026-05-21T12:00:00.000Z", "data": { "order": { "id": "ord_abc123", "orderNumber": "cs_abc123", "status": "PENDING", "amount": 2999, "currency": "USD", "customFields": null, "metadata": null, "items": [/* ... */] }, "customer": { "id": "cus_abc123", "email": "customer@example.com" }, "geo": null } } ``` Geo data (`city`, `region`, `country`) may be `null` on the initial event. It is enriched asynchronously after checkout and is present on later events. ### 2. Payment succeeds [#2-payment-succeeds] Once the payment is confirmed, the `InboundPayment` moves to `COMPLETED` and the `Order` moves to `PROCESSING` while fulfillment runs. When fulfillment finishes, the order moves to `COMPLETED`. A `PAYMENT_COMPLETED` webhook event fires. `PAYMENT_COMPLETED` is the primary event to listen for when fulfilling orders. It fires when the payment is confirmed and includes the full order payload including `customFields` and `metadata`. Geo data is always present on this event. ### 3. Payment fails [#3-payment-fails] If the payment is declined, expires, or the customer abandons checkout, the order moves to `FAILED` (or `CANCELLED` for explicit abandonment), the payment moves to `FAILED`, and a `PAYMENT_FAILED` webhook event fires. If a coupon was applied, its usage count is automatically restored. ### 4. Refund [#4-refund] When a merchant issues a refund, a `Refund` record is created. Refunds settle in one of two ways: | Refund path | Behavior | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Instant | Common case. The refund completes atomically inside the request: balance is decremented, the order moves to `REFUNDED`, the payment moves to `REFUNDED`, and `PAYMENT_REFUNDED` fires immediately. | | Async | For some payment methods the network takes time to confirm. The refund stays in `PROCESSING` until the network confirms it, at which point it transitions to `SUCCESSFUL`, the order moves to `REFUNDED`, the payment moves to `REFUNDED`, and `PAYMENT_REFUNDED` fires. | In both cases, your handler should treat `PAYMENT_REFUNDED` as the signal that the refund is final. ### 5. Dispute [#5-dispute] If a customer opens a chargeback with their bank, a `Dispute` record is created and the order moves to `CHARGEBACK`. The payment moves to `DISPUTED`. The disputed amount plus a $20.00 dispute fee is deducted from the store's available balance. A `PAYMENT_DISPUTED` webhook event fires. Disputes move through several internal states: | Dispute status | Meaning | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `AWAITING_REVIEW` | Dispute opened. Merchant has not yet submitted evidence. | | `UNDER_REVIEW` | Evidence submitted. Card network is reviewing. | | `WON` | Card network ruled in the merchant's favor. Funds restored. | | `LOST` | Card network ruled in the customer's favor. Funds stay deducted. | | `PREVENTED` | The card network prevented the chargeback before it became a formal dispute (Verifi RDR or Ethoca). Treated as a loss with a $30 prevention fee. | When a dispute resolves, one of these events fires: | Outcome | Event | Effect | | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Won | `PAYMENT_DISPUTE_WON` | Disputed amount restored. Order returns to `COMPLETED`, payment to `COMPLETED`. | | Lost | `PAYMENT_DISPUTE_LOST` | Funds stay deducted. Order stays `CHARGEBACK`. | | Prevented | `PAYMENT_DISPUTE_PREVENTED` | Full disputed amount plus a $30 prevention fee is deducted. Order set to `CHARGEBACK`, payment to `DISPUTED`. Does not count against your dispute rate. | ## Webhook events by lifecycle stage [#webhook-events-by-lifecycle-stage] | Stage | Event | Order status after | Payment status after | | ----------------- | --------------------------- | ----------------------------- | -------------------- | | Payment initiated | `PAYMENT_PENDING` | `PENDING` | `PENDING` | | Payment confirmed | `PAYMENT_COMPLETED` | `PROCESSING` then `COMPLETED` | `COMPLETED` | | Payment failed | `PAYMENT_FAILED` | `FAILED` or `CANCELLED` | `FAILED` | | Refund finalized | `PAYMENT_REFUNDED` | `REFUNDED` | `REFUNDED` | | Dispute opened | `PAYMENT_DISPUTED` | `CHARGEBACK` | `DISPUTED` | | Dispute won | `PAYMENT_DISPUTE_WON` | `COMPLETED` | `COMPLETED` | | Dispute lost | `PAYMENT_DISPUTE_LOST` | `CHARGEBACK` | `DISPUTED` | | Dispute prevented | `PAYMENT_DISPUTE_PREVENTED` | `CHARGEBACK` | `DISPUTED` | See [Webhook events](/developers/webhooks/events) for full payload shapes. ## Subscription lifecycle [#subscription-lifecycle] Subscriptions have their own lifecycle on top of the order lifecycle above. Each renewal creates a new `Order` and `InboundPayment` that flows through the states described here, and the subscription itself transitions through `TRIALING`, `ACTIVE`, `PAST_DUE`, `PAUSED`, and `CANCELLED`. See [Subscriptions](/developers/learn/subscriptions) for the full subscription state machine and [Usage-based billing](/developers/learn/usage-based-billing) for the metered-billing variant. # Reports (/developers/learn/reports) ## Overview [#overview] The Reports API generates large financial exports asynchronously. You queue a report, poll for completion, then download a CSV or JSON file from a short-lived pre-signed URL. Use this when you need to: * Pull financial activity into an external system (a data warehouse, an accounting tool, an audit pipeline) * Reconcile payouts against the underlying transactions * Run periodic exports on a cron job for ETL For one-off exports from the dashboard, see the [merchant guide](/store/finances/reports). ## Report types [#report-types] | Type | What it contains | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PAYMENT_ACTIVITY` | Every payment we processed in the period. Order, status, intended and discount and fee and total and settled amounts, presentment currency and rate, customer details. | | `PAYOUT_RECONCILIATION` | Every payout request. Amount, fees, status, PSP reference, risk score, destination bank account. | | `BALANCE_LEDGER` | Every credit and debit on your store: payments in, refunds out, disputes, payouts. Includes the running balance and the tamper-evident hash chain. | | `REFUNDS_DISPUTES` | Refunds and chargebacks combined. One row per event with a `kind` column tagging the section. | ## Authentication [#authentication] Reports endpoints sit on both the dashboard API (session JWT) and the Store API (API tokens). For programmatic access, use a [store API token](/developers/learn/api-tokens) with the right scopes: | Endpoint | Required scope | | ----------------------------------------------------------------------- | --------------- | | `POST /reports` | `REPORTS_WRITE` | | `GET /reports`, `GET /reports/:id`, `GET /reports/:id/download/:format` | `REPORTS_READ` | Create a token in the dashboard under **Settings โ†’ Developers โ†’ API Tokens**, granting only the scopes you need. ## Step 1: Queue a report [#step-1-queue-a-report] ```bash curl -X POST https://api.pandabase.io/v2/core/stores/{storeId}/reports \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "type": "PAYMENT_ACTIVITY", "periodStart": "2026-04-01T00:00:00Z", "periodEnd": "2026-05-01T00:00:00Z", "formats": ["CSV", "JSON"] }' ``` Response: ```json { "ok": true, "data": { "id": "rpt_8h4t6sqzy3x9w5n2k1m0vqbf", "type": "PAYMENT_ACTIVITY", "status": "PENDING", "formats": ["CSV", "JSON"], "periodStart": "2026-04-01T00:00:00.000Z", "periodEnd": "2026-05-01T00:00:00.000Z", "rowCount": null, "sizeBytes": null, "durationMs": null, "errorMessage": null, "requestedByAccountId": null, "requestedByTokenId": "stk_xxx", "startedAt": null, "completedAt": null, "createdAt": "2026-05-22T10:00:00.000Z", "updatedAt": "2026-05-22T10:00:00.000Z" } } ``` ### Parameters [#parameters] | Field | Required | Description | | ------------- | -------- | ----------------------------------------------------------------------------------------- | | `type` | yes | One of `PAYMENT_ACTIVITY`, `PAYOUT_RECONCILIATION`, `BALANCE_LEDGER`, `REFUNDS_DISPUTES`. | | `periodStart` | yes | ISO 8601 datetime. Inclusive lower bound. Interpreted as a UTC instant. | | `periodEnd` | yes | ISO 8601 datetime. Exclusive upper bound. Must be strictly after `periodStart`. | | `formats` | no | Array of `CSV` and/or `JSON`. Defaults to `["CSV"]`. At least one, at most two. | ### Errors [#errors] | Status | Cause | | ------ | ---------------------------------------------------------------------------- | | `400` | Invalid datetimes, `periodEnd` not after `periodStart`, or period > 90 days. | | `403` | Token is missing `REPORTS_WRITE`. | | `404` | Store doesn't exist or token can't access it. | | `429` | Store has 10 or more reports already in `PENDING`/`PROCESSING`. | ## Step 2: Poll for completion [#step-2-poll-for-completion] ```bash curl https://api.pandabase.io/v2/core/stores/{storeId}/reports/{reportId} \ -H "Authorization: Bearer sk_live_..." ``` The `status` field transitions: ``` PENDING โ”€โ”€โ–บ PROCESSING โ”€โ”€โ–บ SUCCEEDED (download available) โ””โ”€โ–บ FAILED (errorMessage populated) ``` Poll every 3 to 5 seconds. Stop polling once the status is `SUCCEEDED` or `FAILED`. When the report succeeds, the response includes: ```json { "status": "SUCCEEDED", "rowCount": 1247, "sizeBytes": 412980, "durationMs": 8412, "startedAt": "2026-05-22T10:00:00.100Z", "completedAt": "2026-05-22T10:00:08.512Z" } ``` ## Step 3: Download the file [#step-3-download-the-file] ```bash curl https://api.pandabase.io/v2/core/stores/{storeId}/reports/{reportId}/download/csv \ -H "Authorization: Bearer sk_live_..." ``` Response: ```json { "ok": true, "data": { "url": "https://s3.pandabase.io/.../rpt_xxx.csv?X-Amz-Algorithm=..." } } ``` Send a `GET` request to the signed URL to download the file. The URL is valid for **5 minutes**, so don't cache it. Re-fetch the download endpoint whenever you need a fresh URL. For `format`, use `csv` or `json` (lowercase). If the report wasn't generated with that format, you'll get `404`. If the report hasn't finished yet, you'll get `409`. ## Complete example (TypeScript) [#complete-example-typescript] ```typescript const STORE_ID = process.env.PANDABASE_STORE_ID!; const API_TOKEN = process.env.PANDABASE_API_TOKEN!; const BASE_URL = `https://api.pandabase.io/v2/core/stores/${STORE_ID}`; const HEADERS = { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json", }; async function exportPaymentActivity(periodStart: string, periodEnd: string) { // 1. Queue const createRes = await fetch(`${BASE_URL}/reports`, { method: "POST", headers: HEADERS, body: JSON.stringify({ type: "PAYMENT_ACTIVITY", periodStart, periodEnd, formats: ["CSV"], }), }); const { data: report } = await createRes.json(); console.log("Queued", report.id); // 2. Poll let status = report.status; while (status !== "SUCCEEDED" && status !== "FAILED") { await new Promise((r) => setTimeout(r, 3000)); const pollRes = await fetch(`${BASE_URL}/reports/${report.id}`, { headers: HEADERS, }); const { data } = await pollRes.json(); status = data.status; console.log("Status:", status); if (status === "FAILED") { throw new Error(`Report failed: ${data.errorMessage}`); } } // 3. Get download URL + fetch the file const urlRes = await fetch(`${BASE_URL}/reports/${report.id}/download/csv`, { headers: HEADERS, }); const { data: { url }, } = await urlRes.json(); const fileRes = await fetch(url); const csv = await fileRes.text(); return csv; } const csv = await exportPaymentActivity( "2026-04-01T00:00:00Z", "2026-05-01T00:00:00Z", ); console.log(`Got ${csv.split("\n").length - 1} rows`); ``` ## CSV format [#csv-format] * **Encoding:** UTF-8, with a byte-order mark for Excel compatibility. * **Header:** the first row. * **Newlines:** `\n`. * **Escaping:** values containing commas, quotes, or newlines are wrapped in double quotes, with embedded quotes doubled (`"` โ†’ `""`). * **Amounts:** stored in the smallest currency unit (cents for two-decimal currencies, whole units for zero-decimal currencies like JPY and KRW). The accompanying `currency` column tells you which. * **Timestamps:** ISO 8601 in UTC, with the `Z` suffix. * **Nulls:** empty cells. ## Limits [#limits] | Limit | Value | | ------------------------------------------------------- | ----------------------------------- | | Maximum period span | 90 days | | Concurrent reports per store (`PENDING` + `PROCESSING`) | 10 | | File retention | 30 days from creation | | Download URL TTL | 5 minutes | | Worker retries on failure | 3 attempts with exponential backoff | Reports older than 30 days are deleted automatically, both the DB row and the underlying file. A `GET` for an expired report ID returns `404`. Re-run the report if you need it again. ## Rate limits [#rate-limits] Reports endpoints inherit the standard Store API limit: **60 requests per second per token**. Polling at 3-second intervals across many parallel reports stays well under this. If you're polling more than 50 reports at once, switch to listing with a status filter: ```bash curl "https://api.pandabase.io/v2/core/stores/{storeId}/reports?status=PROCESSING&limit=100" \ -H "Authorization: Bearer sk_live_..." ``` ## Webhooks [#webhooks] Report completion does not currently fire a webhook. The store owner gets an email when the report finishes (success or failure). If you need a programmatic signal, poll `GET /reports/:id` as shown above. ## Common patterns [#common-patterns] ### Monthly cron job [#monthly-cron-job] Run a cron job at the start of each month that exports the previous month's payment activity: ```typescript import { CronJob } from "cron"; new CronJob("0 0 1 * *", async () => { const now = new Date(); const periodEnd = new Date( now.getFullYear(), now.getMonth(), 1, ).toISOString(); const periodStart = new Date( now.getFullYear(), now.getMonth() - 1, 1, ).toISOString(); const csv = await exportPaymentActivity(periodStart, periodEnd); // Upload to your data warehouse, S3, accountant's mailbox, etc. }).start(); ``` ### Year-long export [#year-long-export] Periods are capped at 90 days. To export a full year, queue four reports back-to-back. ```typescript const QUARTERS = [ { start: "2025-01-01T00:00:00Z", end: "2025-04-01T00:00:00Z" }, { start: "2025-04-01T00:00:00Z", end: "2025-07-01T00:00:00Z" }, { start: "2025-07-01T00:00:00Z", end: "2025-10-01T00:00:00Z" }, { start: "2025-10-01T00:00:00Z", end: "2026-01-01T00:00:00Z" }, ]; const csvs = await Promise.all( QUARTERS.map((q) => exportPaymentActivity(q.start, q.end)), ); ``` # Storefront API (/developers/learn/storefront-api) The Storefront API provides public, read-only access to your store's data. Include your store ID in the URL; **no authentication is required**. Use it to build storefronts, product pages, and category lists. ## Base URL [#base-url] ``` https://api.pandabase.io/v2/storefront/stores/{storeId} ``` All endpoints are prefixed with `/v2/storefront/stores/{storeId}`. ## Endpoints [#endpoints] | Method | Path | Description | | ------ | ------------------------------------------------------- | --------------------------- | | GET | `/v2/storefront/stores/:storeId` | Store info | | GET | `/v2/storefront/stores/:storeId/products` | List products (paginated) | | GET | `/v2/storefront/stores/:storeId/products/:productId` | Product detail | | GET | `/v2/storefront/stores/:storeId/categories` | List categories (paginated) | | GET | `/v2/storefront/stores/:storeId/categories/:categoryId` | Category detail | *** ## Get Store [#get-store] Returns public store information including branding, description, and social links. ``` GET /v2/storefront/stores/{storeId} ``` Response Type ```json { "ok": true, "data": { "id": "shp_abc123", "name": "My Store", "handle": "my-store", "slug": "my-store", "description": "We sell digital products.", "summary": "Digital goods marketplace", "logo": "https://cdn.pandabase.io/stores/logo.png", "favicon": "https://cdn.pandabase.io/stores/favicon.png", "accentColor": "#07C983", "primaryColor": "#0b7031", "supportEmail": "support@mystore.com", "category": ["SOFTWARE", "DIGITAL_PRODUCTS"], "twitterUrl": "https://x.com/mystore", "instagramUrl": null, "linkedinUrl": null, "youtubeUrl": null, "tiktokUrl": null } } ``` ```typescript interface Storefront { id: string; name: string; handle: string; slug: string | null; description: string; summary: string | null; logo: string | null; favicon: string | null; accentColor: string | null; primaryColor: string | null; supportEmail: string | null; category: string[]; twitterUrl: string | null; instagramUrl: string | null; linkedinUrl: string | null; youtubeUrl: string | null; tiktokUrl: string | null; } ``` *** ## List Products [#list-products] Returns a paginated list of active products. Supports filtering, searching, and sorting. ``` GET /v2/storefront/stores/{storeId}/products ``` ### Query Parameters [#query-parameters] | Parameter | Type | Default | Description | | -------------- | ------- | ----------- | -------------------------------------------------------------------------------------------------------------- | | `page` | integer | `1` | Page number (min: 1) | | `limit` | integer | `25` | Items per page (1-100) | | `search` | string | โ€” | Full-text search on product title | | `categoryId` | string | โ€” | Filter by category ID | | `productType` | string | โ€” | Filter by type: `SERIAL`, `SERVICE`, `ONE_TIME`, `PHYSICAL`, `SUBSCRIPTION`, `DIGITAL_DOWNLOAD`, `LICENSE_KEY` | | `pricingModel` | string | โ€” | Filter by pricing: `STANDARD`, `PAY_WHAT_YOU_WANT`, `FREE` | | `sortBy` | string | `createdAt` | Sort field: `title`, `price`, `createdAt` | | `sortOrder` | string | `desc` | Sort direction: `asc`, `desc` | cURL Response ```bash curl "https://api.pandabase.io/v2/storefront/stores/shp_abc123/products?limit=10&sortBy=price&sortOrder=asc" ``` ```json { "ok": true, "data": { "items": [ { "id": "prd_abc123", "title": "Pro Plan", "subtitle": "Everything you need", "handle": "pro-plan", "price": 2999, "compareAtPrice": 4999, "images": ["https://cdn.pandabase.io/products/pro.png"], "inStock": true, "currency": "USD", "productType": "LICENSE_KEY", "pricingModel": "STANDARD", "status": "ACTIVE", "minimumPrice": null, "categories": [ { "id": "ctg_abc123", "name": "Software", "slug": "software" } ] } ], "pagination": { "page": 1, "limit": 10, "total": 42, "totalPages": 5 } } } ``` Only active products are returned. Draft and deleted products are never included in storefront responses. *** ## Get Product [#get-product] Returns full product details including variants, options, and categories. ``` GET /v2/storefront/stores/{storeId}/products/{productId} ``` Response Type ```json { "ok": true, "data": { "id": "prd_abc123", "title": "Pro Plan", "subtitle": "Everything you need", "description": "Full access to all features.", "handle": "pro-plan", "price": 2999, "compareAtPrice": 4999, "images": ["https://cdn.pandabase.io/products/pro.png"], "inStock": true, "currency": "USD", "productType": "LICENSE_KEY", "message": "Thanks for purchasing!", "pricingModel": "STANDARD", "status": "ACTIVE", "minimumPrice": null, "maxPerCustomer": 3, "fulfillmentMode": "MANAGED_LICENSE", "availableFrom": null, "availableUntil": null, "billingInterval": null, "billingAnchor": null, "trialDays": null, "options": [ { "id": "opt_abc123", "name": "License Type", "values": ["Personal", "Commercial"], "position": 0 } ], "variants": [ { "id": "var_abc123", "title": "Personal License", "slug": "personal-license", "description": "For individual use", "sku": "PRO-PERSONAL", "options": { "License Type": "Personal" }, "price": 2999, "compareAtPrice": 4999, "images": [], "inStock": true, "quantity": null, "trackStock": false, "position": 0 } ], "categories": [ { "id": "ctg_abc123", "name": "Software", "slug": "software" } ] } } ``` ```typescript interface StorefrontProduct { id: string; title: string; subtitle: string | null; description: string | null; handle: string; price: number; compareAtPrice: number | null; images: string[]; inStock: boolean; currency: string; productType: string; message: string | null; pricingModel: string; status: string; minimumPrice: number | null; maxPerCustomer: number | null; fulfillmentMode: string | null; availableFrom: string | null; availableUntil: string | null; billingInterval: string | null; billingAnchor: string | null; trialDays: number | null; options: Array<{ id: string; name: string; values: string[]; position: number; }>; variants: Array<{ id: string; title: string; slug: string | null; description: string | null; sku: string | null; options: Record; price: number; compareAtPrice: number | null; images: string[]; inStock: boolean; quantity: number | null; trackStock: boolean; position: number; }>; categories: Array<{ id: string; name: string; slug: string; }>; } ``` The product detail response includes fields not shown in the list view: `description`, `message`, `fulfillmentMode`, `maxPerCustomer`, `availableFrom`/`availableUntil`, subscription fields, `options`, and `variants`. *** ## List Categories [#list-categories] Returns a paginated list of categories. Supports filtering by featured status and parent category. ``` GET /v2/storefront/stores/{storeId}/categories ``` ### Query Parameters [#query-parameters-1] | Parameter | Type | Default | Description | | ---------- | ------- | ------- | ------------------------------------------------ | | `page` | integer | `1` | Page number (min: 1) | | `limit` | integer | `25` | Items per page (1-100) | | `featured` | boolean | โ€” | Filter by featured status | | `parentId` | string | โ€” | Filter by parent category ID (for subcategories) | cURL Response ```bash curl "https://api.pandabase.io/v2/storefront/stores/shp_abc123/categories?featured=true" ``` ```json { "ok": true, "data": { "items": [ { "id": "ctg_abc123", "name": "Software", "slug": "software", "description": "Software products and licenses", "icon": "code", "parentId": null, "displayOrder": 0, "featured": true } ], "pagination": { "page": 1, "limit": 25, "total": 5, "totalPages": 1 } } } ``` *** ## Get Category [#get-category] Returns category details including subcategories and up to 50 active products. ``` GET /v2/storefront/stores/{storeId}/categories/{categoryId} ``` Response Type ```json { "ok": true, "data": { "id": "ctg_abc123", "name": "Software", "slug": "software", "description": "Software products and licenses", "icon": "code", "parentId": null, "displayOrder": 0, "featured": true, "metaTitle": "Software Products", "metaDescription": "Browse our software collection", "showInNavigation": true, "children": [ { "id": "ctg_def456", "name": "Plugins", "slug": "plugins" } ], "products": [ { "id": "prd_abc123", "title": "Pro Plan", "handle": "pro-plan", "price": 2999, "images": ["https://cdn.pandabase.io/products/pro.png"], "inStock": true } ] } } ``` ```typescript interface StorefrontCategory { id: string; name: string; slug: string; description: string | null; icon: string | null; parentId: string | null; displayOrder: number; featured: boolean; metaTitle: string | null; metaDescription: string | null; showInNavigation: boolean; children: Array<{ id: string; name: string; slug: string }>; products: Array<{ id: string; title: string; handle: string; price: number; images: string[]; inStock: boolean; }>; } ``` The category detail endpoint returns up to **50 products**. For full product listings within a category, use the [List Products](#list-products) endpoint with the `categoryId` filter. *** ## Error Responses [#error-responses] All error responses follow the standard format: ```json { "ok": false, "error": "Store not found" } ``` | Status | Error | When | | ------ | ------------------ | ---------------------------------------------- | | `404` | Store not found | Invalid or blocked store ID | | `404` | Product not found | Product doesn't exist, is draft, or is deleted | | `404` | Category not found | Category doesn't exist in this store | ## Example: Building a Product Page [#example-building-a-product-page] ```typescript const STORE_ID = "shp_abc123"; const API = `https://api.pandabase.io/v2/storefront/stores/${STORE_ID}`; // Fetch store branding const store = await fetch(`${API}`).then((r) => r.json()); // Fetch featured products const products = await fetch( `${API}/products?limit=12&sortBy=createdAt&sortOrder=desc`, ).then((r) => r.json()); // Fetch a single product with variants const product = await fetch(`${API}/products/prd_abc123`).then((r) => r.json(), ); // Fetch categories for navigation const categories = await fetch(`${API}/categories?featured=true`).then((r) => r.json(), ); ``` All monetary values are in **cents** (integers). Divide by 100 for display (e.g., `2999` = `$29.99`). # Subscriptions (/developers/learn/subscriptions) ## Overview [#overview] Create a product with `type: SUBSCRIPTION` to accept recurring payments. Pandabase handles billing, retries, 3D Secure authentication, invoices, and cancellation. ## How it works [#how-it-works] 1. **Create a subscription product** with a billing interval (weekly, monthly, or yearly) 2. **Customer checks out** โ€” pays the first charge (or starts a free trial) 3. **Card is saved** โ€” Pandabase stores the payment method securely for future charges 4. **Automatic renewals** โ€” the platform charges the customer on each billing cycle 5. **Invoices sent** โ€” customers receive a PDF invoice email on every charge 6. **Lifecycle events** โ€” webhooks fire on every state change ## Creating a subscription product [#creating-a-subscription-product] Set `type` to `SUBSCRIPTION` and configure the billing interval: ```bash curl -X POST https://api.pandabase.io/v2/core/stores/{storeId}/products \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "title": "Pro Plan", "price": 1999, "type": "SUBSCRIPTION", "billingInterval": "MONTHLY", "status": "ACTIVE", "fulfillmentMode": "MANUAL" }' ``` | Field | Options | Description | | ----------------- | ----------------------------- | ------------------------------------------------ | | `billingInterval` | `WEEKLY`, `MONTHLY`, `YEARLY` | How often the customer is charged | | `trialDays` | `0`โ€“`365` | Free trial period before first charge (optional) | ## Checkout flow [#checkout-flow] Create a checkout session with a subscription product โ€” the checkout UI automatically adapts: * Shows the billing interval (e.g. "$19.99 / month") * Shows trial info if applicable (e.g. "7-day free trial, then $19.99/month") * Restricts payment to card and Link (required for recurring billing) * Shows a consent checkbox before payment * Button text changes to "Subscribe" or "Start subscription" ```bash curl -X POST https://api.pandabase.io/v2/stores/{storeId}/checkouts \ -H "Content-Type: application/json" \ -d '{ "items": [{ "product_id": "prd_xxx", "quantity": 1 }], "customer": { "name": "Jane Doe", "email": "jane@example.com", "billing": { "line1": "123 Main St", "city": "San Francisco", "state": "CA", "postal_code": "94105", "country": "US" } } }' ``` ## Subscription statuses [#subscription-statuses] | Status | Description | | ----------- | -------------------------------------------------------------------- | | `TRIALING` | Customer is in a free trial โ€” no charges until trial ends | | `ACTIVE` | Billing is active, customer is being charged on schedule | | `PAST_DUE` | Payment failed โ€” retrying automatically or awaiting 3DS verification | | `PAUSED` | Billing paused by merchant โ€” no charges until resumed | | `CANCELLED` | Subscription ended โ€” no further charges | ## Free trials [#free-trials] Set `trial_days` on the product to offer a free trial: ```json { "title": "Pro Plan", "price": 1999, "type": "SUBSCRIPTION", "billingInterval": "MONTHLY", "trialDays": 7, "status": "ACTIVE" } ``` During checkout, the customer saves their card without being charged. The subscription starts in `TRIALING` status and the first charge happens automatically when the trial ends. ## Failed payments and retries [#failed-payments-and-retries] When a renewal payment fails, Pandabase: 1. Moves the subscription to `PAST_DUE` 2. Fires a `SUBSCRIPTION_PAST_DUE` webhook 3. Emails the customer about the failed payment 4. Retries automatically with backoff: * **Retry 1**: 24 hours later * **Retry 2**: 48 hours later * **Retry 3**: 72 hours later 5. If all retries fail, the subscription is cancelled ## 3D Secure on renewals [#3d-secure-on-renewals] Some banks require 3D Secure authentication for recurring charges. When this happens: 1. The subscription moves to `PAST_DUE` 2. The customer receives an email with a secure verification link 3. The customer clicks the link and completes the bank's authentication 4. The payment is processed and the subscription returns to `ACTIVE` No action is needed from you โ€” Pandabase handles the authentication flow automatically. ## Managing subscriptions [#managing-subscriptions] ### Via API [#via-api] Use `SUBSCRIPTIONS_READ` and `SUBSCRIPTIONS_WRITE` permissions on your API token: ```bash # list subscriptions GET /v2/core/stores/{storeId}/subscriptions # get subscription detail GET /v2/core/stores/{storeId}/subscriptions/{subscriptionId} # cancel (at period end by default) POST /v2/core/stores/{storeId}/subscriptions/{subscriptionId}/cancel # cancel immediately POST /v2/core/stores/{storeId}/subscriptions/{subscriptionId}/cancel -d '{ "immediate": true }' # pause billing POST /v2/core/stores/{storeId}/subscriptions/{subscriptionId}/pause # resume billing POST /v2/core/stores/{storeId}/subscriptions/{subscriptionId}/resume ``` ### Via Customer Portal [#via-customer-portal] Customers can view and cancel their subscriptions at [mypandabase.com](https://mypandabase.com). Cancellation is always at period end โ€” customers retain access until the current billing period expires. ## Cancellation behavior [#cancellation-behavior] | Who cancels | Behavior | | ------------------------ | -------------------------------------------------------------------------------------- | | **Merchant** (default) | Cancels at end of current billing period | | **Merchant** (immediate) | Cancels immediately, revokes access, revokes licenses if `revoke_on_refund` is enabled | | **Customer** | Always cancels at end of current billing period | | **System** (max retries) | Cancels after 4 failed payment attempts | ## Webhook events [#webhook-events] Six events are available for subscription state changes: | Event | When | | ------------------------ | ----------------------------------------------- | | `SUBSCRIPTION_CREATED` | First payment or trial setup succeeds | | `SUBSCRIPTION_RENEWED` | Renewal payment succeeds | | `SUBSCRIPTION_PAST_DUE` | Renewal failed or needs 3DS | | `SUBSCRIPTION_CANCELLED` | Cancelled by merchant, customer, or max retries | | `SUBSCRIPTION_PAUSED` | Merchant paused billing | | `SUBSCRIPTION_RESUMED` | Merchant resumed billing | See [Webhook Events](/developers/webhooks/events) for payload details and integration examples. ## Invoices [#invoices] Customers automatically receive a PDF invoice via email on: * The first subscription payment * Every renewal payment Invoices include the product name, amount, billing period, tax breakdown, and next charge date. ## Usage-based billing [#usage-based-billing] If you need to charge per request, per token, per seat, or any other metric where the bill is not known in advance, configure the product with `pricingModel: USAGE_BASED` and attach meters. The lifecycle, retries, invoices, and webhook events described on this page apply unchanged. See [Usage-based billing](/developers/learn/usage-based-billing) for the meter model, event ingestion, and renewal math. ## Limitations [#limitations] * Only one subscription item per checkout (no mixing with one-time items) * Billing is in USD only (multicurrency renewals are a future enhancement) * Price is locked at creation โ€” changing the product price does not affect existing subscriptions * Subscription upgrades/downgrades are not yet supported โ€” cancel and re-subscribe # Usage-based billing (/developers/learn/usage-based-billing) ## Overview [#overview] Usage-based billing lets you charge a subscription customer based on how much of your product they use during a billing period. You define one or more **meters** on a subscription product, report **usage events** as they happen, and Pandabase aggregates the totals and bills the customer at each renewal. Use this for API requests, tokens, active users, storage, or other charges that vary with usage. ## How it works [#how-it-works] 1. **Create a subscription product** with `pricingModel` set to `USAGE_BASED`. 2. **Add one or more meters** to the product. Each meter defines an event name, a rate, an aggregation rule, and a settlement mode. 3. **Customer subscribes** through the usual checkout flow. The saved card stays on file for renewals. 4. **Report usage events** to Pandabase as they happen from your application code. Each event is tagged with the meter's `event_name`. 5. **At renewal**, Pandabase aggregates events per meter, prices each one, adds the base subscription amount where applicable, and charges the customer. An invoice email goes out with the full breakdown. ## Step 1: Create a usage-based product [#step-1-create-a-usage-based-product] Set `pricingModel` to `USAGE_BASED` on a subscription product. The product still needs a `billingInterval` (the period over which usage accumulates) and a `price` (the base fee charged at each renewal). ```bash curl -X POST https://api.pandabase.io/v2/core/stores/{storeId}/products \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "title": "AI Inference API", "price": 1000, "type": "SUBSCRIPTION", "billingInterval": "MONTHLY", "pricingModel": "USAGE_BASED", "status": "ACTIVE", "fulfillmentMode": "MANUAL" }' ``` The platform enforces a **$1.00 minimum** on every checkout total. The first charge for a usage-based subscription happens at signup and contains the base `price` only (no usage has been reported yet), so `price` must be at least `100` cents. Pure pay-as-you-go (`price: 0`) is not supported at signup. Pick a small base fee or set the base to cover an included allowance with `BASE_PLUS_OVERAGE` meters. | Field | Required | Description | | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `pricingModel` | yes | Must be `"USAGE_BASED"`. | | `type` | yes | Must be `"SUBSCRIPTION"`. Usage-based one-time products are not supported. | | `billingInterval` | yes | `WEEKLY`, `MONTHLY`, or `YEARLY`. Defines the aggregation window. | | `price` | yes | Base fee charged at each renewal, in cents. Must be at least `100` so the initial checkout clears the $1.00 minimum. | ## Step 2: Configure meters [#step-2-configure-meters] A meter declares one thing your product charges for: tokens, requests, gigabytes, active users, and so on. A product can have up to **10 meters**. ```bash curl -X POST https://api.pandabase.io/v2/core/stores/{storeId}/products/{productId}/meters \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "eventName": "input_tokens", "unitPrice": 300, "unitQuantity": 1000000, "aggregation": "SUM", "settlement": "ARREARS" }' ``` This meter charges **$3.00 per 1,000,000 input tokens** consumed in a period. The math is `floor(billable_quantity * unitPrice / unitQuantity)`, all in cents, so fractional-cent pricing works without floats. | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------- | | `eventName` | The name you'll report events under. Matches `^[a-zA-Z0-9_.-]+$`, unique per product. | | `unitPrice` | Price in cents per `unitQuantity` units. | | `unitQuantity` | The denominator for `unitPrice`. Use `1` for "per event", `1000000` for "per million tokens", etc. Defaults to `1`. | | `aggregation` | How events are combined over the billing period. See [Aggregation rules](#aggregation-rules). | | `settlement` | `ARREARS` or `BASE_PLUS_OVERAGE`. See [Settlement modes](#settlement-modes). | | `includedUnits` | Only valid (and required) for `BASE_PLUS_OVERAGE`. Units already included in the base fee. | ### Read, update, and delete meters [#read-update-and-delete-meters] ```bash # List every meter on a product curl https://api.pandabase.io/v2/core/stores/{storeId}/products/{productId}/meters \ -H "Authorization: Bearer {token}" # Retrieve a single meter curl https://api.pandabase.io/v2/core/stores/{storeId}/products/{productId}/meters/{meterId} \ -H "Authorization: Bearer {token}" # Update rate fields (eventName / aggregation / settlement become immutable # once any event has been reported against the meter) curl -X PATCH https://api.pandabase.io/v2/core/stores/{storeId}/products/{productId}/meters/{meterId} \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "unitPrice": 250 }' # Delete a meter (refused with 409 if any event or billed period references it) curl -X DELETE https://api.pandabase.io/v2/core/stores/{storeId}/products/{productId}/meters/{meterId} \ -H "Authorization: Bearer {token}" ``` `GET` and `LIST` require `METERS_READ`. `POST`, `PATCH`, and `DELETE` require `METERS_WRITE`. ## Step 3: Customer subscribes [#step-3-customer-subscribes] Customers subscribe through the standard hosted checkout. There is no API difference between a flat-rate subscription and a usage-based one from the customer's perspective. The card is saved, the subscription starts in `ACTIVE` (or `TRIALING` if the product has trial days), and the first usage period opens. ```bash curl -X POST https://api.pandabase.io/v2/stores/{storeId}/checkouts \ -H "Content-Type: application/json" \ -d '{ "items": [{ "product_id": "prd_xxx", "quantity": 1 }], "customer": { "name": "Jane Doe", "email": "jane@example.com", "billing": { "line1": "123 Main St", "city": "San Francisco", "state": "CA", "postal_code": "94105", "country": "US" } } }' ``` After the customer completes the checkout, your `SUBSCRIPTION_CREATED` webhook fires and the returned subscription ID becomes the target for usage reporting. ## Step 4: Report usage events [#step-4-report-usage-events] Send a usage event for each billable action. The Store API accepts up to **100 events per call** for a single subscription. The usage endpoints use `snake_case` field names. The rest of the Store API uses `camelCase`. This is intentional: events typically come from analytics pipelines and logs that already emit snake-case keys. Events are accepted for subscriptions in `ACTIVE`, `TRIALING`, and `PAST_DUE` status. Events reported during a trial accrue into the open period and are rolled into the first non-trial charge. Events reported during `PAST_DUE` continue to accumulate; once the failed renewal succeeds on retry, the customer is billed for the full accumulated total. ```bash curl -X POST https://api.pandabase.io/v2/core/stores/{storeId}/usage \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "subscription_id": "sub_xxx", "events": [ { "event_name": "input_tokens", "quantity": 1500, "event_at": "2026-05-21T14:23:00Z", "external_id": "req-abc-123", "metadata": { "endpoint": "/v1/chat", "model": "claude-opus-4-7" } } ] }' ``` The response tells you which events were accepted, which were silently deduplicated, and which were rejected. ```json { "ok": true, "data": { "subscriptionId": "sub_xxx", "accepted": 1, "duplicates": 0, "rejected": [] } } ``` The top-level call returns `201` even if some events were rejected. Check `rejected[]` and handle errors for each rejected event. ### Rejection reasons [#rejection-reasons] Each entry in `rejected[]` has the form `{ "index": , "reason": }`. The `index` matches the position in your submitted `events` array. Possible reasons: | Reason | Cause | Fix | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | `no meter on this product matches event_name ''` | The `event_name` does not match any meter on the subscription's product. | Check spelling and that the meter exists on the right product. | | `subscription is not in a state that accepts usage events` | Subscription is `PAUSED`, `CANCELLED`, or otherwise terminal. | Stop reporting; the subscription is no longer billable. | | `event_at is not a valid ISO timestamp` | `event_at` was provided but not parseable. | Use a full ISO-8601 string, e.g. `"2026-05-21T14:23:00Z"`. | | `event_at is before the current period start` | The timestamp falls in an already-closed billing period. | Drop the event or report it under the next subscription's first period if applicable. | | `event_at is too far in the future` | More than one hour ahead of server time. | Check clock skew on the reporting host. | | `metadata exceeds 20 keys` (and similar key-length / value-length variants) | Metadata exceeded the size caps. | Trim metadata to 20 keys, 40 chars per key, 500 chars per value. | ### Event fields [#event-fields] | Field | Required | Description | | ------------- | -------- | --------------------------------------------------------------------------------------------------------- | | `event_name` | yes | Must match a meter on the subscription's product. | | `quantity` | yes | Non-negative integer. For `aggregation: COUNT` this is usually `1` per event. | | `event_at` | no | ISO-8601 timestamp. Defaults to the current server time. Must fall in `[current_period_start, now + 1h]`. | | `external_id` | no | Your idempotency key. Duplicates on `(meter_id, external_id)` are silently skipped. | | `metadata` | no | Up to 20 string keys, 40 chars per key, 500 chars per value. Stored for audit, not used in billing. | ### TypeScript example [#typescript-example] ```typescript const STORE_ID = process.env.PANDABASE_STORE_ID!; const API_TOKEN = process.env.PANDABASE_API_TOKEN!; async function reportTokenUsage( subscriptionId: string, requestId: string, inputTokens: number, ) { const res = await fetch( `https://api.pandabase.io/v2/core/stores/${STORE_ID}/usage`, { method: "POST", headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ subscription_id: subscriptionId, events: [ { event_name: "input_tokens", quantity: inputTokens, external_id: requestId, }, ], }), }, ); if (!res.ok) { const { error } = await res.json(); throw new Error(`Usage report failed: ${error}`); } } ``` ### Batching for high-throughput reporters [#batching-for-high-throughput-reporters] If you are reporting events at a rate where a per-request HTTP call is impractical, buffer events in memory and flush in batches of up to 100. The `external_id` makes the flush call safe to retry on transient failures. ```typescript type PendingEvent = { event_name: string; quantity: number; external_id: string; event_at?: string; }; class UsageReporter { private buffer: PendingEvent[] = []; private flushing = false; constructor( private readonly storeId: string, private readonly token: string, private readonly subscriptionId: string, private readonly flushIntervalMs = 5000, private readonly batchSize = 100, ) { setInterval(() => this.flush().catch(console.error), this.flushIntervalMs); } enqueue(event: PendingEvent) { this.buffer.push(event); if (this.buffer.length >= this.batchSize) { this.flush().catch(console.error); } } async flush() { if (this.flushing || this.buffer.length === 0) return; this.flushing = true; const batch = this.buffer.splice(0, this.batchSize); try { const res = await fetch( `https://api.pandabase.io/v2/core/stores/${this.storeId}/usage`, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ subscription_id: this.subscriptionId, events: batch, }), }, ); if (!res.ok) { // 5xx or 429: put the batch back at the front and try again on the // next tick. The external_id guarantees no double-billing. this.buffer.unshift(...batch); } } catch { this.buffer.unshift(...batch); } finally { this.flushing = false; } } } ``` Call `reporter.flush()` on graceful shutdown so in-flight events are not lost. ### Listing reported events [#listing-reported-events] Use the list endpoint to review reported events. Results are paginated, with the most recent events first. ```bash curl "https://api.pandabase.io/v2/core/stores/{storeId}/usage?subscription_id=sub_xxx&meter_id=umt_xxx&from=2026-05-01T00:00:00Z&to=2026-06-01T00:00:00Z&page=1&limit=50" \ -H "Authorization: Bearer {token}" ``` | Query param | Required | Description | | ----------------- | -------- | ----------------------------------------------------- | | `subscription_id` | yes | The subscription whose events you want. | | `meter_id` | no | Filter to a single meter. | | `from` | no | ISO timestamp; only events at or after this time. | | `to` | no | ISO timestamp; only events strictly before this time. | | `page` | no | 1-indexed. Defaults to `1`. | | `limit` | no | `1` to `100`. Defaults to `25`. | Requires the `USAGE_READ` scope. ## Step 5: Renewal and billing [#step-5-renewal-and-billing] At the end of each billing period, Pandabase: 1. Aggregates events on each meter using the meter's `aggregation` rule. 2. Computes the billable amount per meter using its `settlement` mode. 3. Adds the product's base `price` to produce the pre-tax subtotal. 4. Applies tax on the subtotal at the customer's billing-country rate, using the same tax engine that runs at signup. 5. Charges the saved payment method for the post-tax total. 6. Sends the customer an itemized invoice email showing the base fee, a row per meter (`eventName ยท quantity ร— $rate / unit = $amount`), and the tax line. 7. Fires a `SUBSCRIPTION_RENEWED` webhook with the new period dates. You can view the same numbers your customer sees by reading the subscription's usage projection: ```bash curl https://api.pandabase.io/v2/core/stores/{storeId}/subscriptions/{subscriptionId}/usage \ -H "Authorization: Bearer {token}" ``` ```json { "ok": true, "data": { "subscriptionId": "sub_xxx", "isUsageBased": true, "currency": "USD", "currentPeriod": { "periodStart": "2026-05-01T00:00:00Z", "periodEnd": "2026-06-01T00:00:00Z", "baseAmount": 1000, "usageAmount": 1970, "projectedTotal": 2970, "meters": [ { "meterId": "umt_xxx", "eventName": "input_tokens", "quantity": 6566667, "amountCharged": 1970 } ] }, "pastPeriods": [ /* one entry per closed billing period */ ] } } ``` The current-period numbers are a live projection. They use the same compute path the renewal worker runs at period close, so what you see is what your customer will be charged. ### Failed renewals [#failed-renewals] If the renewal charge fails (declined card, insufficient funds, expired card, and so on) the subscription moves to `PAST_DUE` and Pandabase retries automatically: at 24, 48, 72, and 96 hours. After four failed attempts the subscription is cancelled. The accumulated usage stays on the open period across retries. Events reported during `PAST_DUE` continue to accrue. Once any retry succeeds, the customer is billed for the full accumulated total and the period advances. ### Cancellation [#cancellation] Use the standard subscription cancellation endpoints. Behavior depends on the `immediate` flag: | `immediate` | Behavior on a usage-based subscription | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `false` (default) | Subscription stays active until `current_period_end`. The final renewal at period close runs as normal and bills the customer for the base fee plus any accumulated usage. | | `true` | Subscription ends immediately. Usage events reported during the open period are **not** billed. The subscription transitions to `CANCELLED` and no further renewal fires. | Customer-initiated cancellations from the customer portal always behave as `immediate: false`. ## Aggregation rules [#aggregation-rules] The `aggregation` value controls how events are combined into a single billable quantity per period. | Rule | Behavior | Typical use | | ------- | -------------------------------------------------- | ----------------------------------------------- | | `SUM` | Add the `quantity` from every event in the period. | Token usage, API calls, bytes transferred. | | `MAX` | Take the highest `quantity` reported. | Peak active users, peak storage. | | `LAST` | Use the most recent event's `quantity`. | Current seat count, current plan tier. | | `COUNT` | Count the number of events. Ignores `quantity`. | Distinct actions where the value is always `1`. | ## Settlement modes [#settlement-modes] The `settlement` value decides how the billable quantity translates into a charge. ### ARREARS [#arrears] Charges the full aggregated quantity at the meter's rate. There is no included allowance. Best for pure pay-as-you-go pricing where the base fee is `0`. ``` billable_quantity = aggregate charge = floor(billable_quantity ร— unitPrice / unitQuantity) ``` ### BASE\_PLUS\_OVERAGE [#base_plus_overage] Subtracts an included allowance before charging. Best when the subscription's base `price` already covers an included amount and you only want to bill for overages. ``` billable_quantity = max(0, aggregate โˆ’ includedUnits) charge = floor(billable_quantity ร— unitPrice / unitQuantity) ``` `includedUnits` is required for `BASE_PLUS_OVERAGE` and rejected for `ARREARS`. ## Editing meters after launch [#editing-meters-after-launch] Some meter fields are locked the moment the first usage event is reported, because changing them after the fact would silently reinterpret historical billing. | Field | Editable | Notes | | --------------- | ------------------------ | --------------------------------------------------------------- | | `eventName` | locked once events exist | Renaming would orphan your application's reporting contract. | | `aggregation` | locked once events exist | Changes how every existing event is bucketed. | | `settlement` | locked once events exist | Changes the billing math. | | `unitPrice` | always editable | New rate applies to the in-progress period at the next renewal. | | `unitQuantity` | always editable | Same as above. | | `includedUnits` | always editable | Same as above. | If you need to change a locked field, create a new meter (or a new product) and migrate. ## Deleting meters [#deleting-meters] A meter can only be deleted if no usage event or billed period summary references it. Once events exist, the meter is permanent: audit history is preserved indefinitely. The API returns `409 Conflict` if you try to delete a meter with referenced records. ## Idempotency and duplicates [#idempotency-and-duplicates] Pass an `external_id` on every event. Pandabase deduplicates on `(meter_id, external_id)` and silently drops repeats, which makes the ingestion endpoint safe to retry from your application without double-billing. Without an `external_id`, an event will be re-counted on every retry. ## Zero-dollar renewal periods [#zero-dollar-renewal-periods] If a renewal period closes with a $0 total, Pandabase silently advances the subscription to the next period instead of attempting to charge the customer for nothing. No invoice email and no `SUBSCRIPTION_RENEWED` webhook fires for that period. Any period with a non-zero total goes through the regular renewal path. This only applies at renewal. The **initial checkout** always charges the customer at signup, so a usage-based subscription must start with a `price` of at least `100` cents. ## Currency [#currency] Usage-based subscriptions are **USD only** today. The product's `price` and every meter's `unitPrice` are in cents, USD. Multi-currency support for usage products is a future enhancement. ## Token scopes [#token-scopes] | Scope | Required for | | -------------- | -------------------------------------------------------- | | `METERS_READ` | List or retrieve meters. | | `METERS_WRITE` | Create, update, or delete meters. | | `USAGE_WRITE` | Report usage events. | | `USAGE_READ` | List raw events and read subscription usage projections. | Provision a separate token for your high-throughput reporting workload, scoped to just `USAGE_WRITE`, so the reporting service cannot change your pricing. Bearer mode is shown in every example on this page. HMAC mode works identically: pass the token ID in `Authorization: Bearer stk_...` and the request body's HMAC-SHA256 signature in `X-Signature`. See [API Tokens](/developers/learn/api-tokens) for signing instructions. ## Webhook events [#webhook-events] No usage-specific webhook events fire on ingestion. The relevant subscription events still apply at the period boundaries: | Event | When | | ------------------------ | -------------------------------------------------- | | `SUBSCRIPTION_CREATED` | First payment or trial setup succeeds. | | `SUBSCRIPTION_RENEWED` | A non-zero renewal payment succeeds. | | `SUBSCRIPTION_PAST_DUE` | Renewal failed or needs additional authentication. | | `SUBSCRIPTION_CANCELLED` | Subscription ended. | The `SUBSCRIPTION_RENEWED` payload includes the new `current_period_start`, `current_period_end`, and the billed `amount` (base + usage + tax). It does **not** include the per-meter breakdown. If your handler needs that detail (for example, to mirror usage charges into your own accounting), call `GET /v2/core/stores/{storeId}/subscriptions/{subscriptionId}/usage` from inside the handler. The newly closed period will appear as the first entry in `pastPeriods` with the full meter breakdown. See [Webhook events](/developers/webhooks/events). ## Limitations [#limitations] * Only `SUBSCRIPTION` products can be usage-based. One-time products and paylinks do not support meters. * One usage event refers to one subscription. The endpoint takes a single `subscription_id` per call. * Usage subscriptions are USD only. * The `event_at` window is strict: `event_at` must be at or after `current_period_start` and no more than one hour in the future (for clock-skew tolerance). Late-arriving events that fall in a closed period are rejected with a clear reason in the response. * A product is capped at 10 meters. * Tiered or graduated pricing (different rates above different volume thresholds) is not yet supported. Use a single rate per meter and adjust `unitQuantity` if you need a different denominator. * Mid-cycle plan changes (upgrade or downgrade between usage-based products) are not yet supported. Cancel and re-subscribe. # Go (/developers/sdks/go) This guide is a work in progress. ## Installation [#installation] Make sure your project is using Go Modules (it will have a `go.mod` file in its root if it is) ```sh go mod init ``` Import `pandabase-go` in your code: ```go import ( "github.com/pandabase/pandabase-go/v1" ) ``` Run any of the normal `go` commands (`build`/`install`/`test`). The Go toolchain will resolve and fetch the `pandabase-go` module automatically. You can also add the package explicitly: ```sh go get -u github.com/pandabase/pandabase-go/v1 ``` # TypeScript (/developers/sdks/typescript) This guide is a work in progress. Use the TypeScript SDK to call the Pandabase API from your server. ## Installation [#installation] Install the package with: npm pnpm bun ```sh npm install @pandabase/node ``` ```sh pnpm add @pandabase/node ``` ```sh bun add @pandabase/node ``` ## Usage [#usage] Configure the client with your store ID and API key from the dashboard. ```ts import { Pandabase } from "@pandabase/node"; const pandabase = new Pandabase({ storeId: "shp_", apiKey: "sk_...", }); await pandabase.checkouts.create({ items: [ { name: "Product A", quantity: 1, amount: 1000, // $10.00 USD }, ], // https://docs.pandabase.io/api-reference/checkouts/create-checkout-session#body-items customer: { email: "joe@gmail.com", }, }); await store.checkouts.estimate({}); // same API as above just returns a preview ``` # Events (/developers/webhooks/events) This page lists every event Pandabase sends and what each one means. For delivery semantics, signature verification, and retry behavior, see the [Webhooks overview](/developers/webhooks/overview). ## All event types [#all-event-types] | Event | Description | | --------------------------- | -------------------------------------------------------------------------------- | | `PAYMENT_PENDING` | A customer initiated payment at checkout and the order is awaiting confirmation. | | `PAYMENT_COMPLETED` | A payment was successfully collected โ€” the primary event for fulfilling orders. | | `PAYMENT_FAILED` | A payment failed, was canceled, or expired without completing. | | `PAYMENT_REFUNDED` | A charge was refunded and the order moved to refunded status. | | `PAYMENT_DISPUTED` | A customer opened a chargeback dispute on a payment. | | `PAYMENT_DISPUTE_WON` | A dispute was resolved in the merchant's favor and funds were restored. | | `PAYMENT_DISPUTE_LOST` | A dispute was resolved against the merchant and funds remain lost. | | `PAYMENT_DISPUTE_PREVENTED` | A chargeback was prevented before becoming a formal dispute. | | `SUBSCRIPTION_CREATED` | A new subscription was activated via checkout or a free trial. | | `SUBSCRIPTION_RENEWED` | A recurring subscription payment was successfully collected. | | `SUBSCRIPTION_PAST_DUE` | A renewal payment failed or requires additional authentication. | | `SUBSCRIPTION_CANCELLED` | A subscription was cancelled by the merchant, customer, or failed retries. | | `SUBSCRIPTION_PAUSED` | A merchant paused a subscription, halting further charges. | | `SUBSCRIPTION_RESUMED` | A merchant resumed a previously paused subscription. | | `SUBSCRIPTION_UPDATED` | A subscription's plan, quantity, or payment method changed. | | `SUBSCRIPTION_TRIAL_ENDING` | A trial is ending soon and billing is about to begin. | | `SUBSCRIPTION_RENEWING` | A subscription is about to renew (sent a few days before the charge). | ## Payload shape [#payload-shape] Every delivery is a JSON `POST` with the event type, a unique event ID, a timestamp, and a `data` object. ```typescript title="webhook.d.ts" type WebhookEvent = | "PAYMENT_PENDING" | "PAYMENT_COMPLETED" | "PAYMENT_FAILED" | "PAYMENT_REFUNDED" | "PAYMENT_DISPUTED" | "PAYMENT_DISPUTE_WON" | "PAYMENT_DISPUTE_LOST" | "PAYMENT_DISPUTE_PREVENTED" | "SUBSCRIPTION_CREATED" | "SUBSCRIPTION_RENEWED" | "SUBSCRIPTION_PAST_DUE" | "SUBSCRIPTION_CANCELLED" | "SUBSCRIPTION_PAUSED" | "SUBSCRIPTION_RESUMED" | "SUBSCRIPTION_UPDATED" | "SUBSCRIPTION_TRIAL_ENDING" | "SUBSCRIPTION_RENEWING"; ``` All monetary values are in **cents** (integers, no floats). ## Payment events [#payment-events] *** ### PAYMENT\_PENDING [#payment_pending] Fired when a customer initiates payment at checkout. The order has been created and a payment intent is awaiting confirmation. At this stage, geo data (`city`, `region`, `country`) may be `null` โ€” it is enriched asynchronously after checkout. Subsequent events will include full geo data. #### Payload [#payload] ```json title="Example event" { "event": "PAYMENT_PENDING", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-07T12:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "PENDING", "amount": 5000, "currency": "USD", "customFields": { "discord": "johndoe#1234" }, "metadata": { "campaign": "spring_sale" }, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_COMPLETED [#payment_completed] Fired when a payment is successfully collected. **This is the primary event to listen for when fulfilling orders.** The order status will be `PROCESSING` (if fulfillment is in progress) or `COMPLETED` (if already fulfilled). The `items` array contains all purchased products, `customFields` includes any data the customer submitted at checkout, and `metadata` contains any developer-defined key-value pairs set when creating the checkout session. #### Payload [#payload-1] ```json title="Example event" { "event": "PAYMENT_COMPLETED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-07T12:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "COMPLETED", "amount": 5000, "currency": "USD", "customFields": { "discord": "johndoe#1234" }, "metadata": { "campaign": "spring_sale", "ref": "partner_abc" }, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": { "ip": "1.2.3.4", "country": "US", "city": "Miami", "region": "FL" } } } ``` *** ### PAYMENT\_FAILED [#payment_failed] Fired when a payment fails, is canceled by the customer, or expires without being completed. The order is moved to `CANCELLED` status. If a coupon was applied, its usage count is automatically restored. #### Payload [#payload-2] ```json title="Example event" { "event": "PAYMENT_FAILED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-07T12:05:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "CANCELLED", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_REFUNDED [#payment_refunded] Fired when a charge is refunded, whether initiated through the Pandabase dashboard or detected from an external refund. The order status is set to `REFUNDED`. #### Payload [#payload-3] ```json title="Example event" { "event": "PAYMENT_REFUNDED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-09T09:30:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "REFUNDED", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_DISPUTED [#payment_disputed] Fired when a customer opens a chargeback dispute on a payment. The order moves to `CHARGEBACK` status and the disputed amount plus a $20.00 dispute fee is deducted from the store's available balance. #### Payload [#payload-4] ```json title="Example event" { "event": "PAYMENT_DISPUTED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-12T14:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "CHARGEBACK", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_DISPUTE\_WON [#payment_dispute_won] Fired when a dispute is resolved in the merchant's favor. The disputed amount and fee are restored to the store's available balance and the order returns to `COMPLETED` status. #### Payload [#payload-5] ```json title="Example event" { "event": "PAYMENT_DISPUTE_WON", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-20T10:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "COMPLETED", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_DISPUTE\_LOST [#payment_dispute_lost] Fired when a dispute is resolved against the merchant. The deducted funds remain lost and the order stays in `CHARGEBACK` status. #### Payload [#payload-6] ```json title="Example event" { "event": "PAYMENT_DISPUTE_LOST", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-20T10:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "CHARGEBACK", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ### PAYMENT\_DISPUTE\_PREVENTED [#payment_dispute_prevented] Fired when Verifi (Visa RDR) or Ethoca prevents a chargeback before it becomes a formal dispute. This is terminal โ€” no `PAYMENT_DISPUTE_WON` or `PAYMENT_DISPUTE_LOST` will follow. The order moves to `CHARGEBACK` status. The disputed amount plus a **$30.00 prevention fee** is deducted from the store's available balance. Prevented disputes do not count toward your dispute rate. #### Payload [#payload-7] ```json title="Example event" { "event": "PAYMENT_DISPUTE_PREVENTED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-15T08:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "CHARGEBACK", "amount": 5000, "currency": "USD", "customFields": null, "metadata": null, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 5000 } ] }, "customer": {}, "geo": null } } ``` *** ## Subscription events [#subscription-events] *** Subscription events include the same `order`, `customer`, and `geo` fields as payment events, plus a `subscription` object with the current subscription state. They are fired **alongside** (not instead of) payment events โ€” for example, a successful renewal triggers both `PAYMENT_COMPLETED` and `SUBSCRIPTION_RENEWED`. ```typescript title="Subscription type" interface WebhookSubscription { id: string; // sub_ prefixed status: "TRIALING" | "ACTIVE" | "PAST_DUE" | "PAUSED" | "CANCELLED"; billingInterval: "WEEKLY" | "MONTHLY" | "YEARLY"; amount: number; // cents currency: string; currentPeriodStart: string; // ISO 8601 currentPeriodEnd: string; nextChargeAt: string | null; // null when cancelled/paused trialEnd: string | null; cancelledAt: string | null; } ``` The `subscription` field is only present on `SUBSCRIPTION_*` events. Payment events (`PAYMENT_COMPLETED`, etc.) do not include it. ### SUBSCRIPTION\_CREATED [#subscription_created] Fired when a new subscription is activated: * A customer completes checkout for a subscription product (first payment collected), or * A customer starts a free trial (card saved, no charge). The `order` in the payload is the initial order that created the subscription. For trial subscriptions, the initial order has an amount of `$0.00` โ€” the first charge happens when the trial ends. #### Payload [#payload-8] ```json title="Example event" { "event": "SUBSCRIPTION_CREATED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-04-18T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "ACTIVE", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": "2026-05-18T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_RENEWED [#subscription_renewed] Fired when a recurring payment is successfully collected. Sent on every billing cycle โ€” weekly, monthly, or yearly depending on the subscription's billing interval. Use this event to extend access, deliver license keys, or update entitlements. #### Payload [#payload-9] ```json title="Example event" { "event": "SUBSCRIPTION_RENEWED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-04-18T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "ACTIVE", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": "2026-05-18T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_PAST\_DUE [#subscription_past_due] Fired when a renewal payment fails or requires additional authentication (3D Secure). The subscription moves to `PAST_DUE` status. Causes include: * The customer's card was declined * The card requires 3DS verification (customer is emailed an authentication link) * The payment method has expired Failed payments are automatically retried up to 3 times over 3 days (24h, 48h, 72h). If all retries fail, the subscription is cancelled and a `SUBSCRIPTION_CANCELLED` event is fired. Do not revoke access on `SUBSCRIPTION_PAST_DUE` โ€” the customer may still authenticate or the retry may succeed. Wait for `SUBSCRIPTION_CANCELLED` before revoking. #### Payload [#payload-10] ```json title="Example event" { "event": "SUBSCRIPTION_PAST_DUE", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-05-18T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "PAST_DUE", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": "2026-05-19T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_CANCELLED [#subscription_cancelled] Fired when a subscription is cancelled. Causes include: * The merchant cancels the subscription (immediately or at period end) * The customer cancels from the customer portal (always at period end) * All automatic payment retries are exhausted If cancelled at period end, the customer retains access until the current billing period ends. Check the order's metadata or the subscription API for the `endsAt` date. #### Payload [#payload-11] ```json title="Example event" { "event": "SUBSCRIPTION_CANCELLED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-05-21T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "CANCELLED", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": null, "trialEnd": null, "cancelledAt": "2026-05-21T00:00:00.000Z" } } } ``` *** ### SUBSCRIPTION\_PAUSED [#subscription_paused] Fired when a merchant pauses a subscription. No further charges are made until the subscription is resumed. The customer retains access during the pause. #### Payload [#payload-12] ```json title="Example event" { "event": "SUBSCRIPTION_PAUSED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-05-01T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "PAUSED", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": null, "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_RESUMED [#subscription_resumed] Fired when a merchant resumes a previously paused subscription. Billing resumes at the end of the current period (or immediately if the period has already ended). #### Payload [#payload-13] ```json title="Example event" { "event": "SUBSCRIPTION_RESUMED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-05-10T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "ACTIVE", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-04-18T00:00:00.000Z", "currentPeriodEnd": "2026-05-18T00:00:00.000Z", "nextChargeAt": "2026-05-18T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_UPDATED [#subscription_updated] Fired when a subscription is changed โ€” its product, variant, billing interval, or quantity is updated, or the card it bills is changed. For an immediate upgrade that prorates, this fires alongside `PAYMENT_COMPLETED` for the prorated charge; an end-of-period change takes effect at the next renewal. The `subscription` object reflects the new plan. #### Payload [#payload-14] ```json title="Example event" { "event": "SUBSCRIPTION_UPDATED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-06-07T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "ACTIVE", "billingInterval": "YEARLY", "amount": 19999, "currency": "USD", "currentPeriodStart": "2026-06-07T00:00:00.000Z", "currentPeriodEnd": "2027-06-07T00:00:00.000Z", "nextChargeAt": "2027-06-07T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_TRIAL\_ENDING [#subscription_trial_ending] Fired a few days before a free trial ends, so you can remind the customer that billing is about to start. The first charge is attempted when the trial ends (`trialEnd`). This is an advisory event and is not tied to a payment. #### Payload [#payload-15] ```json title="Example event" { "event": "SUBSCRIPTION_TRIAL_ENDING", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-06-07T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "TRIALING", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-06-07T00:00:00.000Z", "currentPeriodEnd": "2026-06-14T00:00:00.000Z", "nextChargeAt": "2026-06-14T00:00:00.000Z", "trialEnd": "2026-06-14T00:00:00.000Z", "cancelledAt": null } } } ``` *** ### SUBSCRIPTION\_RENEWING [#subscription_renewing] Fired a few days before a subscription's next renewal, so you can notify the customer ahead of the charge. For the exact amount โ€” including metered usage and tax โ€” call the [upcoming invoice](/developers/api/store-api-reference/subscriptions) endpoint. This is an advisory event and is not tied to a payment. #### Payload [#payload-16] ```json title="Example event" { "event": "SUBSCRIPTION_RENEWING", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-06-07T00:00:00.000Z", "data": { "order": {}, "customer": {}, "geo": null, "subscription": { "id": "sub_cm5x7k2a000001j0g8h3f9d2e", "status": "ACTIVE", "billingInterval": "MONTHLY", "amount": 1999, "currency": "USD", "currentPeriodStart": "2026-05-10T00:00:00.000Z", "currentPeriodEnd": "2026-06-10T00:00:00.000Z", "nextChargeAt": "2026-06-10T00:00:00.000Z", "trialEnd": null, "cancelledAt": null } } } ``` # Migrate webhook signatures (/developers/webhooks/migrate-signatures) **V1 is being deprecated.** All endpoints must move to V2. New endpoints are V2 by default; existing V1 endpoints will be auto-migrated after a 60-day notice period. Endpoints run in **V1** or **V2**. | Mode | Signature | Status | | ------ | --------------------------------------------------------- | ------------------------------------------------- | | **V2** | [Standard Webhooks](https://www.standardwebhooks.com) v1 | Required. Default for endpoints after 2026-05-15. | | **V1** | Hex over `${timestamp}.${rawBody}` (ms) + `X-Pandabase-*` | Deprecated. Migrate before the cutoff. | Check your mode with `GET /v2/stores/:storeId/webhooks/:webhookId` (`signatureVersion`) or the dashboard. **A Standard Webhooks SDK rejects V1 deliveries.** Both modes use the same `Webhook-*` header names, but V1 uses a hex signature, a different signed string, and a timestamp in **milliseconds**. A V2 verifier expects seconds and rejects that timestamp as outside the 5-minute window. This can appear as a "timestamp" or "stale" error. Confirm `signatureVersion` is `V2` **before** you point a Standard Webhooks SDK (or any V2-only verifier) at the endpoint. Doing it the other way around drops live events until you switch. See [Troubleshooting](#troubleshooting). ### Telling the modes apart on the wire [#telling-the-modes-apart-on-the-wire] Check the `Webhook-Signature` format to identify the mode: | Signature header starts with | Mode | Verify as | | ---------------------------- | ---- | ----------------------------------- | | `v1,` (then base64) | V2 | Standard Webhooks | | bare hex (no prefix) | V1 | [`verifyV1`](#v1-replay-protection) | A receiver that supports both modes can use this prefix to choose a verifier. The authoritative answer for a given endpoint is always its `signatureVersion`. ## V1 โ†’ V2 [#v1--v2] ### Deploy a V2 verifier [#deploy-a-v2-verifier] We require webhook signatures to be verified using the raw request body and your webhook signing secret. You can use the snippets from the verification guide, or install a Standard Webhooks SDK to verify deliveries. Your endpoint is still on **V1** until you complete the next step, so a V2-only verifier will reject **every live delivery** in the gap between deploying it and switching the version. Until you've switched and tested, either keep your existing V1 verifier running, or accept the V2 signature *and* fall back to [`verifyV1`](#v1-replay-protection) when `Webhook-Signature` is bare hex (see [Telling the modes apart](#telling-the-modes-apart-on-the-wire)). High-volume endpoints should not deploy a V2-only verifier first. ### Switch your webhook version [#switch-your-webhook-version] In your merchant dashboard, navigate to: Your Store โ†’ Developers โ†’ Webhooks Open the webhook dropdown menu, click **Edit**, and select the webhook version you want to enable. ### Send a test event [#send-a-test-event] After updating the version, return to: Your Store โ†’ Developers โ†’ Webhooks Open the dropdown menu and click Send test to verify that your endpoint is receiving and validating events correctly. ### Roll back if needed [#roll-back-if-needed] You can revert to a previous webhook version at any time. Go back to Your Store โ†’ Developers โ†’ Webhooks, click Edit, and select the previous version. ### Differences [#differences] | | V1 | V2 | | ----------------------- | ------------------------- | --------------------------------------- | | Encoding | Hex | `v1,` (space-separated list) | | Signed string | `${timestamp}.${rawBody}` | `${Webhook-Id}.${timestamp}.${rawBody}` | | `Webhook-Timestamp` | Milliseconds | Seconds | | `Webhook-Id` | `${webhookId}/${jobId}` | Message id (matches `payload.id`) | | `X-Pandabase-*` headers | Sent | Not sent | ## V1 replay protection [#v1-replay-protection] Moving from `X-Pandabase-*` to the V1 `Webhook-*` headers. V1 endpoints already send `Webhook-*` alongside the legacy headers, so you can move off `X-Pandabase-*` (which has no replay protection) without switching modes yet. These V1 `Webhook-*` headers are **not** Standard Webhooks compatible โ€” same header names, but **hex** encoding (no `v1,` prefix), a `${timestamp}.${rawBody}` signed string, and a **millisecond** timestamp. Verify them with `verifyV1` below, **not** a Standard Webhooks SDK. To use an off-the-shelf SDK, migrate the endpoint to V2 first. | Header | `X-Pandabase-*` | `Webhook-*` (V1) | | ----------------- | ---------------------------- | ---------------------------------- | | Signature | `X-Pandabase-Signature` | `Webhook-Signature` | | Timestamp | `X-Pandabase-Timestamp` (ms) | `Webhook-Timestamp` (ms) | | Idempotency | `X-Pandabase-Idempotency` | `Webhook-Id` | | Signed string | `rawBody` | `${timestamp}.${rawBody}` | | Replay protection | None | Reject events older than 5 minutes | Same secret, same hex encoding. Node.js Python Go ```typescript import crypto from "node:crypto"; const TOLERANCE_MS = 5 * 60 * 1000; export function verifyV1( headers: Record, rawBody: string, secret: string, ): boolean { const ts = headers["webhook-timestamp"]; const sig = headers["webhook-signature"]; if (!ts || !sig) return false; const expected = crypto .createHmac("sha256", secret) .update(`${ts}.${rawBody}`) .digest("hex"); if (expected.length !== sig.length) return false; if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return false; return Math.abs(Date.now() - Number(ts)) <= TOLERANCE_MS; } ``` ```python import hmac, hashlib, time TOLERANCE_MS = 5 * 60 * 1000 def verify_v1(headers, raw_body: bytes, secret: str) -> bool: ts = headers.get("webhook-timestamp") sig = headers.get("webhook-signature") if not ts or not sig: return False expected = hmac.new( secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, sig): return False return abs(int(time.time() * 1000) - int(ts)) <= TOLERANCE_MS ``` ```go package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "time" ) const toleranceMs = 5 * 60 * 1000 func VerifyV1(headers map[string]string, rawBody []byte, secret string) bool { ts := headers["Webhook-Timestamp"] sig := headers["Webhook-Signature"] if ts == "" || sig == "" { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + "." + string(rawBody))) if !hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sig)) { return false } tsInt, err := strconv.ParseInt(ts, 10, 64) if err != nil { return false } skew := time.Now().UnixMilli() - tsInt if skew < 0 { skew = -skew } return skew <= toleranceMs } ``` ## Example deliveries [#example-deliveries] **V2:** ```http POST /webhooks/pandabase HTTP/1.1 Webhook-Id: evt_cm5x7k2a000001j0g8h3f9d2e Webhook-Timestamp: 1715688123 Webhook-Signature: v1,7ZH9F8sZqxQk6vJtN5cBp0LmRwXyP3aT8eK1nDhU2gI= { "event": "PAYMENT_COMPLETED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", ... } ``` **V1:** ```http POST /webhooks/pandabase HTTP/1.1 Webhook-Id: whk_abc/job_xyz Webhook-Timestamp: 1715688123456 Webhook-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 X-Pandabase-Idempotency: whk_abc/job_xyz X-Pandabase-Timestamp: 1715688123456 X-Pandabase-Signature: 0a4d55a8d778e5022fab701977c5d840bbc486d0 { "event": "PAYMENT_COMPLETED", ... } ``` ## Pitfalls [#pitfalls] * Hash the **raw** body, not a re-serialized JSON object. * Use `timingSafeEqual` / `hmac.compare_digest`. Never `==`. * Reject stale timestamps. The signature alone doesn't stop replay. * V2 timestamps are **seconds**, V1 are **milliseconds**. * V2 `Webhook-Signature` is a space-separated list โ€” check each `v1,โ€ฆ` entry. * Keep your server NTP-synced. >5 min drift rejects valid events. ## Troubleshooting [#troubleshooting] Check your endpoint's `signatureVersion`. Standard Webhooks SDKs expect V2, but V1 uses a hex signature, a different signed string, and a timestamp in milliseconds. These differences cause verification to fail. Switch the endpoint to V2, or use [`verifyV1`](#v1-replay-protection) until you migrate. You likely have a verifier hard-coded to one mode while the endpoint's `signatureVersion` changed (or you're mid-rollout across machines). Branch on the `Webhook-Signature` prefix โ€” `v1,` is V2, bare hex is V1 โ€” or pin both sides to the same mode. See [Telling the modes apart](#telling-the-modes-apart-on-the-wire). Check whether your handler is hashing a re-serialized body. Hash the **raw** bytes exactly as received โ€” JSON re-encoding changes whitespace and key order, which changes the signature. In Express, use `express.raw({ type: "application/json" })`. ## FAQ [#faq] Yes. V1 is being deprecated. Migrate before the cutoff or your endpoint will be auto-migrated. After a 60-day notice period, emailed to your store contact. The deprecation date will be announced in the changelog. Yes. Yes during migration. `signatureVersion` is per-endpoint. # Overview (/developers/webhooks/overview) Pandabase sends each event to your webhook endpoint using an HTTP `POST` request. Use webhooks to fulfill orders, update subscriptions, or respond to other store activity. See the [event reference](/developers/webhooks/events) for the full list of supported events. ## Payload [#payload] ```json { "event": "PAYMENT_COMPLETED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", "timestamp": "2026-03-07T12:00:00.000Z", "data": { "order": { "id": "ord_cm5x7k2a000001j0g8h3f9d2e", "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e", "status": "COMPLETED", "amount": 2999, "currency": "USD", "customFields": { "discord": "johndoe#1234" }, "metadata": { "campaign": "spring_sale", "ref": "partner_abc" }, "items": [ { "productId": "prd_cm5x7k2a000001j0g8h3f9d2e", "variantId": null, "name": "Pro Plan", "quantity": 1, "amount": 2999 } ] }, "customer": { "id": "cus_cm5x7k2a000001j0g8h3f9d2e", "email": "buyer@example.com" }, "geo": { "ip": "1.2.3.4", "country": "US", "city": "Miami", "region": "FL" } } } ``` Amounts are integers in **cents**. ## Signature modes [#signature-modes] | Mode | Format | Status | | ------ | -------------------------------------------------------- | --------------------------------------------------------------- | | **V2** | [Standard Webhooks](https://www.standardwebhooks.com) v1 | Required. Default for new endpoints. | | **V1** | Hex `Webhook-Signature` + legacy `X-Pandabase-*` | Deprecated. [Migrate](/developers/webhooks/migrate-signatures). | This page documents V2. The verifiers below โ€” including any Standard Webhooks SDK โ€” only validate **V2** endpoints. V1 sends the same `Webhook-*` header names with hex/millisecond values, so a V2 verifier rejects every V1 delivery. Check your endpoint's `signatureVersion` first; if it's V1, follow the [migration guide](/developers/webhooks/migrate-signatures). ## Headers [#headers] | Header | Value | | ------------------- | ----------------------------------------------------------------------------- | | `Webhook-Id` | Message id. Matches `payload.id`. Deduplicate on this. | | `Webhook-Timestamp` | Unix timestamp in **seconds**. Reject if older than 5 minutes. | | `Webhook-Signature` | `v1,` HMAC-SHA256 of `${Webhook-Id}.${Webhook-Timestamp}.${rawBody}`. | ## Retries [#retries] 2xx is success. Anything else โ€” non-2xx, 15s timeout, connection error โ€” retries per the [Standard Webhooks schedule](https://www.standardwebhooks.com/#retries) over \~75 hours, ยฑ10% jitter. | Attempt | Delay | Elapsed | | ------- | ----- | -------- | | 1 | โ€” | 00:00:00 | | 2 | 5s | 00:00:05 | | 3 | 5m | 00:05:05 | | 4 | 30m | 00:35:05 | | 5 | 2h | 02:35:05 | | 6 | 5h | 07:35:05 | | 7 | 10h | 17:35:05 | | 8 | 14h | 31:35:05 | | 9 | 20h | 51:35:05 | | 10 | 24h | 75:35:05 | If all 10 attempts fail, the store owner receives an email, limited to one per endpoint every 6 hours. ## Verification [#verification] Hash the **raw** request body. Whitespace and key order change the bytes. ### SDK [#sdk] Node.js Python Go ```typescript import { Webhook } from "standardwebhooks"; const wh = new Webhook(process.env.WEBHOOK_SECRET!); app.post( "/webhooks/pandabase", express.raw({ type: "application/json" }), (req, res) => { try { const event = wh.verify(req.body.toString(), req.headers as any); // event is the parsed JSON body, signature already verified switch (event.event) { case "PAYMENT_COMPLETED": console.log(`Payment completed for order ${event.data.order.id}`); break; case "PAYMENT_REFUNDED": console.log(`Refund issued for order ${event.data.order.id}`); break; } res.status(200).send("OK"); } catch { res.status(401).send("Invalid signature"); } }, ); ``` ```python from standardwebhooks import Webhook from flask import Flask, request, jsonify app = Flask(__name__) wh = Webhook(WEBHOOK_SECRET) @app.route("/webhooks/pandabase", methods=["POST"]) def handle_webhook(): try: event = wh.verify(request.data, dict(request.headers)) except Exception: return jsonify(error="Invalid signature"), 401 if event["event"] == "PAYMENT_COMPLETED": print(f"Payment completed for order {event['data']['order']['id']}") return jsonify(message="OK"), 200 ``` ```go import standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go" wh, _ := standardwebhooks.NewWebhook(webhookSecret) func webhookHandler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := wh.Verify(body, r.Header); err != nil { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // body is verified โ€” parse and dispatch w.WriteHeader(http.StatusOK) } ``` ### Manual [#manual] Node.js Python Go ```typescript import crypto from "node:crypto"; const TOLERANCE_MS = 5 * 60 * 1000; function verifyWebhook( headers: Record, rawBody: string, secret: string, ): boolean { const msgId = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatureHeader = headers["webhook-signature"]; if (!msgId || !timestamp || !signatureHeader) return false; // Webhook-Signature is a space-separated list of `vN,` entries. // We only need to match one. const signatures = signatureHeader .split(" ") .filter((s) => s.startsWith("v1,")) .map((s) => s.slice(3)); const signedPayload = `${msgId}.${timestamp}.${rawBody}`; const expected = crypto .createHmac("sha256", secret) .update(signedPayload) .digest("base64"); const valid = signatures.some( (sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)), ); if (!valid) return false; const skewMs = Math.abs(Date.now() - Number(timestamp) * 1000); return skewMs <= TOLERANCE_MS; } ``` ```python import hmac, hashlib, base64, time TOLERANCE_MS = 5 * 60 * 1000 def verify(headers, raw_body: bytes, secret: str) -> bool: msg_id = headers.get("webhook-id") timestamp = headers.get("webhook-timestamp") sig_header = headers.get("webhook-signature") if not msg_id or not timestamp or not sig_header: return False signed_payload = f"{msg_id}.{timestamp}.{raw_body.decode()}".encode() expected = base64.b64encode( hmac.new(secret.encode(), signed_payload, hashlib.sha256).digest() ).decode() sigs = [s[3:] for s in sig_header.split(" ") if s.startswith("v1,")] if not any(hmac.compare_digest(s, expected) for s in sigs): return False skew_ms = abs(int(time.time() * 1000) - int(timestamp) * 1000) return skew_ms <= TOLERANCE_MS ``` ```go package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "net/http" "strconv" "strings" "time" ) const toleranceMs = 5 * 60 * 1000 func Verify(h http.Header, rawBody []byte, secret string) bool { msgID := h.Get("Webhook-Id") ts := h.Get("Webhook-Timestamp") sigHeader := h.Get("Webhook-Signature") if msgID == "" || ts == "" || sigHeader == "" { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(msgID + "." + ts + "." + string(rawBody))) expected := base64.StdEncoding.EncodeToString(mac.Sum(nil)) matched := false for _, entry := range strings.Split(sigHeader, " ") { if !strings.HasPrefix(entry, "v1,") { continue } if hmac.Equal([]byte(entry[3:]), []byte(expected)) { matched = true break } } if !matched { return false } tsSec, err := strconv.ParseInt(ts, 10, 64) if err != nil { return false } skew := time.Now().UnixMilli() - tsSec*1000 if skew < 0 { skew = -skew } return skew <= toleranceMs } ``` ## Best practices [#best-practices] * Return a 2xx response promptly, then process the event asynchronously. * Verify every request. Reject stale timestamps. * Deduplicate on `Webhook-Id`. * Subscribe only to events you need. * Don't rely on delivery order โ€” use the order `status` field. # Getting Started (/developers/websdk/getting-started) The WebSDK is in **Private Preview**. The API may change before general availability. Contact support to request access. The WebSDK is a low-level alternative to the Checkout SDK. Use it when you want to build a fully custom checkout UI in the browser instead of redirecting to or embedding an iframe of the hosted checkout page. ## Installation [#installation] npm pnpm bun ```sh npm install @pandabase/websdk # for React apps: npm install @pandabase/react-websdk ``` ```sh pnpm add @pandabase/websdk # for React apps: pnpm add @pandabase/react-websdk ``` ```sh bun add @pandabase/websdk # for React apps: bun add @pandabase/react-websdk ``` For PCI compliance, the WebSDK runtime is always loaded from our CDN at `https://js.pandabase.io/websdk-production.js` โ€” the npm package is a thin loader that fetches the same script. You can also include it directly: ```html ``` Never self-host, bundle, or proxy the script. Doing so takes your application out of PCI SAQ-A scope. For usage instructions, contact [support@pandabase.io](mailto:support@pandabase.io). # Analytics (/store/analytics) Your analytics dashboard tracks gross volume, net revenue, refund and dispute rates, and orders by country in real time. No setup is required. Store analytics dashboard Use the date range selector in the top right to switch between **Last 7 days**, **Last 30 days**, **Last 90 days**, or a custom window. ## Gross volume [#gross-volume] The main chart shows total payment volume before deductions for the selected period. Fees and refunds appear underneath. ## Summary [#summary] The summary card on the right shows: | Metric | What it measures | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **Net Revenue** | Gross volume minus fees and refunds, alongside total order count. | | **Balance** | Currently available balance plus the amount still in the pending window. | | **Refund Rate** | Refunded amount as a percentage of gross volume, with refund count. | | **Dispute Rate** | Disputed amount as a percentage of gross volume. Shows "insufficient data" until you have enough volume for the rate to be meaningful. | ## Orders by country [#orders-by-country] See your top countries by order volume for the selected period. Use this to decide which local payment methods and currencies to offer. ## Coming soon [#coming-soon] These cards will show data when the features are released: * **Disputes.** Track and respond to chargebacks directly from the dashboard. * **Top Payment Methods.** See which methods (card, Apple Pay, Google Pay, local methods) your customers actually use. # Cases (/store/cases) A **case** is a customer support ticket for a completed order. Customers can report a missing license, broken download, or subscription issue, or ask about a refund. Customers open cases from the **Pandabase customer portal** at [mypandabase.com](https://mypandabase.com) on any of their completed orders. You receive an email and a dashboard notification as soon as one is opened. Cases list view with Awaiting you, Open, Awaiting customer, Closed, and All tabs ## States [#states] | State | Meaning | | --------------------- | ------------------------------------------------------ | | **Awaiting you** | Customer has replied and is waiting for your response. | | **Open** | Case is active. Either side may reply next. | | **Awaiting customer** | You've replied; waiting for the customer to come back. | | **Closed** | Case is resolved. Either party can reopen. | The tabs at the top of the list filter by state. Check **Awaiting you** for cases that need a reply. ## Priority [#priority] Each case has a priority โ€” **Low**, **Normal**, **High**, or **Urgent**. Customers set the initial priority when they open the case; you can change it from the dropdown at the top of the detail view. Use priority to decide which cases to handle first. It doesn't affect SLAs or visibility to customers. ## Replying [#replying] Case detail view with conversation, reply box, customer info, and identifiers Open a case from the list and use the **Reply** box at the bottom. You can write up to **4,000 characters** and attach up to **5 files** per message. Allowed file types: images (`image/*`), PDF, plain text, and ZIP archives. Sending a reply automatically: * Emails the customer with your message * Moves the case to **Awaiting customer** * Updates the timeline The sidebar shows the customer's name and email, case timestamps, the related order with its current status and amount, and internal identifiers for support tickets. ## Closing and reopening [#closing-and-reopening] Use the **Status** dropdown at the top of the detail view to close a case once it's resolved. Closed cases stay searchable and can be reopened by either party โ€” clicking **Reopen case** from the dashboard, or the customer replying from the portal. ### Auto-close [#auto-close] Cases that sit in **Awaiting customer** without activity automatically close after a configurable idle period (default 14 days). The customer is emailed beforehand so they can reply if they still need help. ### Auto-reply [#auto-reply] Set an **auto-reply** under **Settings โ†’ Cases** to tell customers when to expect a response or link to known issues. It is sent when a customer opens a case. ## Pandabase support intervention [#pandabase-support-intervention] Pandabase support can join cases involving disputes, refund disagreements, or fraud claims. Messages are labeled **Pandabase Support** and visible to you and the customer. You don't need to invite us. We intervene when our automated signals (dispute risk, EFW match, repeated unresolved cases) suggest it would help. ## Attachments [#attachments] Customers can attach files to their messages too โ€” most commonly screenshots of an error or the receipt they received. Attachments use short-lived URLs and aren't publicly indexable. The allowlist is the same as your replies: `image/*`, `application/pdf`, `text/plain`, `application/zip`. ## Programmatic access [#programmatic-access] Cases are exposed via the [Store API](/developers/api/introduction) under the `CASES_READ` and `CASES_WRITE` token scopes, and through the Pandabase MCP server. List, fetch, reply, mark read, and update status โ€” all available without the dashboard. # Bank accounts (/store/finances/bank-accounts) Pandabase pays out to the bank accounts you connect under **Finances โ†’ Bank Accounts**. You can have multiple accounts on file and mark one as **Primary** to use by default. Bank Accounts page in the merchant dashboard ## Adding an account [#adding-an-account] Click **+ Add Account** and fill in your bank details. The form adapts to your store's country โ€” US merchants enter an ABA routing number; UK accounts use sort codes; EU/EEA accounts use IBANs; and so on. Add US Bank Account panel The bank account **must belong to the verified account holder** for your Pandabase account, in the country you're verified in. Payouts to mismatched accounts will fail and the funds will be returned, but the process can take days. The bank account's country **must match the country of the ID** you verified with. For example, if you verified with a US ID, you can only connect a US bank account. ### Fields by country [#fields-by-country] | Country | What you need | | --------- | -------------------------------------------------------- | | US | Account number, 9-digit ABA routing number, account type | | UK | Account number, 6-digit sort code | | EU / EEA | IBAN | | Canada | Account number, transit number, institution number | | Singapore | Account number, bank code, branch code | | Turkiye | IBAN, SWIFT/BIC code | | Other | IBAN or local equivalent โ€” SWIFT for international rails | Select **Set as default payout account** to make this your primary account. The previous account stays connected. ## Verification [#verification] New accounts enter an **In Review** state for **2 to 3 business days** while we verify the details with your bank and run compliance checks. You can't request payouts to an account during the review. Once approved, the status changes to **Approved** and the account is eligible to receive payouts. ### First payout waiting period [#first-payout-waiting-period] Your **first payout** to any newly approved account has a **7 to 14 day waiting period** before settlement. This hold reduces payout risk and applies to every new account, including replacements. ## Managing existing accounts [#managing-existing-accounts] From the row's options menu you can: * **Set as primary** โ€” promote a different account to default. * **Edit** โ€” update non-identifying fields (label, preferences). Account/routing numbers can't be edited; remove and re-add to change them. * **Remove** โ€” disconnect the account. Pending payouts to that account aren't affected; new requests can't target it. You always need at least one approved account on file to receive payouts. ## Currencies [#currencies] Each bank account has a settlement currency tied to its country โ€” USD for US, GBP for UK, EUR for EU/EEA, and so on. Your store's balance is in USD; payouts are converted to the destination currency at request time, and the FX rate is included in the fee breakdown shown before you confirm. # Payouts (/store/finances/payouts) Pandabase pays out directly to your bank account. Once your identity is verified and a bank account is added, you can request payouts from your store's available balance. Payouts list view in the merchant dashboard Payouts are processed in batches on **Monday, Wednesday, and Friday**. Requests submitted between batches queue up and go out in the next batch. Some payouts are approved immediately. Others are processed in the next scheduled batch. Queued requests are processed automatically. ## Minimum [#minimum] The minimum payout amount is **$50.00** in all countries. ## Fees [#fees] Fees are calculated **at the time you request a payout** and depend on your bank's country, currency, and the rails used. Every payout has a $1.50 base fee; non-US destinations also incur cross-border and currency conversion fees. The exact fee breakdown is shown in the **Request Payout** panel before you confirm โ€” including what you receive, the base fee, and the total deducted from your balance. Request payout panel showing amount, destination bank account, and fee summary ## Supported countries [#supported-countries] Payouts are available everywhere we support stores. Local rails (ACH, SEPA, BECS, FPS, NEFT/IMPS) are used where available; remaining destinations settle over SWIFT. **Americas:** ๐Ÿ‡จ๐Ÿ‡ฆ Canada, ๐Ÿ‡บ๐Ÿ‡ธ United States **Europe:** ๐Ÿ‡ฆ๐Ÿ‡น Austria, ๐Ÿ‡ง๐Ÿ‡ช Belgium, ๐Ÿ‡ญ๐Ÿ‡ท Croatia, ๐Ÿ‡จ๐Ÿ‡พ Cyprus, ๐Ÿ‡จ๐Ÿ‡ฟ Czech Republic, ๐Ÿ‡ฉ๐Ÿ‡ฐ Denmark, ๐Ÿ‡ช๐Ÿ‡ช Estonia, ๐Ÿ‡ซ๐Ÿ‡ฎ Finland, ๐Ÿ‡ซ๐Ÿ‡ท France, ๐Ÿ‡ฉ๐Ÿ‡ช Germany, ๐Ÿ‡ฌ๐Ÿ‡ง Great Britain, ๐Ÿ‡ฌ๐Ÿ‡ท Greece, ๐Ÿ‡ญ๐Ÿ‡บ Hungary, ๐Ÿ‡ฎ๐Ÿ‡ช Ireland, ๐Ÿ‡ฎ๐Ÿ‡น Italy, ๐Ÿ‡ฑ๐Ÿ‡ป Latvia, ๐Ÿ‡ฑ๐Ÿ‡น Lithuania, ๐Ÿ‡ฑ๐Ÿ‡บ Luxembourg, ๐Ÿ‡ฒ๐Ÿ‡น Malta, ๐Ÿ‡ณ๐Ÿ‡ฑ Netherlands, ๐Ÿ‡ณ๐Ÿ‡ด Norway, ๐Ÿ‡ต๐Ÿ‡ฑ Poland, ๐Ÿ‡ต๐Ÿ‡น Portugal, ๐Ÿ‡ท๐Ÿ‡ด Romania, ๐Ÿ‡ธ๐Ÿ‡ฐ Slovakia, ๐Ÿ‡ธ๐Ÿ‡ฎ Slovenia, ๐Ÿ‡ช๐Ÿ‡ธ Spain, ๐Ÿ‡ธ๐Ÿ‡ช Sweden, ๐Ÿ‡จ๐Ÿ‡ญ Switzerland, ๐Ÿ‡น๐Ÿ‡ท Turkiye **Asia-Pacific:** ๐Ÿ‡ฆ๐Ÿ‡บ Australia, ๐Ÿ‡ญ๐Ÿ‡ฐ Hong Kong, ๐Ÿ‡ฎ๐Ÿ‡ณ India, ๐Ÿ‡ณ๐Ÿ‡ฟ New Zealand, ๐Ÿ‡ต๐Ÿ‡ญ Philippines, ๐Ÿ‡ธ๐Ÿ‡ฌ Singapore, ๐Ÿ‡น๐Ÿ‡ผ Taiwan, ๐Ÿ‡น๐Ÿ‡ญ Thailand **Middle East:** ๐Ÿ‡ฎ๐Ÿ‡ฑ Israel, ๐Ÿ‡ถ๐Ÿ‡ฆ Qatar, ๐Ÿ‡ฆ๐Ÿ‡ช United Arab Emirates **Africa:** ๐Ÿ‡ฒ๐Ÿ‡ฆ Morocco ## New bank accounts [#new-bank-accounts] A new bank account enters an **In Review** state for 2 to 3 business days while we verify the details and run compliance checks. You can't request payouts during the review. We'll email you once it's approved. After approval, your **first payout** has a 7 to 14 day waiting period before settlement, for risk mitigation. This applies to every new bank account, including replacement accounts after you change details. ## Settlement times [#settlement-times] Subsequent payouts settle within **1 to 5 business days**, depending on your country. Local transfers typically clear in 1 to 2 business days; international wires take 3 to 5. Settlement times vary. Bank holidays, compliance reviews, and bank processing schedules can delay a payout. Individual payout requests may be held for review. You'll receive an email with the status of your request. ## Failed or returned payouts [#failed-or-returned-payouts] If a payout is rejected by your bank (wrong details, closed account, compliance hold on the receiving side), our reconciliation and batch system **automatically returns the funds to your available balance within 15 days**. In some cases it can take up to **30 days**. The funds are returned automatically. Once the funds are back in your balance, fix the bank details and request a new payout. ## Payout hasn't arrived? [#payout-hasnt-arrived] Wait at least **5 business days** from when the payout was marked as paid. Banks sometimes hold incoming funds even after the payout shows **Completed** in your Dashboard, and the deposit usually arrives within this window. If it still hasn't arrived, contact your bank first โ€” they have the most direct view of incoming transfers. Ask them to trace the transfer using these details: * Trace ID * Amount * Date the payment was initiated * Your bank's name * Last four digits of your account number If your bank can't locate the funds, ask them for a written statement confirming this and send it to support with the payout ID. We'll then investigate further with our payout provider. # Reports (/store/finances/reports) Export your store's activity as CSV or JSON for accounting, reconciliation, and audits. Reports run in the background, and we'll email you when they're ready. Files are deleted after 30 days. ## When to use reports [#when-to-use-reports] * **Month-end or quarter-end bookkeeping.** Pull a payment activity report for the period and hand it to your accountant. * **Reconciling a payout against the underlying transactions.** Run a payout reconciliation report to see exactly which orders, refunds, and disputes are bundled into each payout. * **Auditing your balance.** Run a balance ledger report to see every credit and debit on your store, including a tamper-evident hash chain. * **Filing taxes.** Payment activity includes the tax we collected and remitted on your behalf, broken down per transaction. ## Available reports [#available-reports] One row per payment we processed. Includes the order number, status, currency, intended amount, discounts, tax, platform and processing fees, settled amount, presentment currency and exchange rate, plus the customer's email, name, country, and IP. Use this to review revenue or share payment records with your accountant. One row per payout request. Includes the payout ID, status, amount, fees, net amount, the bank account it landed in, our internal risk score, and the PSP reference ID for tracing the transfer at your bank. Use this when a payout shows up in your bank account and you want to verify it matches what we sent. One row for every credit and debit on your store: payments received, refunds, disputes, payouts, and adjustments. Each row carries the resulting balance, the affected order, payment, refund, dispute, or payout IDs, a short note, and a SHA-256 hash chain so an auditor can verify nothing has been tampered with. Use this to audit changes to your balance. Combined export of refunds and chargebacks for the period. One row per event with a `kind` column (`REFUND` or `DISPUTE`), the related order number, amount, fee, status, and reason. Use this for chargeback rate analysis and refund pattern reviews. ## Generating a report [#generating-a-report] ### Open the Reports page [#open-the-reports-page] From the dashboard sidebar, go to **Finances โ†’ Reports**. ### Click New Report [#click-new-report] Pick the report type, period, and output format(s). ### Wait for it to finish [#wait-for-it-to-finish] The status column shows **Generating** while the report is in progress, then changes to **Ready** when the file is available. Most reports finish in seconds; larger periods can take a few minutes. You can leave the page. We'll email you when it's done. ### Download [#download] Click the **Download** button in the Actions column to save the file. The reports table shows status, type, period, row count, file size, and how long ago the report was requested. Use the tabs at the top to filter by **All / Ready / Generating / Failed**. ## Period and limits [#period-and-limits] * **Maximum period:** 90 days per report. To export a longer window, run several reports back-to-back (e.g. one per month for a year). * **Concurrent reports:** up to 10 reports can be running at a time per store. If you queue an 11th, you'll see a message asking you to wait for the current batch to finish. * **Retention:** 30 days from the time you requested it. After that, the file is deleted automatically. Re-run if you need it again. ## Output format [#output-format] ### CSV [#csv] A standard comma-separated file with a UTF-8 byte-order mark so Excel, Numbers, and Google Sheets detect the encoding correctly. The first row is a header; every subsequent row is a record. Amounts are stored in the smallest currency unit (cents for USD/EUR/GBP, whole yen for JPY, etc.). Divide by 100 to get the major-unit display value for most currencies. ### JSON [#json] The same data as a JSON array of objects, for use in scripts, data warehouses, or reporting tools. ## Email notifications [#email-notifications] We send an email when a report finishes, whether it succeeded or failed, to the email address on your account. ## Troubleshooting [#troubleshooting] ### My download link doesn't work [#my-download-link-doesnt-work] Download links are signed for security and expire after 5 minutes. Open the report from the dashboard again and click Download to generate a fresh link. ### My report says Failed [#my-report-says-failed] We retry a few times before marking a report as failed. When it does fail, the error message in the dashboard explains why (most commonly a transient infrastructure issue). Click **Retry** to re-queue with the same parameters. If a report fails repeatedly with the same error, [reach out to support](mailto:support@pandabase.io) with the report ID. ### My report doesn't include a recent transaction [#my-report-doesnt-include-a-recent-transaction] Reports include any record whose `created_at` falls in the period you selected. If a transaction completed at 23:59 UTC on the last day of your period, it's included; one at 00:01 UTC the next day is not. Periods are interpreted in UTC. ### Can I schedule reports to run automatically? [#can-i-schedule-reports-to-run-automatically] Not yet. Every report is generated on demand. If you need a recurring export, ask your developer to run [the Reports API](/developers/learn/reports) on a cron job. ## API access [#api-access] Everything described above is also available programmatically. See [Reports (Developer guide)](/developers/learn/reports). # Disputes (/store/manage-payments/disputes) A dispute (also called a chargeback) is when a customer asks their bank to reverse a payment instead of asking you for a refund. The bank takes the money back, and you get a chance to prove the charge was valid. As the Merchant of Record, Pandabase handles the dispute with the card network and passes it to you so you can respond. You have **7 days** to send your evidence once a dispute opens. We'll email you when a dispute is filed. Disputes list view with Needs Response, Under Review, Won, and Lost tabs ## Lifecycle [#lifecycle] ### Opened [#opened] The order moves to `Chargeback`. The disputed amount plus a **$20.00 dispute fee** is held from your balance until it's resolved. ### Evidence [#evidence] You submit your evidence from the dashboard within 7 days. We pass it on to the card network for you. ### Under review [#under-review] The customer's bank reviews the evidence. This usually takes **30 to 75 days**. ### Resolved [#resolved] **You win** โ€” the disputed amount comes back and the order returns to `Completed`. The **$20 dispute fee is not refunded**. **You lose** โ€” the money stays with the customer. ## Reasons [#reasons] | Reason | What it means | | ---------------------- | --------------------------------------------------------- | | Fraudulent | The cardholder claims they didn't authorize the charge. | | Product not received | Customer says they never got what they paid for. | | Product unacceptable | Received but doesn't match the description. | | Subscription cancelled | Customer says they cancelled but were billed anyway. | | Duplicate | Customer believes they were charged twice. | | Credit not processed | You promised a refund and the customer never received it. | | General | No specific reason given by the issuing bank. | ## Fees [#fees] | Outcome | Effect | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Dispute opened | Disputed amount + $20.00 fee deducted from your available balance. | | Won | Disputed amount restored to your balance. The $20.00 dispute fee is **not** refunded. | | Lost | Deduction stands. The $20.00 dispute fee and the original processing and platform fees are not refunded. | | Prevented | Disputed amount + **$30.00** prevention fee deducted. Terminal, doesn't affect your rate. See [Prevented disputes](#prevented-disputes). | ## Dispute rate [#dispute-rate] Your dispute rate is the share of your payments that get disputed, measured over the last **3 months**. Keep it **below 0.75%**. If it stays above 0.75%, we'll contact you and review new payments more closely. Card networks can restrict or stop card payments if the rate remains high. You can check your current rate on the [Analytics](/store/analytics) page. ## Respond [#respond] Open the disputed order from the **Disputes** page. The detail view shows the dispute reason, disputed amount, dispute fee, total impact on your balance, the original order items, customer info, and the full payment timeline. Dispute detail page showing reason, amounts, items, and timeline Click **Submit Evidence** to open the response form. Submit Evidence dialog with Activity Log text field Pandabase **automatically includes** the customer's name, email, IP address, and billing address in the evidence package. You provide the **Activity Log** โ€” a written description of how the customer used what they paid for. Up to 20,000 characters. Be specific: * Login timestamps, session durations, IP addresses they accessed your platform from * Downloads, license activations, API calls, content viewed * Delivery confirmations, support tickets, emails or chats with the customer * For "product unacceptable", what was delivered and how it matched the description * For "subscription cancelled", proof of continued usage after the alleged cancellation date You can submit evidence once. Gather everything before submitting. ## Accepting a dispute [#accepting-a-dispute] To accept a dispute, leave it without submitting evidence. There is no "Accept" button. After 7 days, the chargeback is finalized and the disputed amount and fee remain deducted. ## Prevention [#prevention] Pandabase prevents most disputes before they reach you. All of the following run automatically โ€” there's nothing to configure. ### Always-on 3D Secure [#always-on-3d-secure] Every card payment is authenticated with **3D Secure 2** at checkout. If the cardholder can't authenticate, the transaction is rejected. Successful 3DS shifts liability for fraud disputes from you to the issuing bank in nearly every region we operate. ### Order Insight & Consumer Clarity [#order-insight--consumer-clarity] When a cardholder questions a charge with their bank, the bank receives order details (product, merchant name, billing address, fulfillment info) directly from Pandabase. Most "I don't recognize this charge" disputes resolve here. ### Early Fraud Warnings [#early-fraud-warnings] Sometimes the card network tells us a payment is **likely to be disputed** before the chargeback is actually filed. These warnings are **free** โ€” there's no fee to receive one. * **Orders โ‰ค $50** โ€” refunded automatically. No impact on your dispute rate, fulfillment revoked. * **Orders > $50** โ€” you decide whether to refund pre-emptively or contest. We email you with the EFW reason. Acting on an EFW keeps the chargeback off your dispute rate entirely โ€” which is what matters most once you're approaching the 0.75% threshold. ### Prevented disputes [#prevented-disputes] If a chargeback is filed without an earlier EFW, Verifi's **Rapid Dispute Resolution (RDR)** and **Ethoca Alerts** can still catch it and resolve it with the issuing bank before it becomes a formal dispute. The cardholder is refunded by the network. Unlike an EFW, you don't get a chance to contest โ€” the network intercepts after the dispute is filed. The disputed amount plus a **$30 prevention fee** is deducted from your balance, the order moves to `Chargeback`, and the dispute appears with status **Prevented**. Prevented disputes require no evidence and have no won or lost outcome. They have **no impact on your dispute rate** or VDMP/ECM ratio. ### Compelling Evidence 3.0 [#compelling-evidence-30] For fraud-coded disputes, Pandabase automatically submits a **CE 3.0** package when the data supports it. CE 3.0 lets merchants defeat fraud disputes by proving a prior good relationship โ€” we look for **two or more prior undisputed transactions** from the same cardholder with matching IP, device fingerprint, billing address, or customer account. CE 3.0 has a high win rate but doesn't apply to every dispute. Disputes where the network confirms the card was used without authorization will be lost regardless of any evidence submitted. ## What you can do [#what-you-can-do] * Use a clear store name and product descriptions. Most "fraudulent" disputes are customers not recognizing the charge. * Make refunds easy. Most disputes start because the customer couldn't get a response from you. * Send fulfillment confirmations promptly โ€” receipt and access details in the same email. # Fraud (/store/manage-payments/fraud) Pandabase checks payments for card testing, stolen cards, and account takeover to reduce fraud and disputes. ## Fraud checks [#fraud-checks] ### Device reputation [#device-reputation] When a customer reaches checkout, we analyze their device and assign a device ID and a risk score. A fingerprint tied to a previous dispute starts with a higher score, so repeat offenders are flagged before they pay. ### Dynamic 3D Secure [#dynamic-3d-secure] When a card payment begins, we apply [3D Secure](/store/manage-payments/disputes#always-on-3d-secure) dynamically based on the card's risk score. Higher-risk cards face a full authentication challenge; trusted ones pass through frictionlessly. ### Rules & human review [#rules--human-review] We combine signals from the underlying payment processor with our own fraud-detection toolkit โ€” hundreds of custom rules backed by human review. When a rule is triggered, the payment is routed to manual review, where we accept or refund it. ### Early fraud warnings [#early-fraud-warnings] Through partner integrations with processors and the card networks, we receive early fraud warnings (EFWs) on payments likely to be disputed. Because most flagged payments end in a chargeback, we refund them automatically. ## Dispute prevention [#dispute-prevention] Beyond detection, our integrations with **Verifi** (Visa) and **Ethoca** (Mastercard) intercept disputes before they become formal chargebacks. A fee applies to each prevented dispute. See [Disputes โ†’ Prevention](/store/manage-payments/disputes#prevention) for the full breakdown of fees and outcomes. ## What you can do [#what-you-can-do] To help reduce disputes: * Use a clear store name and product descriptions โ€” most "fraudulent" disputes are customers not recognizing the charge. * Send fulfillment confirmations, with receipt and access details in the same email. With these in place, most of our merchants keep dispute rates well under **0.05%**. ## FAQ [#faq] We received an **early fraud warning** for that payment from the card network. An EFW means the issuing bank has flagged the charge as likely fraudulent โ€” and once flagged, the vast majority go on to become chargebacks. Refunding immediately avoids the dispute, the dispute fee, and any impact on your dispute rate. Fulfillment is revoked at the same time. See [Early Fraud Warnings](/store/manage-payments/disputes#early-fraud-warnings). That's a **prevented dispute**. A chargeback was filed without an earlier warning, and Verifi or Ethoca intercepted it and resolved it directly with the issuing bank before it became a formal dispute. The customer is refunded by the network, the disputed amount plus a **$30 prevention fee** is deducted from your balance, and the order moves to `Chargeback` with status **Prevented**. You don't need to submit evidence, and there is no won or lost outcome. Prevented disputes have **no impact on your dispute rate**. See [Prevented disputes](/store/manage-payments/disputes#prevented-disputes). # Payments (/store/manage-payments/payments) Pandabase processes payments as the **Merchant of Record (MoR)**: we charge the customer, collect tax, handle disputes, and issue refunds on your behalf. After fees, we settle the net to your store's available balance in **USD**. ## Currency [#currency] Customers pay in their local currency wherever supported. This unlocks region-specific payment methods (iDEAL, BLIK, PIX, UPI, etc.) and gives them a familiar checkout. Exchange rates are locked at checkout time and the conversion happens on our side, so you always receive USD. Final charge amounts may vary slightly due to FX fluctuations between checkout and settlement. Your USD settlement is unaffected. ## Payment methods [#payment-methods] Global methods (cards, Apple Pay, Google Pay) work in every supported currency. Local methods appear automatically when the checkout is presented in that region's currency. | Method | Code | Presentment currency | | --------------------------------------- | ------------- | -------------------- | | Card (Visa, Mastercard, Amex, Discover) | `card` | Any supported | | Apple Pay | `apple_pay` | Any supported | | Google Pay | `google_pay` | Any supported | | Alipay | `alipay` | USD | | Amazon Pay | `amazon_pay` | USD, GBP, EUR | | Bancontact | `bancontact` | EUR | | Bizum | `bizum` | EUR | | BLIK | `blik` | PLN | | Cash App Pay | `cash_app` | USD, GBP | | EPS | `eps` | EUR | | iDEAL | `ideal` | EUR | | Kakao Pay | `kakao_pay` | KRW | | MB WAY | `mb_way` | EUR | | Multibanco | `multibanco` | EUR | | Naver Pay | `naver_pay` | KRW | | Pay by Bank | `pay_by_bank` | GBP | | PAYCO | `payco` | KRW | | PIX | `pix` | BRL | | Przelewy24 | `p24` | PLN | | Satispay | `satispay` | EUR | | Samsung Pay | `samsung_pay` | KRW | | UPI | `upi` | INR | | WeChat Pay | `wechat_pay` | USD | ## Lifecycle [#lifecycle] ### Checkout [#checkout] Customer completes a checkout session and submits payment. ### Capture [#capture] The payment is authorized and captured. 3-D Secure 2 runs automatically when the issuer requires it. ### Fulfillment [#fulfillment] On success, an order is created and fulfillment starts (license keys, downloads, webhooks). ### Settlement [#settlement] The net amount (sale minus fees) is added to your available balance, ready to [pay out](/store/finances/payouts). ## Statuses [#statuses] | Status | Meaning | | ---------- | ------------------------------------------ | | Pending | Awaiting customer or network confirmation. | | Processing | Payment is being processed. | | Completed | Payment succeeded, order fulfilled. | | Failed | Declined or rejected. | | Cancelled | Cancelled before completion. | | Refunded | Order was refunded. | | Chargeback | Customer filed a dispute with their bank. | ## Dashboard [#dashboard] The **Payments** page lists every payment your store has received. Filter by status, search by order or customer, and customize the visible columns. Payments list view Click any row to open the detail view, which includes: * **Items and totals.** Line items, tax, platform fee, processing fee, and the net settled amount. * **Customer.** Email and billing address as captured at checkout. * **Location & Network.** Country, partial phone, IP, ISP, and browser fingerprint, useful for triaging fraud. * **Internal IDs.** Order, customer, payment, and payment reference IDs for support tickets and webhook lookups. * **Timeline.** Every event for the payment in order: checkout confirmed, webhooks dispatched, payment captured, order fulfilled. Payment detail view Refunds are issued from the action button in the top right. See [Refunds](/store/manage-payments/refunds) for the full flow. ## Fees [#fees] You pay per sale, with no monthly or setup fees. See [Pricing](/pricing) for the full breakdown with examples. # Refunds (/store/manage-payments/refunds) As merchant of record, Pandabase processes refunds through the card network on your behalf. The customer is credited, and the refunded amount is deducted from your store's available balance. ## When you can refund [#when-you-can-refund] You can refund any payment in `Completed` status, up to **30 days** from the original charge. After that, refunds aren't available. Payments with an open dispute can't be refunded โ€” handle those through the [dispute flow](/store/manage-payments/disputes) instead. ## Issuing a refund [#issuing-a-refund] Open the payment from the **Payments** page and click **Issue Refund** in the top right. Pick a reason and confirm: Issue refund dialog | Reason | Use when | | --------------------- | ------------------------------------------------------------------------ | | Requested by customer | Customer asked for their money back. Most common. | | Duplicate payment | The customer was charged twice for the same order. | | Fraudulent | You believe the original payment was fraudulent. Flags the order for us. | The refund leaves your available balance immediately. The customer's bank typically credits the original payment method within **5 to 10 business days**. ## Full refunds only [#full-refunds-only] Only **full refunds** are supported. Refunding a payment returns the entire captured amount โ€” partial refunds aren't available. The order moves to `Refunded`, coupon usage is restored, the subscription (if any) is cancelled, and fulfillment is revoked. ## Fees [#fees] The original processing and platform fees are both **non-refundable** when you issue a refund. | Charge type | Refundable? | | -------------- | ---------------------------------- | | Sale amount | Yes | | Tax collected | Yes (we remit the adjusted amount) | | Platform fee | No โ€” retained by Pandabase | | Processing fee | No โ€” retained by the network | ## Effect on fulfillment [#effect-on-fulfillment] * **License keys** โ€” pool entries are returned to your inventory; managed and webhook-issued licenses are revoked if you enabled "revoke on refund" on the product. * **Digital downloads & redirects** โ€” access is not automatically removed. If you serve files yourself, revoke on your end. * **Subscriptions** โ€” the underlying subscription is cancelled immediately. ## What customers see [#what-customers-see] Customers get an email confirming the refund was issued, including the amount and the original payment reference. The credit shows up on their statement within 5 to 10 business days, depending on their bank. ## Negative balances [#negative-balances] If your available balance is too low to cover a refund, the refund still goes through and your balance can become negative. The shortfall is settled against your next payouts before any funds are paid out to your bank. # Paylinks (/store/paylinks) A **Paylink** is a static, shareable checkout URL. Send it in an email, DM, invoice, or post it anywhere โ€” anyone with the link can pay without signing in or going through your storefront. Open the **Paylinks** page from the dashboard sidebar to see active links, disabled ones, and a full history. Paylinks list view in the merchant dashboard ## Creating a Paylink [#creating-a-paylink] Click **+ Create Paylink** in the top right and fill in: Create Paylink panel ### Basic information [#basic-information] * **Title** *(optional)* โ€” shown to customers at checkout. Examples: `Pro plan, June`, `Setup fee`, `Invoice #1042`. * **Description** *(optional)* โ€” extra detail about what's being purchased. ### Items [#items] Up to **10 items per link**. Mix catalog products and custom line items in the same Paylink. | Type | Use for | | ------- | -------------------------------------------------------------------------- | | Catalog | Existing products from your store. Pulled live, including price and stock. | | Custom | Ad-hoc charges. Set a name, amount, and quantity directly on the link. | Subscription products **aren't supported** in Paylinks. Use a regular checkout for recurring billing. ### Limits [#limits] Cap how many times the link can be used. Once the total usage limit is hit, the Paylink stops accepting new payments and any open checkout sessions can no longer be completed. Leave the toggle off for unlimited uses. ### Expiration [#expiration] Set a date when the link should stop working. After expiry, the link returns a checkout-closed page to anyone who visits. Leave blank for no expiry โ€” the link works until you disable it manually. ## Sharing [#sharing] Once created, the Paylink gets a permanent URL like `https://pay.pandabase.io/pay_ld7zj6jeyloacgbqp8qe6xjqk4`. Copy it from the row's **Options** menu or the detail view. Customers don't need an account. They land on a hosted checkout page, pay, and receive a receipt by email. The order shows up in your **Payments** page like any other sale. ## Managing existing Paylinks [#managing-existing-paylinks] The list view shows each link's title, item count, status, total uses, and expiry. Filters at the top let you switch between **Active**, **Disabled**, and **All**. From the row's options menu you can: * **Copy URL** โ€” copy the link. * **Disable** โ€” stop accepting new payments immediately. Existing checkout sessions can still complete. * **Re-enable** โ€” reactivate a previously disabled link. * **Delete** โ€” permanent. Existing orders are kept; the URL stops working. ## What customers see [#what-customers-see] The hosted checkout page shows your store name, the Paylink title, line items, and total. It supports every payment method available in the customer's country โ€” cards, Apple Pay, Google Pay, plus local methods like iDEAL, PIX, UPI, and so on. Customers can apply coupons at checkout if your store has any active. ## Limits [#limits-1] * **10 items** per Paylink. * **$0.01 minimum** per custom item. * **$1.00 minimum** total per checkout. * No subscriptions. Use a regular checkout flow for recurring products. ## Where Paylinks fit [#where-paylinks-fit] Use Paylinks for invoices, custom quotes, one-off services, donations, or paid content. Use a storefront when customers need to browse or search a catalog. # Convert to an entity (/store/store-ownership/convert-to-entity) This guide is **deprecated**. The steps below may no longer reflect the current process. Stores are initially owned by the person who signed up and completed KYC. If you register a company, you can request to transfer ownership to it. Turnaround is up to **14 business days** from a complete submission. ## Start a conversion [#start-a-conversion] Email [compliance@pandabase.io](mailto:compliance@pandabase.io) with the subject **"Convert store to entity โ€” \[store ID]"** and a one-line request. **Don't attach documents.** We reply with a link to a private submission portal scoped to your account. The link is single-use and expires after 7 days. Don't email ID, bank details, or ownership documents. The portal is the only supported intake channel. Submitted documents are stored encrypted in our internal compliance vault. Access is limited, logged, and audited. Documents are deleted after the regulatory retention window expires, where law allows. ## What you'll upload [#what-youll-upload] ### Entity [#entity] * Certificate of incorporation or formation (Companies House extract, K-bis, SIRET, IRS EIN letter, etc.) * Operating agreement, bylaws, or shareholder register * Proof of registered address (dated within 90 days) * Tax ID (EIN, VAT, GSTIN, or local equivalent) ### Ownership [#ownership] If you're not a citizen of the country where your company is registered, the company must have at least one shareholder who is a citizen of that country and owns **at least 50% of the company**. Companies must demonstrate a legitimate business purpose and verifiable operations, disclose their ultimate beneficial owners and controllers, and disclose any nominee arrangements. The required local shareholder must beneficially own at least 50% of the company, rather than hold those shares on another person's behalf. We may request supporting evidence and decline or suspend service where ownership, control, or business activity cannot be verified. **Example:** If you're not a US citizen and your company is registered in the US, at least one US citizen must beneficially own 50% or more of the company. A US citizen who owns 50% for their own benefit satisfies the local ownership requirement, subject to verification of the company's ownership, control, and business activity. Someone listed as a 50% shareholder who holds those shares on your behalf does not satisfy the requirement. * For each beneficial owner with **โ‰ฅ25% stake**: full legal name, date of birth, residential address, nationality, and government-issued ID * Name and role of the primary controller if different from the largest owner ### Bank account [#bank-account] Business bank account in the entity's legal name. Personal accounts can't be carried over. | Country | Fields | | -------- | -------------------------------------------------------- | | US | Account number, 9-digit ABA routing number, account type | | UK | Account number, sort code | | EU / EEA | IBAN | | Canada | Account number, transit number, institution number | | Other | IBAN or local equivalent โ€” SWIFT for international rails | ## Process [#process] ### Request [#request] Email compliance with your store ID. ### Portal link [#portal-link] We send a single-use submission link. ### Upload [#upload] Submit documents through the portal. Save progress if needed. ### Review [#review] KYB on the entity and every beneficial owner. Follow-ups come through the portal. ### Convert [#convert] Store ownership re-assigned to the entity. Your personal account becomes the **controller**. ### Confirmation [#confirmation] Email confirming the new ownership record. ## What changes [#what-changes] | | Before | After | | --------------------- | --------------------- | ----------------------------------- | | Store owner | Individual | Entity | | Merchant of record | Your name | Entity name | | Invoices and receipts | Your name | Entity name | | Verification | Individual KYC | KYB + UBO KYC | | Payouts | Personal bank account | Business bank account | | Login | Your personal account | Same account, now as **controller** | ## What stays the same [#what-stays-the-same] * Store ID, products, orders, customers, balances * API tokens, webhook endpoints, paylinks * Active subscriptions and renewals * Past payout history (new payouts route to the business account from the conversion date) ## First payout after conversion [#first-payout-after-conversion] The new business bank account goes through the standard **In Review** period and the **7 to 14 day waiting period** on the first payout, regardless of prior payout history. This is a compliance requirement. ## Cancelling [#cancelling] You can halt a conversion any time before final approval. After approval, converting back to individual ownership isn't supported. # Delete a store (/store/store-ownership/delete-store) You can request deletion of any store on your account. Deletion is permanent โ€” once we process it, the store stops accepting payments, all access is revoked, and customer-facing pages return 404. Deletion isn't immediate. We need to verify ownership, settle any outstanding balance, and confirm there are no open disputes before closing the store. ## Before you delete [#before-you-delete] * **Available balance** must reach your bank account first. Request a final payout and wait for it to settle. * **Open disputes** must be resolved. If you have disputes in review, you can wait for a decision or accept them โ€” but the store can't be deleted while any are pending. * **Active subscriptions** are cancelled as part of deletion. Customers on recurring billing are emailed and not charged again. * **API tokens, webhooks, paylinks, storefront** stop working the moment deletion is processed. ## How to request [#how-to-request] Email [compliance@pandabase.io](mailto:compliance@pandabase.io) with the subject **"Delete store โ€” \[store ID]"**. Confirm: * The store ID * That you've withdrawn your available balance (or want any residual paid out before closure) * That you understand deletion is permanent We may ask follow-up questions through the support thread. Once we confirm there are no blockers, deletion is processed within **5 business days**. ## What gets deleted [#what-gets-deleted] When deletion is processed: * The store's storefront, checkout pages, and paylinks return 404 * API tokens are revoked * Webhook endpoints stop receiving deliveries * Active subscriptions are cancelled * The store is removed from your dashboard ## What we retain [#what-we-retain] Pandabase is a US-based business. We retain some data after deletion to comply with US federal and state regulations โ€” primarily the Bank Secrecy Act (AML record-keeping), IRS tax requirements, and card-network rules. Retained data is **moved to restricted access** โ€” encrypted at rest in our compliance vault, accessible only to compliance staff for audits, tax reporting, and law-enforcement requests. It is not used for any other purpose. | Category | Retained for | | ------------------------------------------------- | ------------------------------------------------ | | Transaction records (payments, refunds, disputes) | **7 years** (IRS / 1099-K reporting) | | KYC / KYB documents | **5 years** post-closure (Bank Secrecy Act) | | Merchant of record invoices and receipts | **7 years** (US tax recordkeeping) | | Bank account details | **5 years** (Bank Secrecy Act) | | Suspicious activity reports (if any were filed) | 5 years from filing date (FinCEN) | | Dispute and fraud evidence | Up to 24 months (Visa / Mastercard requirements) | | Webhook / API audit logs | Up to 1 year | | Marketing data, support tickets, profile metadata | **Deleted on closure** | ## Your rights during the retention period [#your-rights-during-the-retention-period] We process customer and merchant personal data worldwide and respect data-subject rights wherever you're located โ€” **GDPR** in the EU, **UK GDPR** in the United Kingdom, **LGPD** in Brazil, and equivalent regimes elsewhere. The right to erasure (e.g., [GDPR Article 17](https://gdpr-info.eu/art-17-gdpr/), Art. 17(3)(b)) doesn't override US AML and tax obligations โ€” those legal duties take precedence โ€” but the rest of your rights still apply during retention: * **Right of access** โ€” request a copy of what we retain about you. * **Right to rectification** โ€” correct inaccurate information. * **Right to restriction** โ€” ask us to limit processing further (we already minimize during retention, but you can formalize this). * **Right to object** to processing outside the legal-obligation basis. Email [compliance@pandabase.io](mailto:compliance@pandabase.io) for any of the above. ## Customers [#customers] Customers who bought from your store keep access to: * Their receipts and invoices through the customer portal * Their refund and dispute history * Their support cases (read-only once the store is gone) They can still raise refund or dispute requests directly with Pandabase as the merchant of record. We handle these on your behalf using the retained transaction records. ## After deletion [#after-deletion] The store ID is permanently retired โ€” it can't be reused or restored. If you want to open a new store later, you'll create it as a new entity with a new ID. There is no migration path from a deleted store to a new one. # Prohibited Businesses (/trust-and-safety/prohibited-businesses) This page lists allowed, restricted, and prohibited products and activities. It isn't exhaustive. If you're unsure whether a product is allowed, email [compliance@pandabase.io](mailto:compliance@pandabase.io) before listing it. ## Digital products and services only [#digital-products-and-services-only] Pandabase is built exclusively for **digital** products and services. You may sell: * Software, SaaS, web apps, plugins, scripts, mobile apps. * Digital downloads โ€” ebooks, templates, themes, fonts, presets, assets, sample packs, stock media. * Online courses, video tutorials, written guides, paid newsletters, paid community access. * License keys, activation codes, in-game items and currencies (for games you own or are authorized to resell). * Hosted services โ€” APIs, processing, generation, storage, analytics. * Coaching and consulting delivered remotely (calls, written feedback, async review). **Physical goods of any kind are not supported** โ€” that includes apparel, print-on-demand merch, books in print, food, beverages, dietary supplements, cosmetics, hardware, accessories, and anything that ships. A store that mixes digital and physical products is treated as a physical-goods store. ## Prohibited categories [#prohibited-categories] You may not use Pandabase to sell, distribute, facilitate, or promote any of the following. Each item lists concrete examples โ€” but the category is broader than its examples. ### Financial, investment, and advisory services [#financial-investment-and-advisory-services] * Investment advisory (paid stock picks, "alpha" Discords, signal services, trading rooms). * Robo-advisors, copy-trading platforms, prop-firm challenges. * Trading bots, EAs, MQL/Pine scripts sold with guaranteed-return claims. * Crypto investing, NFT marketplaces, token sales, ICOs, IDOs, presales. * Securities, fractional shares, derivatives, yield-bearing products. * Money transmission, peer-to-peer transfers, currency exchange. * Lending, payday loans, BNPL, cash advances, "income share agreements". * Credit repair, debt settlement, credit-boost services, "tradeline" sales. * Legal services, tax preparation, accounting services, immigration paperwork services. * "Get-rich-quick" courses, dropshipping/Amazon FBA programs with earnings claims, MLM downlines, pyramid schemes, referral pyramids. ### Regulated and age-restricted goods [#regulated-and-age-restricted-goods] * Pharmaceuticals (prescription or OTC), telemedicine, online pharmacies. * Medical devices, diagnostic kits, prosthetics, hearing aids. * Dietary supplements, nootropics, peptides, SARMs, "research chemicals". * Weight-loss products or programs with medical or transformation claims. * CBD, THC, kratom, kava, delta-8/9/10, vape products, e-liquids, nicotine pouches. * Alcohol sales or alcohol delivery services. * Tobacco, cigars, hookah, rolling papers when marketed for tobacco use. * Firearms, ammunition, magazines, suppressors, conversion kits, 3D-printed firearm files. * Explosives, fireworks, blasting caps, demolition supplies. * Knives or weapons marketed for combat (switchblades, butterfly knives, gravity knives, ballistic knives). * Endangered species products โ€” ivory, rhino horn, tiger parts, coral, exotic skins, CITES-listed wildlife. * Hazardous chemicals, pool/industrial reagents, radioactive material, restricted lab equipment. ### Adult and sexual content [#adult-and-sexual-content] * Pornography, erotica, sexually explicit images or video (live or recorded). * Adult cam platforms, paid OnlyFans-style content delivery. * Fetish content, BDSM material, adult ASMR. * Escort services, "companionship" services, sugar-dating platforms. * Sex toys, lingerie marketed for adult sexual use, fetish gear. * **AI-generated sexual content** โ€” undress apps, sexual deepfake tools, synthetic CSAM detectors that don't apply (no exceptions), erotic chatbots primarily for explicit roleplay. * Non-consensual or impersonation-based sexual content of any kind. ### Gambling and games of chance [#gambling-and-games-of-chance] * Casino games (slots, blackjack, roulette, baccarat, craps). * Sports betting, in-play betting, prediction markets with real-money payouts. * Poker rooms, online poker software, "rakeback" services. * Sweepstakes models, "social casino" platforms with cash-out paths. * Lotteries, raffles, scratch-cards, bingo with prizes. * Daily fantasy sports for money, esports betting. * Loot box marketplaces, gambling-mechanic mobile games sold standalone. ### Fraud, abuse, and circumvention [#fraud-abuse-and-circumvention] * Phishing kits, scam landing-page templates, "spamming tools". * Carding tools, BIN checkers, CC validators, dump shops. * Account-takeover services, credential stuffing tools, OTP-bypass kits. * Hacked or stolen account marketplaces (Spotify, Netflix, gaming, social). * Bot farms, fake-follower services, view-bot tools, engagement manipulation. * Fake review services, rating boosters, app-store ranking manipulation. * Fake certifications, diploma mills, falsified credentials, made-up "verified" badges. * Personal data sales, scraped contact lists, "leads" derived from breaches. * Stalkerware, spouseware, hidden-tracking apps, surveillance tools sold to consumers. * KYC bypass services, document forgery, sale of ID templates or government documents. * VPN/proxy services explicitly marketed for ban-evasion, fraud, or policy circumvention. * SEO, growth, or marketing services that promise guaranteed rankings, traffic, or income. ### Malware and offensive security [#malware-and-offensive-security] * Malware, ransomware, ransomware-as-a-service. * Spyware, keyloggers, RATs (remote-access trojans). * DDoS-for-hire services, "stresser" or "booter" services. * Exploit kits, weaponized 0-days, automated attack frameworks for unauthorized targets. * Crypters, packers, AV evasion tools marketed for malicious use. Legitimate offensive-security businesses (pentesting services, vulnerability research, red-team tooling sold to verified enterprise customers) are not categorically banned but require review. Reach out before listing. ### AI tools used for abuse [#ai-tools-used-for-abuse] * Tools primarily designed to generate CSAM or non-consensual sexual content. * "Undress" apps, sexual deepfake generators, sexual likeness clones. * Voice or video deepfake tools marketed for impersonation or fraud. * AI systems primarily designed to evade content moderation, KYC, or platform protections. * AI bots designed to manipulate elections, public discourse, or generate large-scale misinformation. ### Illegitimate digital resale and piracy [#illegitimate-digital-resale-and-piracy] * Resold or redistributed digital products without rights โ€” PLR abuse, scraped paid courses, leaked content packs. * Pirated software, cracked games, keygens, "lifetime activation" license sales. * IPTV services, Kodi builds with paid IPTV access, streaming-key reseller services. * Unauthorized streams of sports, films, or live events. * Cheats, hacks, or modded clients for online games when sale violates the game's terms. * Stolen content republished without permission (writing, art, music, video). ### Other restricted categories [#other-restricted-categories] * Dating, matchmaking, and "romance" services. * Timeshares, vacation-club memberships. * Multi-level marketing programs, network-marketing structures. * Religious "miracle" products, prosperity offerings sold with guaranteed-outcome claims. * Psychic readings, spell-casting services, paid horoscope subscriptions with specific predictions. ## Prohibited activities [#prohibited-activities] These rules apply to all merchants, regardless of what they sell. ### Zero-tolerance โ€” immediate termination [#zero-tolerance--immediate-termination] * **CSAM** or any form of child exploitation material. We report to NCMEC and the relevant law enforcement. * Terrorist content, terrorist financing, or material support to designated terrorist organizations. * Sale or promotion of any prohibited product or service listed above after a written warning, or first-strike when intent is clear. * Fraud โ€” using Pandabase to charge customers for goods you can't or won't deliver. * Identity theft, account takeover activity, impersonation of real people or businesses. * Distribution or facilitation of malware, ransomware, or destructive payloads. * Phishing operations or coordinated scam campaigns. ### May result in warning or termination [#may-result-in-warning-or-termination] * Harassment, threats, bullying, or abusive behavior toward customers, support staff, or other merchants. * Hate speech or content targeting protected characteristics (race, religion, national origin, gender, sexual orientation, disability). * Spam, unsolicited bulk messaging, or "blast" marketing using customer data from Pandabase. * Unauthorized collection, exposure, or sale of customer personal data. * Content promoting self-harm, suicide, or eating disorders. * Misleading claims, bait-and-switch listings, or "free trial" patterns designed to entrap customers into hard-to-cancel subscriptions. * Fake reviews, manipulated ratings, or artificial engagement on your own listings. * Attempting to evade enforcement โ€” re-registering after suspension, using nominee owners, splitting one prohibited business across multiple stores. * Violating laws of any jurisdiction where your product is offered, sold, or accessed. ## How we enforce [#how-we-enforce] * We review onboarding listings, product titles, descriptions, and incoming dispute patterns. * Customer reports, network alerts (Verifi/Ethoca), and EFW signals are weighed alongside our own monitoring. * For ambiguous cases we'll reach out, ask questions, and give time to provide context before any account action. * For zero-tolerance violations we suspend immediately and refer to law enforcement where required. This list isn't exhaustive. Pandabase determines whether a business, product, or activity violates this policy. If you're unsure, ask first โ€” [compliance@pandabase.io](mailto:compliance@pandabase.io). # Buyer support (/v2/direct/buyer-support) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Pandabase Direct includes billing support for buyers. You remain the legal seller and handle product support, tax obligations, and dispute liability. ## Billing questions [#billing-questions] Pandabase helps buyers with questions about payments, charges, invoices, and receipts. If resolving a question requires information about your product or a decision from you as the seller, you'll need to help. ## Product questions [#product-questions] You handle delivery, account access, setup, bugs, and other questions about what the buyer purchased. Keep your support email up to date and make it easy to find on your storefront. ## Information to include [#information-to-include] When passing a billing issue to support, include: * The order or payment ID. * The email address used at checkout. * The payment date, amount, and currency. * A short description of the problem and any relevant messages. Never ask buyers to send their full card number or card security code. ## Refunds and disputes [#refunds-and-disputes] As the seller, you remain responsible for resolving refund requests and meeting your obligations to buyers. If a buyer has raised a chargeback through their bank, follow the [Direct dispute process](/v2/direct/disputes). # Disputes and fraud (/v2/direct/disputes) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. With Pandabase Direct, dispute and fraud liability stays with you. Billing support doesn't transfer that liability to Pandabase. ## Responding to a dispute [#responding-to-a-dispute] Review the reason for the dispute and gather records that address the buyer's claim. Follow the instructions and deadline provided for the case through your payment processing partner. Useful evidence includes delivery confirmations, download or access records, customer messages, and refund or cancellation history. Explain what the buyer purchased, when you delivered it, and how the records support your response. The exact Direct submission workflow is still being documented. ## Costs [#costs] The v2 pricing page lists **$30 per dispute handled** and **$30 per dispute prevented**. Confirm which services and processing partner charges apply to your Direct account. These fees are separate from the disputed payment amount. ## Using 3D Secure [#using-3d-secure] 3D Secure asks the cardholder's bank to authenticate a payment. Standalone 3DS costs **$0.12 per authentication**. Eligible authenticated payments can shift fraud liability to the issuer. This doesn't cover every dispute reason, such as a claim that a product wasn't delivered. See [Stripe's explanation of 3DS liability shift](https://support.stripe.com/questions/liability-shift-with-frictionless-flow-for-3d-secure-v2-%283ds2%29?locale=en-GB) for background on this distinction; the rules applied to your payments depend on your processing partner and card network. ## Reduce avoidable disputes [#reduce-avoidable-disputes] Explain charges clearly, show subscription terms before checkout, and provide a working support contact. Keep delivery and usage records, and respond to buyers when they report a problem. # Fees (/v2/direct/fees) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Pandabase Direct costs **3.9% + 29ยข** per successful transaction, plus **Interchange++** fees from our payment processing partner, passed through at cost. These rates apply to software and digital products. ## What Interchange++ means [#what-interchange-means] Interchange++ separates card processing costs into three parts: | Part | What it pays for | | ---------------- | ------------------------------------------------------- | | Interchange | The card issuer's fee. | | Card network fee | The card network's fee. | | Acquirer fee | The processing partner's fee for accepting the payment. | These costs are added to the Pandabase fee. They aren't a single fixed rate for every payment. [Adyen's explanation of Interchange++](https://help.adyen.com/knowledge/payment-methods/explore-payment-methods/what-is-the-interchange) describes this pricing model; it doesn't set the rates for your Pandabase account. ## Example payment [#example-payment] A buyer pays **$50.00**. For this example only, assume the total processing partner charge is **$1.00**. | Item | Amount | | --------------------------------------- | ---------- | | Payment | $50.00 | | Pandabase fee: 3.9% ร— $50.00 + 29ยข | โˆ’$2.24 | | Processing partner charge, illustrative | โˆ’$1.00 | | Remaining after these fees | **$46.76** | The $1.00 processing charge is an example, not a quoted rate. This calculation excludes tax, payout fees, and any other charges that apply to the payment. ## Other fees [#other-fees] Billing, disputes, standalone 3DS, and payouts can have separate charges. See [Pricing](/v2/pricing) for the listed rates and confirm the processing terms for your account. # How Pandabase Direct works (/v2/direct) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. With Pandabase Direct, you remain the legal seller. Pandabase provides checkout, billing, invoicing, and payment processing through our partners. You keep responsibility for tax, disputes, and fraud liability. ## What Pandabase handles [#what-pandabase-handles] | Area | Our role | | ------------- | ------------------------------------------------------------------------------------------------------------ | | Checkout | Hosted, embedded, and in-app checkout. | | Payments | Payment processing through our partners, with the same payment methods and currencies as Merchant of record. | | Billing | Billing, invoicing, and payment links. | | Buyer support | Help buyers with billing questions. | ## What you handle [#what-you-handle] You set your prices, deliver your products, and provide product support. As the legal seller, you also handle tax registration, collection, filing, and payment where required. Dispute and fraud liability stays with you. Billing support is included, but it doesn't transfer these responsibilities to Pandabase. ## From payment to payout [#from-payment-to-payout] 1. A buyer pays through checkout. 2. Our payment processing partner processes the payment for you as the seller. 3. You deliver the product or give the buyer access. 4. Payments are settled through the processing partner under the terms for your account. See [Tax](/v2/direct/tax), [Disputes and fraud](/v2/direct/disputes), [Buyer support](/v2/direct/buyer-support), and [Payouts](/v2/direct/payouts) for details. ## Fees [#fees] The standard rate for software and digital products is **3.9% + 29ยข** per successful transaction, plus processing costs passed through at cost. See [Fees](/v2/direct/fees) for an example or [Pricing](/v2/pricing) to compare modes. To set up your Workspace, follow [Start selling](/v2/quickstart). # Payouts (/v2/direct/payouts) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Payments on Pandabase Direct are processed and settled through our payment processing partner. Your settlement terms determine when funds become available and how they reach your bank account. ## Before your first payout [#before-your-first-payout] Complete the required identity checks and provide accurate bank details. Check the payout currency and destination before relying on an expected transfer. ## Timing [#timing] The v2 Direct payout schedule, minimum amount, and settlement times are still being documented. Confirm the terms for your account, including any initial waiting period or funds held in reserve. ## Fees [#fees] The v2 [pricing page](/v2/pricing#payouts-and-other-services) lists **$1 per US ACH payout** and **2โ€“4% + $1 per international payout**. Direct payments settle through a processing partner, so confirm whether these rates or separate partner payout charges apply to your account. Transaction fees and Interchange++ processing costs are separate from payout fees. See [Fees](/v2/direct/fees) for an example. ## Reconcile a payout [#reconcile-a-payout] Keep the payout reference alongside the payments, fees, refunds, and disputes it covers. The original payment amount and the amount paid into your bank account can differ because of these adjustments. ## If a payout hasn't arrived [#if-a-payout-hasnt-arrived] Check the status, expected settlement date, and bank details. When contacting support, include the payout reference, amount, currency, and date so the transfer can be traced with the processing partner. # Tax (/v2/direct/tax) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. With Pandabase Direct, you remain the legal seller. You are responsible for registering for, collecting, filing, and paying sales tax, VAT, and GST where required. ## Before you sell [#before-you-sell] Determine which tax registrations and reporting requirements apply to your business and its sales. These depend on where you operate, where your buyers are, and what you sell. Work with your accountant or tax adviser to establish the requirements for your business. ## Collecting tax [#collecting-tax] Make sure your checkout and product pricing reflect the tax treatment you need. Keep product and buyer billing information accurate so your records support the amounts charged. The Direct tax configuration steps are still being documented. Don't assume that using Pandabase checkout registers your business for tax or files returns for you. ## Filing and payment [#filing-and-payment] Keep records of sales, tax collected, refunds, and adjustments. You are responsible for filing returns and paying the relevant authorities by their deadlines. ## If you want Pandabase to handle sales tax [#if-you-want-pandabase-to-handle-sales-tax] In [Merchant of record mode](/v2/merchant-of-record/tax), Pandabase is the legal seller and handles sales tax on those orders. Compare the responsibilities and rates on [Pricing](/v2/pricing). # What is Pandabase? (/v2) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Pandabase helps you accept payments for your digital products. We rebuilt this version from the ground up to support payments across multiple processors and give you more flexibility. In this version of Pandabase, Workspaces replace stores as the main containers for your business. Each Workspace is isolated and can have several stores attached to it. Your existing stores continue to work as usual. ## Get started [#get-started] Follow [Start selling](/v2/quickstart) to create your account and set up a Workspace. You can sell in two modes. See [Pricing](/v2/pricing) for rates and what each mode includes. Pandabase is the legal seller and handles payments, tax, disputes, and payouts for you. You remain the legal seller. Payments run through our processing partners, and you keep responsibility for tax, disputes, and fraud liability. ## Learn more [#learn-more] Explore existing guides for payments, products, stores, and payouts. Find documentation for integrating with Pandabase. Find documentation for platforms using Pandabase. # Buyer support (/v2/merchant-of-record/buyer-support) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Pandabase provides billing support for buyers when we act as the merchant of record. You remain the contact for questions about your product and how to use it. ## Billing questions [#billing-questions] Pandabase handles questions about payments, charges, invoices, and receipts. If a buyer asks about a charge they don't recognize, include the order details so we can identify the payment. ## Product questions [#product-questions] You handle product delivery, account access, setup, bugs, and other questions about what the buyer purchased. Keep your support email up to date and make it easy to find on your storefront. If a billing question depends on what happened inside your product, we may need your help. For example, a buyer might say they paid but never received access. ## Information to include [#information-to-include] When passing a billing issue to support, include: * The order or payment ID. * The email address used at checkout. * The payment date, amount, and currency. * A short description of the problem and any relevant messages. Never ask buyers to send their full card number or card security code. ## Refunds and disputes [#refunds-and-disputes] A refund request and a chargeback are different. A buyer requests a refund from the seller; a chargeback is raised through their bank. If the payment is already disputed, follow the [dispute process](/v2/merchant-of-record/disputes). # Disputes (/v2/merchant-of-record/disputes) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. A dispute, or chargeback, starts when a buyer asks their bank to reverse a payment. In Merchant of record mode, Pandabase handles the response and submits evidence on your behalf. ## When an order is disputed [#when-an-order-is-disputed] We may need information about how you delivered the product and how the buyer used it. Respond to any evidence request by the deadline provided for that case. Useful records include: * Delivery emails, downloads, and license activations. * Account access and product usage records. * Messages exchanged with the buyer. * Refund and cancellation history. * The product description and terms shown at the time of purchase. Keep the response factual and relevant to the buyer's complaint. For example, a download record helps with a claim that a digital file was never delivered. ## Dispute fees [#dispute-fees] Dispute handling costs **$30 per dispute**. This fee is separate from the amount the buyer is disputing. See [Pricing](/v2/pricing#billing-and-disputes) for current rates. ## Dispute prevention [#dispute-prevention] Some disputes can be resolved with the card issuer before they become chargebacks. Dispute prevention costs **$30 per dispute prevented**. ## Reduce avoidable disputes [#reduce-avoidable-disputes] Use clear product descriptions, explain recurring charges before payment, and make your support contact easy to find. Deliver access promptly and keep buyers informed when something goes wrong. Billing questions belong with [buyer support](/v2/merchant-of-record/buyer-support). You still handle questions about the product itself. # How merchant of record works (/v2/merchant-of-record) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. With Merchant of record, Pandabase is the legal seller on orders processed in this mode. We handle the payment, sales tax, billing support, and disputes. You provide the product and help customers use it. ## What Pandabase handles [#what-pandabase-handles] | Area | Our role | | ------------- | ------------------------------------------------------------------------- | | Payments | Process payments through our checkout and payment partners. | | Tax | Calculate, collect, file, and pay sales tax, VAT, and GST on these sales. | | Disputes | Screen for fraud and respond to chargebacks with evidence. | | Buyer support | Help buyers with billing questions. | | Payouts | Pay out your earnings after applicable fees and adjustments. | ## What you handle [#what-you-handle] You set your prices, describe your products, and deliver what the buyer ordered. You also handle product support, including account access, setup, and bugs. Keep records of delivery and customer activity. If a buyer disputes an order, we may need those records to prepare a response. ## From payment to payout [#from-payment-to-payout] 1. A buyer pays through checkout. 2. Pandabase processes the payment and handles applicable sales tax. 3. You deliver the product or give the buyer access. 4. Your earnings are paid out after fees and any adjustments. See [Tax](/v2/merchant-of-record/tax), [Disputes](/v2/merchant-of-record/disputes), [Buyer support](/v2/merchant-of-record/buyer-support), and [Payouts](/v2/merchant-of-record/payouts) for details. ## Fees [#fees] The standard rate for software and digital products is **5.9% + 50ยข** per successful transaction. Payments from outside the US and subscription payments have additional fees. See [Pricing](/v2/pricing) for the full breakdown. To set up your Workspace, follow [Start selling](/v2/quickstart). # Payouts (/v2/merchant-of-record/payouts) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. A payout transfers your earnings to your bank account. The amount you receive reflects transaction fees and any applicable refunds, disputes, or other adjustments. ## Before your first payout [#before-your-first-payout] Complete the identity verification described in [Start selling](/v2/quickstart). You can accept payments before verification, but we need to verify your identity before releasing payouts. Make sure the bank details you provide are correct. An incorrect account number or a closed account can delay or prevent a transfer. ## Fees [#fees] | Destination | Fee | | ---------------------------- | ---------------------------------------------- | | US bank account, through ACH | $1 per payout | | International bank account | 2โ€“4% + $1 per payout, depending on the country | Payout fees are separate from transaction fees. See [Pricing](/v2/pricing#payouts-and-other-services) for the full fee list. ## Timing [#timing] The v2 payout schedule, minimum payout amount, and settlement times are still being documented. Confirm the terms for your account before planning around a payout date. ## If a payout hasn't arrived [#if-a-payout-hasnt-arrived] Check the payout status and destination bank details first. When contacting support, include the payout ID, amount, currency, and date. If the transfer has been sent, your bank may also need its reference to locate it. # Tax (/v2/merchant-of-record/tax) Pandabase v2 is still in development, and we're still writing these docs. Some content may be inaccurate, incomplete, or inconsistent between pages. Pandabase handles sales tax, VAT, and GST for orders where we act as the merchant of record. That includes calculating and collecting tax, then filing and paying it to the relevant tax authority. ## At checkout [#at-checkout] Tax is calculated as part of checkout. Keep your product descriptions and categories accurate, and make sure buyers provide the billing details requested during payment. These details help determine the tax treatment of the order. ## Registration, filing, and payment [#registration-filing-and-payment] Pandabase handles the tax registrations, filings, and payments needed for sales where we are the legal seller. You don't need to file those sales taxes on our behalf. This applies to sales processed in Merchant of record mode. It doesn't cover sales you make through [Pandabase Direct](/v2/direct/tax) or another service where you remain the seller. ## Your records [#your-records] Keep your order, invoice, and payout records for your bookkeeping. Sales tax collected from a buyer is separate from your earnings. If a buyer has a question about tax on an order, include the order ID when asking for [billing support](/v2/merchant-of-record/buyer-support). ## What stays with you [#what-stays-with-you] You still manage your own business accounts and any tax obligations outside the sales Pandabase handles as merchant of record. This service doesn't replace your business's accounting or income tax reporting. # Pricing (/v2/pricing) Choose Merchant of record or Pandabase Direct. There are no monthly fees, setup fees, or minimum commitments. Transaction fees apply to successful payments; other fees are listed below. All prices are in USD. These rates apply to software and digital products. Other business categories are priced at 8โ€“9%. Contact sales for a quote. ## Plans [#plans] Pandabase is the legal seller and handles payments, tax, disputes, and payouts for you. You remain the legal seller. Payments run through our processing partners, and you keep responsibility for tax, disputes, and fraud liability. Contact sales for rates based on your payment volume and business needs. ### Merchant of record [#merchant-of-record] **5.9% + 50ยข** per successful transaction. * **+1.5%** for cards and alternative payment methods outside the US * **+0.5%** for subscription payments Included: * Pandabase is the legal seller of record * Sales tax, VAT, and GST filed and paid * Fraud screening and dispute handling * Processor fees * Checkout, billing, and invoicing ### Pandabase Direct [#pandabase-direct] **3.9% + 29ยข** per successful transaction, plus **Interchange++** fees from our payment processing partner, passed through at cost. Included: * Checkout, billing, and invoicing * The same payment methods and currencies as Merchant of record Your responsibilities: * Tax registration and filing * Dispute and fraud liability ### Custom [#custom] Contact sales if you process a high volume of payments or need different rates. Custom plans can include: * Volume discounts * Multi-product pricing * Country-specific rates * Dedicated support ## Compare plans [#compare-plans] The main difference is who acts as the legal seller. With Merchant of record, Pandabase handles tax and disputes. With Pandabase Direct, those responsibilities stay with you. | Feature | Pandabase Direct | Merchant of record | | ------------------------------------------- | ---------------- | ------------------ | | Checkout, cards, wallets, and local methods | Included | Included | | Billing, invoicing, and payment links | Included | Included | | Legal seller of record | You | Pandabase | | Sales tax and VAT registration | You register | Included | | Tax filing and remittance | You file | Included | | Dispute handling and liability | You | Included | | Billing support for buyers | Included | Included | | Processor fees | Passed through | Included | ## Payments [#payments] | Product | Plan | Rate | | ------------------------------------- | ------------------ | ------------------------------------- | | Cards and wallets | Merchant of record | 5.9% + 50ยข per successful transaction | | Bank and local payment methods | Merchant of record | 5.9% + 50ยข per successful transaction | | Adaptive currency | Merchant of record | 3โ€“4%, paid by the customer, not you | | Cards, wallets, and local methods | Pandabase Direct | 3.9% + 29ยข + Interchange++ | | Standalone 3DS | Both | $0.12 per authentication | | Hosted, embedded, and in-app checkout | Both | Included in the standard rate | * **Cards and wallets:** Visa, Mastercard, American Express, Discover, Apple Pay, and Google Pay, online and in-app. * **Bank and local payment methods:** iDEAL, Pix, UPI, Pay by Bank, Alipay, WeChat Pay, and more. * **Adaptive currency:** show prices in the buyer's local currency at checkout. * **Standalone 3DS:** ask the cardholder's bank to verify a card payment before you charge it, so eligible fraud liability shifts to the issuer. With Merchant of record, cards and alternative payment methods from outside the US add **1.5%**, and subscription payments add **0.5%**. ## Billing and disputes [#billing-and-disputes] | Product | Rate | | --------------------------------- | -------------------------------------------------------------- | | Recurring and usage-based billing | +0.5% per subscription payment, on top of the transaction rate | | Invoices | Included in the standard rate | | Dispute handling | $30 per dispute | | Dispute prevention | $30 per dispute prevented | * **Recurring and usage-based billing:** plans, seats, credits, trials, proration, and failed payment retries. * **Invoices:** hosted invoice pages with automatic reminders and receipts. * **Dispute handling:** we respond to chargebacks with evidence on your behalf. * **Dispute prevention:** resolve disputes with the card issuer before they become chargebacks. ## Payouts and other services [#payouts-and-other-services] | Product | Rate | | --------------------- | ---------------------------------------------- | | US payouts | $1 per ACH payout | | International payouts | 2โ€“4% + $1 per payout, depending on the country | | Ledger (alpha) | Free during alpha | | Vault (alpha) | Free during alpha | * **Ledger:** post balanced entries and close periods over the API. * **Vault:** tokenize cards and sensitive data through our vault. ## Example payment [#example-payment] A customer in the US pays **$49.00** for a one-time purchase, sold with Pandabase as merchant of record: | | Amount | | ----------------------------------- | ---------- | | Sale | $49.00 | | Pandabase fee (5.9% ร— $49.00 + 50ยข) | -$3.39 | | **You keep** | **$45.61** | You keep **$45.61** after the transaction fee. Payout fees and any other applicable charges are separate. Follow [Start selling](/v2/quickstart) to set up your Workspace and accept your first payment. # Start selling (/v2/quickstart) Set up your account and Workspace, then share your storefront or connect the Checkout API. See [Pricing](/v2/pricing) to compare Merchant of record and Pandabase Direct. You must be **at least 18 years old**, wherever you live, and register and verify your merchant account using your own identity and ID. Using someone else's ID, including a parent's, violates our [age and identity requirements](/account#age-and-identity-requirements), even with their permission. In this version of Pandabase, Workspaces replace stores as the main containers for your business. Each Workspace is isolated and can have several stores attached to it. Your existing stores continue to work as usual. ### Create your account [#create-your-account] Sign up at [merchant.pandabase.io](https://merchant.pandabase.io/auth/signup) with your email address. We'll send you a magic link to verify your account. ### Complete your profile [#complete-your-profile] Open the magic link and enter your username, first and last name, phone number, and country. Review and accept the legal policies to continue. ### Create your Workspace [#create-your-workspace] Enter your Workspace name, URL, statement descriptor, support email, and country of operation. Choose a category and describe your business and what you sell. Your storefront goes live when you submit the form. | Country | Region | | ------------------------- | ------------ | | ๐Ÿ‡จ๐Ÿ‡ฆ Canada | Americas | | ๐Ÿ‡บ๐Ÿ‡ธ United States | Americas | | ๐Ÿ‡ฆ๐Ÿ‡น Austria | Europe | | ๐Ÿ‡ง๐Ÿ‡ช Belgium | Europe | | ๐Ÿ‡ญ๐Ÿ‡ท Croatia | Europe | | ๐Ÿ‡จ๐Ÿ‡พ Cyprus | Europe | | ๐Ÿ‡จ๐Ÿ‡ฟ Czech Republic | Europe | | ๐Ÿ‡ฉ๐Ÿ‡ฐ Denmark | Europe | | ๐Ÿ‡ช๐Ÿ‡ช Estonia | Europe | | ๐Ÿ‡ซ๐Ÿ‡ฎ Finland | Europe | | ๐Ÿ‡ซ๐Ÿ‡ท France | Europe | | ๐Ÿ‡ฉ๐Ÿ‡ช Germany | Europe | | ๐Ÿ‡ฌ๐Ÿ‡ง Great Britain | Europe | | ๐Ÿ‡ฌ๐Ÿ‡ท Greece | Europe | | ๐Ÿ‡ญ๐Ÿ‡บ Hungary | Europe | | ๐Ÿ‡ฎ๐Ÿ‡ช Ireland | Europe | | ๐Ÿ‡ฎ๐Ÿ‡น Italy | Europe | | ๐Ÿ‡ฑ๐Ÿ‡ป Latvia | Europe | | ๐Ÿ‡ฑ๐Ÿ‡น Lithuania | Europe | | ๐Ÿ‡ฑ๐Ÿ‡บ Luxembourg | Europe | | ๐Ÿ‡ฒ๐Ÿ‡น Malta | Europe | | ๐Ÿ‡ณ๐Ÿ‡ฑ Netherlands | Europe | | ๐Ÿ‡ณ๐Ÿ‡ด Norway | Europe | | ๐Ÿ‡ต๐Ÿ‡ฑ Poland | Europe | | ๐Ÿ‡ต๐Ÿ‡น Portugal | Europe | | ๐Ÿ‡ท๐Ÿ‡ด Romania | Europe | | ๐Ÿ‡ธ๐Ÿ‡ฐ Slovakia | Europe | | ๐Ÿ‡ธ๐Ÿ‡ฎ Slovenia | Europe | | ๐Ÿ‡ช๐Ÿ‡ธ Spain | Europe | | ๐Ÿ‡ธ๐Ÿ‡ช Sweden | Europe | | ๐Ÿ‡จ๐Ÿ‡ญ Switzerland | Europe | | ๐Ÿ‡น๐Ÿ‡ท Turkiye | Europe | | ๐Ÿ‡ฆ๐Ÿ‡บ Australia | Asia-Pacific | | ๐Ÿ‡ญ๐Ÿ‡ฐ Hong Kong | Asia-Pacific | | ๐Ÿ‡ฎ๐Ÿ‡ณ India | Asia-Pacific | | ๐Ÿ‡ณ๐Ÿ‡ฟ New Zealand | Asia-Pacific | | ๐Ÿ‡ต๐Ÿ‡ญ Philippines | Asia-Pacific | | ๐Ÿ‡ธ๐Ÿ‡ฌ Singapore | Asia-Pacific | | ๐Ÿ‡น๐Ÿ‡ผ Taiwan | Asia-Pacific | | ๐Ÿ‡น๐Ÿ‡ญ Thailand | Asia-Pacific | | ๐Ÿ‡ฎ๐Ÿ‡ฑ Israel | Middle East | | ๐Ÿ‡ถ๐Ÿ‡ฆ Qatar | Middle East | | ๐Ÿ‡ฆ๐Ÿ‡ช United Arab Emirates | Middle East | | ๐Ÿ‡ฒ๐Ÿ‡ฆ Morocco | Africa | ### Verify your identity [#verify-your-identity] Submit your ID and verify your phone number with two-factor authentication (2FA). You can start accepting payments before verification, but you need to complete it before we release payouts. We use Sumsub to verify your identity before sending payouts. Sumsub processes your ID; Pandabase doesn't store it. ### Complete the Workspace questionnaire [#complete-the-workspace-questionnaire] Open the questionnaire from the banner at the top of your dashboard and answer the questions about your business. ### Accept your first payment [#accept-your-first-payment] Share your storefront link or connect the [Checkout API](/developers/learn/hosted-checkout). The API guide is in the current documentation. With Merchant of record, Pandabase handles tax, disputes, and payouts. With Pandabase Direct, you remain the legal seller and keep responsibility for tax, disputes, and fraud liability. # Your account (/v2/account) This section is being written. Until then, see [Your account](/account) in the current documentation. # Developers (/v2/developers) This section is being written. Until then, see the [current documentation](/developers/learn/lifecycle). # Platforms (/v2/platforms) This section is being written. Until then, see the [current documentation](/). # Dispute prevention (/v2/workspace/disputes/dispute-prevention) This page is a draft. Content is coming soon. ## How prevention works [#how-prevention-works] Draft. ## Prevented disputes [#prevented-disputes] Draft. # Disputes overview (/v2/workspace/disputes) This page is a draft. Content is coming soon. ## How disputes work [#how-disputes-work] Draft. ## Dispute statuses [#dispute-statuses] Draft. # Responding to disputes (/v2/workspace/disputes/responding-to-disputes) This page is a draft. Content is coming soon. ## Submitting evidence [#submitting-evidence] Draft. ## Dispute outcomes [#dispute-outcomes] Draft. # Manage your workspace (/v2/workspace) This section is being written. Until then, see [Manage your store](/store/analytics) in the current documentation. # Accounts and balances (/v2/workspace/ledger/accounts) Ledger is in alpha. There are no Ledger fees during alpha. An account tracks a balance in one currency. Create separate accounts for the balances you need to track, such as a customer's wallet. The account's **normal side** tells the ledger which entries increase its balance. Asset and expense accounts normally increase with a `debit`; liability, equity, and revenue accounts normally increase with a `credit`. A customer wallet usually uses `credit` because its balance represents money you owe the customer. ## Create an account [#create-an-account] | Field | Description | | --------------------------------- | -------------------------------------------------------------------------------------------- | | `ledger_id` | **Required.** The ledger the account belongs to. | | `code` | **Required.** Up to 128 bytes, unique within the ledger. A readable key like `wallet:alice`. | | `currency` | **Required.** A registered currency code. | | `normal_side` | **Required.** `debit` or `credit`. Can't be changed later. | | `name`, `description`, `metadata` | Display details and your own attributes. | | `allow_negative` | Default `false`. Allows unlimited negative balances. | | `overdraft_limit` | Default `"0"`. How far below zero the balance may go when `allow_negative` is `false`. | ```json { "ledger_id": "ldg_...", "code": "wallet:alice", "name": "Alice's wallet", "currency": "USD", "normal_side": "credit" } ``` ## Balances [#balances] Each account reports three balances. Every balance includes total `debits`, total `credits`, and an `amount`. For a debit-normal account, the amount is debits minus credits. For a credit-normal account, it is credits minus debits. | Balance | Meaning | | ----------- | ------------------------------------------------------------------------------------ | | `posted` | The balance from entries that have been posted. | | `pending` | Posted totals plus pending debits and credits. | | `available` | Posted funds minus pending outflows and holds. Pending inflows aren't spendable yet. | The account also reports `held`, the funds reserved by outstanding [holds](/v2/workspace/ledger/holds). ```json { "posted": { "debits": "0", "credits": "5000", "amount": "5000" }, "pending": { "debits": "1500", "credits": "5000", "amount": "3500" }, "available": { "debits": "1500", "credits": "5000", "amount": "3500" } } ``` This credit-normal wallet has $50 posted and $35 available after a pending $15 withdrawal. You can also read balances for an accounting date range. Those totals cover entries in the range and **exclude holds**, so use the account itself for the current spendable balance. ## History [#history] The entry history shows posted entries in the order they were posted. Each entry includes `balance_after`, the balance immediately after that entry. Backdating an entry doesn't change its place in this history. ## Account status [#account-status] | Status | Behavior | | -------- | --------------------------------------------------------------------------------- | | `open` | New accounts start open. | | `frozen` | Rejects new money movements until unfrozen. | | `closed` | Permanent. Requires a zero posted balance, no pending changes, and no held funds. | Repeating the same status change returns the account unchanged. ## Currencies [#currencies] ISO 4217 currencies are preloaded. To use another currency, register it first with a `code` (3โ€“16 characters: uppercase letters, digits, or `_`, starting with a letter) and an `exponent`, the number of decimal places in one whole unit. ```json { "code": "ETH", "exponent": 18 } ``` Registration is permanent; currencies can't be updated or deleted. ## Account categories [#account-categories] Use categories to total related accounts, such as all customer wallets in USD. Accounts in a category must belong to the same ledger and use the same currency. Categories can nest up to seven levels deep. An account included through more than one branch is counted only once. ```json { "ledger_id": "ldg_...", "currency": "USD", "normal_side": "credit", "name": "Customer wallets" } ``` The category's normal side controls the sign of its totals, and its members must match its ledger and currency. Deleting a category leaves its accounts in place. # Closing periods (/v2/workspace/ledger/closing-periods) Ledger is in alpha. There are no Ledger fees during alpha. Close a period when you have finished reporting on it and want to prevent later changes. Set the ledger's `closed_before` to the start of the next period. For example, this closes accounting dates before September 1, 2026: ```json { "closed_before": "2026-09-01T00:00:00Z" } ``` ## What's blocked [#whats-blocked] The ledger rejects these operations when their accounting date is before `closed_before`: * New transactions * New pending transactions * Changes to pending transactions * Transactions inside a batch ## What still works [#what-still-works] * Archiving a pending transaction, since it doesn't change a closed period. * Reversals, which are always dated now. * Retrying a request that succeeded before the close, which returns its original result. The close waits for changes already in progress to finish. Once it completes, new postings can't enter the closed period. `closed_before` can't be in the future. ## Reopen a period [#reopen-a-period] Set `closed_before` to an earlier time to reopen part of the closed period. Set it to `null` to remove the cutoff entirely. # Events and webhooks (/v2/workspace/ledger/events) Ledger is in alpha. There are no Ledger fees during alpha. Ledger saves an event whenever a record changes. The event and the change are saved together in the same database transaction. An event's `data` shows the resource at the time of that change. Fetch the resource again if your application needs its latest state. ## Event types [#event-types] | Type | Sent when | | -------------------------------------------------------------- | ----------------------------------------------------------- | | `account.created`, `account.updated` | An account is created or changed, including status changes. | | `transaction.created` | A transaction is created, including ones created posted. | | `transaction.updated` | A pending transaction is edited. | | `transaction.posted` | A pending transaction is posted. | | `transaction.archived` | A transaction is archived. | | `hold.created`, `hold.captured`, `hold.voided`, `hold.expired` | A hold changes. | | `balance_monitor.triggered` | A balance monitor's condition becomes true. | | `bulk_request.completed` | A bulk request finishes. | | `settlement.created` | A settlement is created. | Events are kept for 30 days. Use them to react to changes, and use ledger entries for the accounting history. ## Webhook endpoints [#webhook-endpoints] Register an HTTPS URL to receive events as JSON `POST` requests. Choose which events to receive with `event_types`, using exact names like `hold.captured`, patterns like `transaction.*`, or `*` for everything. ```json { "url": "https://example.com/webhooks/ledger", "event_types": ["transaction.*", "hold.captured"], "description": "Payments service" } ``` Creating an endpoint returns a signing secret that starts with `whsec_`. It's only shown once, so save it then. ## Receive events [#receive-events] The same event can arrive more than once, and events can arrive out of order. Track event IDs so you don't process a duplicate twice. Return a `2xx` response once you've accepted the event. Any other status, a network error, or no response within 10 seconds counts as a failed attempt. ## Verify signatures [#verify-signatures] Read the raw request body before parsing the JSON. Extract `t` and `v1` from the signature header, which looks like `t=1788220800,v1=`. Compute an HMAC-SHA256 of `.`, using the **entire** secret string, including `whsec_`, as the key. Compare the lowercase hex digest with `v1` using a constant-time comparison. Reject timestamps outside your tolerance, such as five minutes, and deduplicate by event ID. ## Retries [#retries] A failed delivery is retried 11 times after the first attempt, over roughly three days: after 1 minute, 5 minutes, 30 minutes, 1 hour, 2 hours, 5 hours, 10 hours, 10 hours, 12 hours, 12 hours, and 12 hours. Each attempt gets a fresh timestamp and signature. You can also request another attempt for a failed delivery yourself. # Holds and scheduled transactions (/v2/workspace/ledger/holds) Ledger is in alpha. There are no Ledger fees during alpha. A hold reserves funds now. A scheduled transaction attempts to post entries later. Use a hold when the funds must stay available for a specific operation. ## Create a hold [#create-a-hold] A hold reduces the available balance without creating a pending transaction. You can capture it once, release it by voiding it, or let it expire. | Field | Description | | ------------- | ------------------------------------------------------ | | `account_id` | **Required.** The account whose funds are reserved. | | `amount` | **Required.** The positive amount to reserve. | | `expires_at` | **Required.** When the hold can no longer be captured. | | `description` | What the reservation is for. | ```json { "account_id": "acct_...", "amount": "10000", "description": "Order reservation", "expires_at": "2030-01-01T00:00:00Z" } ``` ### Capture a hold [#capture-a-hold] Capturing creates a ledger transaction for up to the held amount. The destination account must use the same ledger and currency. ```json { "destination_account_id": "acct_...", "amount": "6240", "description": "Order completed" } ``` Capturing releases the **whole** hold, including any unused amount. In this example, $100 was held and $62.40 captured, so the remaining $37.60 becomes available again. ### Release a hold [#release-a-hold] | Outcome | What happens | | ------- | ----------------------------------------------------------------------------------- | | Voided | You release the hold. No transaction is created. | | Expired | The hold passes `expires_at` and can't be captured, even before its status updates. | A hold's status is `pending`, `captured`, `voided`, or `expired`. ## Scheduled transactions [#scheduled-transactions] Set `execute_at` alongside the usual transaction fields to schedule a transaction. It runs at or after that time. ```json { "execute_at": "2030-01-01T00:00:00Z", "description": "Monthly interest", "entries": [ { "account_id": "acct_interest...", "side": "debit", "amount": "1000" }, { "account_id": "acct_income...", "side": "credit", "amount": "1000" } ] } ``` Scheduling doesn't reserve funds. Account and balance checks happen when the schedule runs, so it can fail if funds are no longer available. A schedule has one of four statuses: `scheduled`, `executed`, `failed`, or `canceled`. You can cancel it only while it is `scheduled`. Schedules can't be edited or retried. If one fails, read its `failure` reason before creating a replacement with a new idempotency key. # Ledger overview (/v2/workspace/ledger) Ledger is in alpha. There are no Ledger fees during alpha. Pandabase uses ledgers internally to keep track of your money. A ledger records changes to balances so each amount can be traced back to the entries that explain it. You can also use Ledger in your own application to track balances, reserve funds, and record settlements. It records the movement of money between accounts. Your application and payment providers handle the actual bank, card, or blockchain transfers. Ledger is built on [Astrum](https://github.com/pandabase/astrum), our open-source double-entry ledger core. The repository includes API documentation and examples. ## How it works [#how-it-works] Each **account** tracks a balance in one currency. A **transaction** records a change through two or more **entries**, each of which debits or credits an account. The total debits and credits must match in each currency. This is called double-entry accounting. Instead of overwriting a balance, you add entries that explain why it changed. The posted balance comes from posted entries; pending transactions and holds also affect what's available. For example, a USD wallet with $50 posted and a pending $15 withdrawal has $35 available. The ledger keeps the posted balance and the reserved amount separate until the withdrawal is posted or canceled. Accounts hold one currency and track posted, pending, and available balances. Record a balance change, reserve it as pending, or reverse a posted transaction. Reserve available funds now and capture them later. Save account records for a period and record which entries have been settled. ## Terms used in these guides [#terms-used-in-these-guides] | Concept | What it is | | --------------------- | -------------------------------------------------------------------------------------------------- | | Ledger | A group of accounts that can transact with each other. Transactions can't cross ledger boundaries. | | Account | Holds one currency. Its normal side decides which entries increase its balance. | | Transaction | A balanced set of 2โ€“1,000 entries. Can be pending, posted, or archived. | | Entry | A single debit or credit to one account. | | Hold | A reservation of available funds that you capture, void, or let expire. | | Scheduled transaction | A transaction that runs at or after a given time. | | Statement | A saved snapshot of an account's balances and entries for a period. | | Settlement | Records the net unsettled amount against another account, called the contra account. | | Account category | Groups account balances into totals, with up to seven levels of categories. | | Balance monitor | Emits an event when an account's balance crosses a condition. | | Event | A snapshot of a change, delivered to your webhook endpoints. | ## Amounts, dates, and IDs [#amounts-dates-and-ids] * **Amounts** are integer minor units as strings, up to 38 digits. `"100"` is USD 1.00. Entry, hold, and capture amounts must be positive; balances can be negative. * **Times** are RFC 3339 strings, such as `2026-09-01T00:00:00Z`. * **IDs** carry a resource prefix, such as `ldg_` for ledgers, `acct_` for accounts, and `txn_` for transactions. * **Metadata** is a JSON object up to 16 KiB for your own attributes. Updates merge: keys you omit stay, and a key set to `null` is removed. ## More guides [#more-guides] Use [Closing periods](/v2/workspace/ledger/closing-periods) to prevent changes to a period you've already reported on. Use [Events and webhooks](/v2/workspace/ledger/events) to notify your application when ledger records change. Astrum's repository also includes examples for marketplaces, wallets, usage-based billing, and card authorizations. # Statements and settlements (/v2/workspace/ledger/statements-and-settlements) Ledger is in alpha. There are no Ledger fees during alpha. Statements save records for reporting. Settlements mark entries as settled, and balance monitors notify your application when a balance meets a condition. ## Statements [#statements] A statement saves an account's posted opening balance, closing balance, and entries for a period. Later backdated transactions don't change a saved statement, so it stays a reliable record of what was reported. ```json { "account_id": "acct_...", "effective_at_lower_bound": "2026-09-01T00:00:00Z", "effective_at_upper_bound": "2026-10-01T00:00:00Z", "description": "September statement" } ``` This includes entries dated September 1 up to, but not including, October 1. The statement returns `starting_balance`, `ending_balance`, and `entry_count`. You can also list its saved entries. ## Settlements [#settlements] A settlement totals an account's unsettled postings, records the net amount against another account, and marks the covered entries as settled. The other account is called the **contra account**. Each posting can be settled only once. Use this to record a seller payout or a transfer to a bank account in the ledger. Your payment provider still handles the actual transfer. | Field | Description | | -------------------------- | -------------------------------------------------------------------------------------- | | `settled_account_id` | **Required.** The account whose unsettled entries are covered. | | `contra_account_id` | **Required.** A different account in the same ledger and currency. | | `effective_at_upper_bound` | Only cover postings before this accounting date. Omit to include everything unsettled. | ```json { "settled_account_id": "acct_vendor...", "contra_account_id": "acct_bank...", "description": "Vendor payout" } ``` If the covered entries net to zero, the settlement marks them without creating a transaction. ## Balance monitors [#balance-monitors] A balance monitor watches an account and emits a `balance_monitor.triggered` event when a condition becomes true, such as a wallet dropping below $5. ```json { "account_id": "acct_...", "alert_condition": { "field": "available_balance_amount", "operator": "lt", "value": "500" }, "description": "Low wallet balance" } ``` The monitor sends an event when a balance change makes the condition true after it was false. It does not send an event just because the condition is already true when you create it. To watch a different account or change the condition, replace the monitor. # Transactions (/v2/workspace/ledger/transactions) Ledger is in alpha. There are no Ledger fees during alpha. A transaction records a balance change between accounts in the same ledger. Its total debits and credits must match **in each currency**. You can include more than one currency, but a USD debit can't balance a EUR credit. ## Create a transaction [#create-a-transaction] | Field | Description | | ------------------------- | -------------------------------------------------------------- | | `entries` | **Required.** 2โ€“1,000 entries. | | `status` | `posted` (default) or `pending`. | | `effective_at` | The accounting date. Defaults to the creation time. | | `external_id` | Your own reference, up to 255 bytes, unique within the ledger. | | `description`, `metadata` | A readable explanation and your own attributes. | Each entry needs an `account_id`, a `side` (`debit` or `credit`), and a positive `amount`. ```json { "description": "Opening capital", "external_id": "opening-2026", "entries": [ { "account_id": "acct_cash...", "side": "debit", "amount": "10000" }, { "account_id": "acct_equity...", "side": "credit", "amount": "10000" } ] } ``` ## Balance conditions [#balance-conditions] Add a balance condition to an entry when the transaction must leave the account within a limit, such as keeping at least $5 available. The ledger checks the condition as part of the transaction, so another balance change cannot slip between the check and the posting. | Condition | Checks | | -------------------------- | -------------------------------------------- | | `available_balance_amount` | The available balance after the transaction. | | `posted_balance_amount` | The posted balance after the transaction. | | `pending_balance_amount` | The pending balance after the transaction. | Each condition can use `gt`, `gte`, `eq`, `lt`, `lte`, or `not_eq`, and every comparison you supply must pass. For example, `{"gte": "500"}` on a USD account requires at least $5 after the whole transaction. Include the account's expected `lock_version` if the transaction should only proceed when the account is unchanged since you last read it. If the version has changed, the ledger rejects the transaction. ## Pending, posted, and archived transactions [#pending-posted-and-archived-transactions] Transactions are posted by default. Create one as pending when you need to reserve funds before deciding whether to post or archive it. ### Pending [#pending] Create a transaction with `status: "pending"` to reserve outgoing funds. Pending inflows raise the pending balance but not the available balance. You can still edit a pending transaction's description, metadata, accounting date, or entries. ### Posted [#posted] Post a pending transaction to make it final. You can post all of its entries, or post less โ€” the unused part of the reservation is released and nothing stays pending. ### Archived [#archived] Archive a pending transaction to release its reservation without posting it. ### Reverse a transaction [#reverse-a-transaction] Posted transactions are never edited or deleted. To undo one, reverse it: this creates a new posted transaction with the opposite entries and leaves the original intact. Each transaction can be reversed once, and normal balance checks still apply. ## Batches and bulk requests [#batches-and-bulk-requests] | | Batch | Bulk request | | ---------- | ------------------------------- | ----------------------------- | | Size | 1โ€“1,000 transactions | 1โ€“10,000 transactions | | Processing | Synchronous | Queued in the background | | Atomic | Yes by default, or independent | Always independent | | Results | In the response, in input order | Fetched later, in input order | A batch is all-or-nothing by default: if one transaction fails, none are saved. Always inspect each item's result, even when the whole response isn't a success. ## Retry requests safely [#retry-requests-safely] Creating transactions, batches, bulk requests, holds, captures, reversals, schedules, and settlements requires an `Idempotency-Key`. Use a unique key per intended operation, and reuse it only when retrying that exact request. ```http Idempotency-Key: order-1001-payment ``` A retry with the same key and identical request returns the saved result without recording the operation twice. The same key with a different request is rejected. Use globally unique keys and don't recycle them. Batch and bulk items get `/0`, `/1`, and so on appended to your key. # 3D Secure (/v2/workspace/payment-fraud/3d-secure) This page is a draft. Content is coming soon. ## Dynamic 3D Secure [#dynamic-3d-secure] Draft. ## Standalone 3DS [#standalone-3ds] Draft. # Early fraud warnings (/v2/workspace/payment-fraud/early-fraud-warnings) This page is a draft. Content is coming soon. ## What an early fraud warning is [#what-an-early-fraud-warning-is] Draft. ## Responding to a warning [#responding-to-a-warning] Draft. # Fraud checks (/v2/workspace/payment-fraud/fraud-checks) This page is a draft. Content is coming soon. ## Risk scoring [#risk-scoring] Draft. ## Rules and review [#rules-and-review] Draft. # Payment fraud overview (/v2/workspace/payment-fraud) This page is a draft. Content is coming soon. ## How fraud checks work [#how-fraud-checks-work] Draft. ## What you can do [#what-you-can-do] Draft. # Payments overview (/v2/workspace/payments) This page is a draft. Content is coming soon. ## How a payment flows [#how-a-payment-flows] Draft. ## Payment statuses [#payment-statuses] Draft. # Payment methods (/v2/workspace/payments/payment-methods) This page is a draft. Content is coming soon. ## Cards and wallets [#cards-and-wallets] Draft. ## Bank and local methods [#bank-and-local-methods] Draft. # Refunds (/v2/workspace/payments/refunds) This page is a draft. Content is coming soon. ## Issuing a refund [#issuing-a-refund] Draft. ## Refund statuses [#refund-statuses] Draft. # Viewing payments (/v2/workspace/payments/viewing-payments) This page is a draft. Content is coming soon. ## Searching and filtering [#searching-and-filtering] Draft. ## Payment details [#payment-details] Draft. # Bank accounts (/v2/workspace/payouts/bank-accounts) This page is a draft. Content is coming soon. ## Adding a bank account [#adding-a-bank-account] Draft. ## Verification [#verification] Draft. # Payouts overview (/v2/workspace/payouts) This page is a draft. Content is coming soon. ## How payouts work [#how-payouts-work] Draft. ## Payout statuses [#payout-statuses] Draft. # Payout schedule (/v2/workspace/payouts/payout-schedule) This page is a draft. Content is coming soon. ## Schedules [#schedules] Draft. ## Minimum payout [#minimum-payout] Draft. # Requesting a payout (/v2/workspace/payouts/requesting-a-payout) This page is a draft. Content is coming soon. ## Requesting a payout [#requesting-a-payout] Draft. ## Payout fees [#payout-fees] Draft. # Create a store (/v2/workspace/stores/create-a-store) This page is a draft. Content is coming soon. ## Before you start [#before-you-start] Draft. ## Creating a store [#creating-a-store] Draft. # Stores overview (/v2/workspace/stores) This page is a draft. Content is coming soon. ## What a store is [#what-a-store-is] Draft. ## Stores and your workspace [#stores-and-your-workspace] Draft. # Store settings (/v2/workspace/stores/settings) This page is a draft. Content is coming soon. ## Store details [#store-details] Draft. ## Branding [#branding] Draft. ## Checkout settings [#checkout-settings] Draft. # Team members (/v2/workspace/stores/team-members) This page is a draft. Content is coming soon. ## Inviting a teammate [#inviting-a-teammate] Draft. ## Roles and permissions [#roles-and-permissions] Draft. # Access and security (/v2/workspace/vault/access-and-security) Vault is in alpha. There are no Vault fees during alpha. Vault uses API keys and permissions to control what each integration can do. Access logs record token activity, and events notify your application when tokens change. ## Keys and permissions [#keys-and-permissions] | Key | Prefix | Use | | --------------- | ---------------------- | ----------------------------------------------------------------------------- | | Secret key | `sk_live_`, `sk_test_` | Server-side requests. Never expose it in a browser or app. | | Publishable key | `pk_live_`, `pk_test_` | Loading Elements in the browser. It can only create tokens through a session. | Choose permissions based on what the integration needs to do. Reading a token and revealing its raw data are separate permissions: | Permission | Allows | | --------------- | ------------------------------------------------------------------- | | `tokens:read` | Retrieving and listing tokens, without raw data. | | `tokens:write` | Creating, updating, and deleting tokens, and creating sessions. | | `tokens:reveal` | Revealing raw data. Grant it only to keys that must see raw values. | | `proxy:use` | Sending proxy requests. | Test keys only work with test tokens, and test tokens can't hold real card numbers. ## Access logs [#access-logs] Access logs record each action on a token, the actor, and the source of the request. Use them to check when a token was read, revealed, or sent through the proxy. `GET /tokens/{id}/access_logs` returns a list, newest first. ```json { "object": "list", "data": [ { "object": "access_log", "id": "val_01j9zt4n6q8s0u2w4y6a8c0e2g", "token_id": "tok_01j9zq3c7mfx8v2k5n6p4r8t0w", "action": "revealed", "actor": { "type": "api_key", "id": "key_01j9zs2m4p6r8t0v2x4z6b8d0f" }, "ip_address": "203.0.113.24", "created_at": "2026-09-02T09:15:00Z" } ], "has_more": false, "next_cursor": null } ``` The `action` field is `created`, `retrieved`, `updated`, `revealed`, `proxied`, or `deleted`. Proxy logs also include the destination host and `Pandabase-Proxy-Request-Id`, which you can match to the response header. ## Events [#events] Vault sends events to your webhook endpoints when tokens change. | Type | Sent when | | ---------------- | ----------------------------------------------- | | `token.created` | A token is created, including through Elements. | | `token.updated` | A token's metadata or expiry changes. | | `token.revealed` | A token's raw data is revealed. | | `token.deleted` | A token is deleted or expires. | Event data never includes raw values. ## Retry requests safely [#retry-requests-safely] Include an `Idempotency-Key` when creating a token or sending a proxy request. Give each operation its own key. If you need to retry, send the same request with the same key. ```http Idempotency-Key: order-1001-charge ``` An identical retry with the same key returns the saved result without repeating the operation. Reusing the key with a different request returns `422 idempotency_key_reused`. ## Errors [#errors] Errors return JSON with the content type `application/problem+json`. Check `code` to decide how your application should respond. The `detail` field explains what went wrong, and `request_id` identifies the request. ```json { "type": "urn:pandabase:vault:error:permission_denied", "title": "Forbidden", "status": 403, "code": "permission_denied", "detail": "This key doesn't have the tokens:reveal permission.", "request_id": "req_01j9zv6q8s0u2w4y6a8c0e2g4j" } ``` | Status | Codes | | ------ | ------------------------------------------------------------------- | | 400 | `invalid_request`, `idempotency_key_required` | | 401 | `unauthorized` | | 403 | `permission_denied`, `proxy_destination_not_allowed` | | 404 | `token_not_found`, `session_not_found` | | 409 | `idempotency_key_in_use` | | 410 | `token_expired`, `session_expired`, `cvc_unavailable` | | 422 | `validation_error`, `invalid_card_number`, `idempotency_key_reused` | | 429 | `rate_limited` | | 502 | `proxy_destination_unreachable` | # Elements (/v2/workspace/vault/elements) Vault is in alpha. There are no Vault fees during alpha. Elements are input fields hosted by Pandabase and embedded in your page. Each field runs in an iframe and sends the customer's input directly to Vault. Your page receives a token and display details, such as a card's last four digits, without reading the raw input. Elements are part of the [WebSDK](/developers/websdk/getting-started). Load the SDK from `https://js.pandabase.io`. ### Create a session on your server [#create-a-session-on-your-server] Create a session from your server with a secret key. The session lets the browser create one token. Pass its `client_secret` to the page that contains your form. ```sh curl https://api.pandabase.io/v2/workspaces/wks_01j9zk2m4p6r8t0v2x4z6b8d0f/vaults/vlt_01j9zm3n5q7s9u1w3y5a7c9e1g/sessions \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "type": "card", "metadata": { "customer": "cus_123" } }' ``` ```json { "object": "vault_session", "id": "vss_01j9zr8k2p4m6q8s0u2w4y6a8c", "type": "card", "client_secret": "vss_01j9zr8k2p4m6q8s0u2w4y6a8c_secret_7Hq2Lm9xR4", "metadata": { "customer": "cus_123" }, "expires_at": "2026-09-01T12:30:00Z", "created_at": "2026-09-01T12:00:00Z" } ``` A session expires after 30 minutes or when it creates a token. The token belongs to the session's vault and inherits its `metadata`. Create a new session when the previous one has been used or has expired. ### Mount Elements on your page [#mount-elements-on-your-page] Add an empty container where the field should appear, then create and mount the Element with your publishable key and the session's `client_secret`. ```html
``` ```js const pandabase = Pandabase('pk_live_...'); const elements = pandabase.vault.elements({ clientSecret }); const card = elements.create('card'); card.mount('#card-element'); ```
### Save the data as a token [#save-the-data-as-a-token] ```js const form = document.getElementById('card-form'); form.addEventListener('submit', async (event) => { event.preventDefault(); const { token, error } = await pandabase.vault.tokenize(card); if (error) { showError(error.message); return; } await fetch('/api/save-card', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenId: token.id }), }); }); ``` `tokenize` returns the token and its `mask`. Use the mask to display a label such as "Visa ending in 4242", and send the token ID to your server to save. The result does not include raw data.
## Element types [#element-types] | Type | Collects | Token type | | ------------------------------------- | ------------------------------------------ | -------------- | | `card` | Number, expiry, and CVC in one field | `card` | | `cardNumber`, `cardExpiry`, `cardCvc` | Each part of a card in its own field | `card` | | `bankAccount` | Account and routing numbers | `bank_account` | | `text` | A single sensitive value, such as a tax ID | `pii` | For separate card fields, create the number, expiry, and CVC fields from the same `elements` instance. Calling `tokenize` on any one of them combines all three into one card token. ```js const number = elements.create('cardNumber'); const expiry = elements.create('cardExpiry'); const cvc = elements.create('cardCvc'); number.mount('#card-number'); expiry.mount('#card-expiry'); cvc.mount('#card-cvc'); const { token } = await pandabase.vault.tokenize(number); ``` For a `text` Element, pass a `format` to validate and mask the input, such as `us_ssn` or `iban`. ## Respond to field changes [#respond-to-field-changes] Use field events to show errors and enable the submit button when the input is complete. ```js card.on('change', (event) => { submitButton.disabled = !event.complete; errorMessage.textContent = event.error ? event.error.message : ''; }); ``` | Event | Fires when | | --------------- | --------------------------------------------------------------------------------- | | `ready` | The Element has loaded and can accept input. | | `change` | The value changes. Includes `complete`, `empty`, `error`, and for cards, `brand`. | | `focus`, `blur` | The Element gains or loses focus. | ## Style the fields [#style-the-fields] Your page's CSS can't style the fields inside the iframe. Pass a `style` object when creating an Element. Set the default appearance in `base`, then add `invalid` or `complete` styles if needed. ```js const card = elements.create('card', { style: { base: { color: '#0a0f0c', fontFamily: 'Geist, sans-serif', fontSize: '15px', '::placeholder': { color: '#9aa4a0' }, }, invalid: { color: '#dc2626' }, }, }); ``` Load custom fonts by passing `fonts` to `pandabase.vault.elements()`, as a list of CSS URLs from a font provider. # Vault overview (/v2/workspace/vault) Vault is in alpha. There are no Vault fees during alpha. Vault stores card details, bank details, and other sensitive data. It gives your application a **token** that refers to the saved data. You store that token in your database and use it in later requests. Use **Elements** to collect data directly from the browser and the **proxy** to send it to a payment processor or another service. This lets your application work with tokens without collecting the raw values itself. You can also create tokens from your server or retrieve raw values with a **reveal** request. Those paths expose the data to your server and need the appropriate permissions and handling. ## How it works [#how-it-works] 1. Your server creates a **session** that lets the browser save one token. 2. The customer enters their details in **Elements**, which sends them directly to Vault. 3. Vault returns a **token**. Your page sends the token ID to your server to save. 4. To use the saved data, your server sends a request through the **proxy**. Vault replaces token references with their values before forwarding the request. Create, retrieve, update, and delete tokens. Collect cards and sensitive data in secure fields on your page. Send tokenized data to third parties without handling it yourself. Keys, permissions, access logs, and errors. ## Terms used in these guides [#terms-used-in-these-guides] | Term | What it is | | ----------- | --------------------------------------------------------------------------------------------------------------- | | Token | An ID that refers to data stored in Vault, such as a card. Your application stores it in place of the raw data. | | Mask | The non-sensitive part of the data returned with a token, such as a card's last four digits. | | Fingerprint | A value that's the same for every token holding the same data, so you can spot duplicates. | | Session | A short-lived credential that lets Elements create one token from the browser. | | Elements | Secure input fields you embed on your page to collect data. | | Proxy | Forwards your request to a third party and replaces tokens with their data on the way. | | Reveal | Returns a token's raw data to a server with permission to see it. | ## Requests and responses [#requests-and-responses] Each Workspace can have one or more vaults. Every request identifies both the Workspace and the vault: ``` https://api.pandabase.io/v2/workspaces/{workspace_id}/vaults/{vault_id} ``` Add the paths in these guides, such as `/tokens`, to that URL. For server requests, authenticate with a secret key and use `Content-Type: application/json` when sending JSON: ```sh curl https://api.pandabase.io/v2/workspaces/wks_01j9zk2m4p6r8t0v2x4z6b8d0f/vaults/vlt_01j9zm3n5q7s9u1w3y5a7c9e1g/tokens/tok_01j9zq3c7mfx8v2k5n6p4r8t0w \ -H "Authorization: Bearer sk_live_..." ``` The examples use `wks_01j9zk2m4p6r8t0v2x4z6b8d0f` and `vlt_01j9zm3n5q7s9u1w3y5a7c9e1g` as the workspace and vault IDs. Replace them with your own. * **IDs** carry a resource prefix, such as `wks_` for workspaces, `vlt_` for vaults, `tok_` for tokens, `vss_` for sessions, and `val_` for access logs. * **Times** are RFC 3339 strings, such as `2026-09-01T12:00:00Z`. * **Objects** include an `object` field naming their type. * **Metadata** is a JSON object up to 16 KiB for your own attributes. Updates merge: keys you omit stay, and a key set to `null` is removed. * **Lists** return up to `limit` items (1โ€“100, default 25). Pass `next_cursor` as `cursor` to get the next page. ```json { "object": "list", "data": [], "has_more": false, "next_cursor": null } ``` ## Keep data in separate vaults [#keep-data-in-separate-vaults] A token belongs to the vault where it was created. You can't retrieve it, reveal it, or use it in a proxy request through another vault. Use separate vaults when you need to keep collections of data apart, such as data for different products or regions. Find your Workspace and vault IDs in your Workspace settings under **Vault**. # Proxy (/v2/workspace/vault/proxy) Vault is in alpha. There are no Vault fees during alpha. The proxy sends a request from Vault to another service, such as a payment processor. Your server supplies token references. Vault replaces them with the saved values before forwarding the request. This lets you send saved data without first revealing it to your server. ## Send a proxy request [#send-a-proxy-request] Send the destination's expected request body to `/proxy`. Set `Forward-Url` to the destination URL and put `{{ token_id.field }}` where each saved value belongs. The example below uses a placeholder processor URL. Replace it with an allowed destination and use the request format that service expects. ```sh curl https://api.pandabase.io/v2/workspaces/wks_01j9zk2m4p6r8t0v2x4z6b8d0f/vaults/vlt_01j9zm3n5q7s9u1w3y5a7c9e1g/proxy \ -H "Authorization: Bearer sk_live_..." \ -H "Forward-Url: https://api.processor.example/v1/charges" \ -H "Idempotency-Key: order-1001-charge" \ -H "Content-Type: application/json" \ -d '{ "amount": 4900, "currency": "usd", "card": { "number": "{{ tok_01j9zq3c7mfx8v2k5n6p4r8t0w.number }}", "exp_month": "{{ tok_01j9zq3c7mfx8v2k5n6p4r8t0w.exp_month }}", "exp_year": "{{ tok_01j9zq3c7mfx8v2k5n6p4r8t0w.exp_year }}", "cvc": "{{ tok_01j9zq3c7mfx8v2k5n6p4r8t0w.cvc }}" } }' ``` Vault forwards the request method, body, and headers, except `Authorization`, `Forward-Url`, and headers starting with `Pandabase-`. The `Authorization` header authenticates your request to Vault. To authenticate with the destination as well, send its credentials in `Pandabase-Proxy-Authorization`. Vault forwards that value as the destination's `Authorization` header. ## Responses [#responses] Vault returns the destination's status code, headers, and response body unchanged. Check how the destination handles sensitive values before logging its response. Vault adds two headers: | Header | Description | | ---------------------------- | ----------------------------------------------------------------------------------- | | `Pandabase-Proxy-Request-Id` | The proxy request's ID, which also appears in the token's access log. | | `Pandabase-Proxy-Status` | `forwarded` if the destination was reached, or `failed` if Vault couldn't reach it. | If Vault rejects the request before forwarding it, such as for an unknown token or a destination that isn't allowed, it returns its own [error](/v2/workspace/vault/access-and-security#errors) instead. ## Token references [#token-references] | Reference | Replaced with | | --------------------------------------------------- | ---------------------------------------------------- | | `{{ tok_....number }}` | The card number. | | `{{ tok_....exp_month }}`, `{{ tok_....exp_year }}` | The expiry month and year. | | `{{ tok_....cvc }}` | The CVC, if it hasn't been used or expired yet. | | `{{ tok_....value }}` | The value of a `pii` token. | | `{{ tok_....field.path }}` | A field inside a `custom` token, using dot notation. | You can use references in the request body and header values. A single request can use more than one token. ## Allowed destinations [#allowed-destinations] Add destination hosts in your Workspace settings under **Vault โ†’ Proxy destinations** before sending requests. Destinations must use HTTPS. A request to a host outside this list returns `403 proxy_destination_not_allowed`. # Tokens (/v2/workspace/vault/tokens) Vault is in alpha. There are no Vault fees during alpha. A token refers to sensitive data saved in Vault, such as a card or bank account. Store its ID in your application so you can use the saved data in later requests. Use [Elements](/v2/workspace/vault/elements) to collect data in the browser without sending raw values through your server. The server-side creation example below is for integrations where your server already handles that data. Paths are relative to your [vault URL](/v2/workspace/vault#requests-and-responses). | Method | Endpoint | Result | | ------ | --------------------- | -------------------------------------------------------------------- | | POST | `/tokens` | `201`: token. Requires an idempotency key. | | GET | `/tokens` | `200`: list of tokens. | | GET | `/tokens/{id}` | `200`: token, without raw data. | | PATCH | `/tokens/{id}` | `200`: updated token. | | POST | `/tokens/{id}/reveal` | `200`: token with raw data. Requires the `tokens:reveal` permission. | | DELETE | `/tokens/{id}` | `200`: deletion acknowledgement. | ## Token types [#token-types] | Type | Holds | Mask | | -------------- | --------------------------------------------------- | ----------------------------------------------------- | | `card` | Card number, expiry, and optionally the CVC | Brand, last four digits, expiry, funding, and country | | `bank_account` | Account and routing numbers | Bank name, last four digits, and country | | `pii` | A personal value, such as a tax ID or date of birth | The last few characters, depending on the format | | `custom` | Any JSON object up to 16 KiB | None | ## Create a token [#create-a-token] | Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------ | | `type` | string | **Required.** `card`, `bank_account`, `pii`, or `custom`. | | `data` | object | **Required.** The sensitive data. Its shape depends on the type. | | `deduplicate` | boolean | Default `false`. Return the existing token if one already holds the same data. | | `expires_at` | timestamp | When the token and its data are deleted. Omit to keep it until you delete it. | | `metadata` | object | Your own attributes. | ```sh curl https://api.pandabase.io/v2/workspaces/wks_01j9zk2m4p6r8t0v2x4z6b8d0f/vaults/vlt_01j9zm3n5q7s9u1w3y5a7c9e1g/tokens \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: cus_123-card" \ -H "Content-Type: application/json" \ -d '{ "type": "card", "data": { "number": "4242424242424242", "exp_month": 12, "exp_year": 2030, "cvc": "123" }, "metadata": { "customer": "cus_123" } }' ``` ```json { "object": "token", "id": "tok_01j9zq3c7mfx8v2k5n6p4r8t0w", "type": "card", "fingerprint": "fp_4kq9zx2mt7vb1nr8", "mask": { "brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2030, "funding": "credit", "country": "US" }, "metadata": { "customer": "cus_123" }, "expires_at": null, "created_at": "2026-09-01T12:00:00Z", "updated_at": "2026-09-01T12:00:00Z" } ``` The CVC is available only for a [proxy request](/v2/workspace/vault/proxy). Vault deletes it after its first use or after one hour, whichever comes first. A reveal request never returns it. ## Retrieve and list tokens [#retrieve-and-list-tokens] Retrieving a token returns its type, mask, fingerprint, and metadata, but never its raw data. To narrow the list, filter by `type`, `fingerprint`, or `metadata[key]=value`. Metadata filters match top-level string values. Results show the newest tokens first. ```sh curl -G https://api.pandabase.io/v2/workspaces/wks_01j9zk2m4p6r8t0v2x4z6b8d0f/vaults/vlt_01j9zm3n5q7s9u1w3y5a7c9e1g/tokens \ -H "Authorization: Bearer sk_live_..." \ --data-urlencode "type=card" \ --data-urlencode "metadata[customer]=cus_123" ``` ### Find duplicate cards [#find-duplicate-cards] Tokens containing the same data share a `fingerprint`, even if they were created separately. Filter by that fingerprint to find duplicates. To reuse an existing token during creation, set `deduplicate: true`. Vault returns the matching token if one already exists. ## Update a token [#update-a-token] You can change a token's `metadata` and `expires_at`. Send only the fields you want to update. The token's type and raw data can't be edited. To replace the saved data, create a new token and delete the old one. ```json { "metadata": { "customer": "cus_123", "default": "true", "old_reference": null } } ``` ## Reveal a token [#reveal-a-token] A reveal request returns the raw values in the token's `data` field. It requires a secret key with `tokens:reveal`. Each reveal is recorded in the token's [access log](/v2/workspace/vault/access-and-security#access-logs). ```json { "object": "token", "id": "tok_01j9zq3c7mfx8v2k5n6p4r8t0w", "type": "card", "data": { "number": "4242424242424242", "exp_month": 12, "exp_year": 2030 }, "mask": { "brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2030, "funding": "credit", "country": "US" }, "created_at": "2026-09-01T12:00:00Z" } ``` Revealing a token sends its raw data to your server. If you only need to forward that data to another service, use the [proxy](/v2/workspace/vault/proxy) to send it directly from Vault. ## Delete a token [#delete-a-token] Deleting a token permanently erases its saved data. You can no longer retrieve or reveal the token, or use it in a proxy request. Its ID remains in the access logs. ```json { "object": "token", "id": "tok_01j9zq3c7mfx8v2k5n6p4r8t0w", "deleted": true } ```