# OurPay > Open source payment infrastructure for products, checkouts, subscriptions, and billing. ## Documentation - [API Overview](https://docs.ourpay.dev/api-reference/2026-04/introduction): Official SDK quickstarts, base URLs, authentication, pagination, rate limits, and API concepts - [API Overview](https://docs.ourpay.dev/api-reference/2026-10/introduction): Official SDK quickstarts, base URLs, authentication, pagination, rate limits, and API concepts - [API Overview](https://docs.ourpay.dev/api-reference/introduction): Official SDK quickstarts, base URLs, authentication, pagination, rate limits, and API concepts - [Product Updates](https://docs.ourpay.dev/changelog/recent): Stay up to date with the latest changes and improvements to OurPay. - [Analytics](https://docs.ourpay.dev/features/analytics): Understand how every metric in your dashboard is calculated. - [Credits Benefit](https://docs.ourpay.dev/features/benefits/credits): Create your own Credits benefit - [Custom Benefit](https://docs.ourpay.dev/features/benefits/custom): Create your own Custom benefit - [Automate Discord Invites & Roles](https://docs.ourpay.dev/features/benefits/discord-access): Sell Discord access & roles with ease - [Feature Flag Benefit](https://docs.ourpay.dev/features/benefits/feature-flags): Gate access to features using simple, API-driven feature flags - [Automate Customer File Downloads](https://docs.ourpay.dev/features/benefits/file-downloads): Offer digital file downloads with ease - [Automate Private GitHub Repo(s) Access](https://docs.ourpay.dev/features/benefits/github-access): Sell premium GitHub repository access with ease - [Automated Benefits](https://docs.ourpay.dev/features/benefits/introduction) - [Automate Customer License Key Management](https://docs.ourpay.dev/features/benefits/license-keys): Sell license key access to your service, software or APIs with ease - [Shared Slack Channel](https://docs.ourpay.dev/features/benefits/slack-shared-channel): Give customers a shared Slack channel via Slack Connect - [Embedded Payment Method](https://docs.ourpay.dev/features/checkout/embed-payment-method): Embed our payment method flow on your site - [Embedded Checkout](https://docs.ourpay.dev/features/checkout/embed): Embed our checkout directly on your site - [Checkout Links](https://docs.ourpay.dev/features/checkout/links): Persistent URLs that create a Checkout Session on visit - [Checkout Localization](https://docs.ourpay.dev/features/checkout/localization): Serve checkout in your customer's preferred language - [Checkout API](https://docs.ourpay.dev/features/checkout/session): Create checkout sessions programmatically for complete control - [Cost Events](https://docs.ourpay.dev/features/cost-insights/cost-events): Track costs by adding cost metadata to your ingested events - [Cost Traces](https://docs.ourpay.dev/features/cost-insights/cost-traces): Aggregate events by user sessions to calculate costs - [Introduction to Cost Insights](https://docs.ourpay.dev/features/cost-insights/introduction): Track costs, profits, and customer lifetime value with event-based cost tracking - [Custom Fields](https://docs.ourpay.dev/features/custom-fields): Learn how to add custom input fields to your checkout with OurPay - [Customer Management](https://docs.ourpay.dev/features/customer-management): Get insights on your customers and sales - [Customer Portal](https://docs.ourpay.dev/features/customer-portal/introduction): The self-service destination for your customers - [Navigate Customers to the Portal](https://docs.ourpay.dev/features/customer-portal/navigate-customers): Three ways your customers can reach their Customer Portal - [Customer Portal Settings](https://docs.ourpay.dev/features/customer-portal/settings): Configure what customers can do from the Customer Portal - [Discounts](https://docs.ourpay.dev/features/discounts): Create discounts on products and subscriptions - [Payout Accounts](https://docs.ourpay.dev/features/finance/accounts): Connect a Stripe Connect Express account to receive your earnings - [Account Balance & Transparent Fees](https://docs.ourpay.dev/features/finance/balance): Monitor your OurPay balance without hidden fees - [Payouts](https://docs.ourpay.dev/features/finance/payouts): Withdraw money from your OurPay account - [Affonso Affiliates with OurPay](https://docs.ourpay.dev/features/integrations/affonso) - [OurPay Integration in Fernand](https://docs.ourpay.dev/features/integrations/fernand): Learn how to sync customer and payment data from OurPay to Fernand. - [OurPay for Framer](https://docs.ourpay.dev/features/integrations/framer): The fastest way to sell digital products on your Framer site - [Purchase Power Parity with ParityDeals](https://docs.ourpay.dev/features/integrations/paritydeals): Offer products with different price across the globe - [OurPay for Raycast](https://docs.ourpay.dev/features/integrations/raycast): The fastest way to access OurPay from your keyboard - [OurPay for Zapier](https://docs.ourpay.dev/features/integrations/zapier): Connect OurPay to hundreds of other apps with Zapier - [Orders](https://docs.ourpay.dev/features/orders): Every paid transaction on OurPay is an order. - [Payment providers](https://docs.ourpay.dev/features/payment-providers): Collect PayPal centrally while each organization remains the seller and invoice issuer. - [Products](https://docs.ourpay.dev/features/products): Start selling digital products on OurPay in minutes. - [Manage Refunds](https://docs.ourpay.dev/features/refunds): You can easily refund orders on OurPay — both in full or in parts. - [Seat-Based Pricing](https://docs.ourpay.dev/features/seat-based-pricing): Sell team products with assignable seats and tiered pricing - [Single Sign-On](https://docs.ourpay.dev/features/sso): Let your team sign in to OurPay through your own identity provider, and become members automatically. - [Recovering failed payments](https://docs.ourpay.dev/features/subscriptions/failed-payments): How OurPay retries failed renewals before revoking a subscription. - [Subscriptions](https://docs.ourpay.dev/features/subscriptions/introduction): Recurring revenue on top of your products. - [Managing subscriptions](https://docs.ourpay.dev/features/subscriptions/manage): Everything you can do to a subscription after it's been created. - [Proration](https://docs.ourpay.dev/features/subscriptions/proration): Control how the price difference is billed when a subscription is upgraded or downgraded. - [Trials](https://docs.ourpay.dev/features/subscriptions/trials): Offer free trials on your subscriptions - [Tax Inclusive Pricing](https://docs.ourpay.dev/features/tax-inclusive-pricing): Control whether your product prices include or exclude tax at checkout - [Team Management](https://docs.ourpay.dev/features/team-management): Invite teammates to your organization, manage their roles, and control what they can access in the OurPay dashboard. - [Billing](https://docs.ourpay.dev/features/usage-based-billing/billing): How billing works with Usage Based - [Credits](https://docs.ourpay.dev/features/usage-based-billing/credits): Crediting customers for Usage Based Billing - [Event Ingestion](https://docs.ourpay.dev/features/usage-based-billing/event-ingestion): Ingest events from your application - [Delta Time Strategy](https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/delta-time-strategy): Ingest delta time of arbitrary execution - [Strategy Introduction](https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/ingestion-strategy): Ingestion strategies for Usage Based Billing - [LLM Strategy](https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/llm-strategy): Ingestion strategy for LLM Usage - [S3 Strategy](https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/s3-strategy): Ingestion strategy for S3 Operations - [Stream Strategy](https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/stream-strategy): Ingestion strategy for Readable & Writable Streams - [Introduction](https://docs.ourpay.dev/features/usage-based-billing/introduction): Usage based billing using ingested events - [Meters](https://docs.ourpay.dev/features/usage-based-billing/meters): Creating and managing meters for Usage Based Billing - [Integrate OurPay with Encore](https://docs.ourpay.dev/guides/encore): In this guide, we'll show you how to integrate OurPay with Encore. - [How to Grant Meter Credits After Purchase](https://docs.ourpay.dev/guides/grant-meter-credits-after-purchase): Learn how to automatically grant meter credits to users after purchase using the Meter Credits benefits or Webhooks. - [How to Grant Meter Credits Before Purchase](https://docs.ourpay.dev/guides/grant-meter-credits-before-purchase): Learn how to grant meter credits to customers before they make any purchase using the OurPay API. - [Guides](https://docs.ourpay.dev/guides/introduction): Step-by-step walkthroughs for implementing common OurPay patterns. - [Integrate OurPay with Laravel](https://docs.ourpay.dev/guides/laravel): In this guide, we'll show you how to integrate OurPay with Laravel. - [Migrate Seat-Based Pricing to the Member Model](https://docs.ourpay.dev/guides/migrate-seat-based-to-member-model): What changes when your organization moves to the member model, and how to update your integration - [Integrate OurPay with Next.js](https://docs.ourpay.dev/guides/nextjs): In this guide, we'll show you how to integrate OurPay with Next.js. - [Implementing Seat-Based Pricing](https://docs.ourpay.dev/guides/seat-based-pricing): Complete guide to implementing team products with seat-based pricing - [Authentication](https://docs.ourpay.dev/integrate/authentication) - [Customer State](https://docs.ourpay.dev/integrate/customer-state): The quickest way to integrate billing in your application - [OurPay over Model Context Protocol (MCP)](https://docs.ourpay.dev/integrate/mcp): Extend the capabilities of your AI agents with OurPay's MCP Server - [Organization Access Tokens](https://docs.ourpay.dev/integrate/oat) - [OAuth 2.0 Connect](https://docs.ourpay.dev/integrate/oauth2/connect) - [Introduction](https://docs.ourpay.dev/integrate/oauth2/introduction): For partners building services and extensions for OurPay customers - [Create an OAuth 2.0 Client](https://docs.ourpay.dev/integrate/oauth2/setup) - [Sandbox Environment](https://docs.ourpay.dev/integrate/sandbox) - [Astro](https://docs.ourpay.dev/integrate/sdk/adapters/astro): Payments and Checkouts made dead simple with Astro - [BetterAuth](https://docs.ourpay.dev/integrate/sdk/adapters/better-auth): Payments and Checkouts made dead simple with BetterAuth - [Deno](https://docs.ourpay.dev/integrate/sdk/adapters/deno): Payments and Checkouts made dead simple with Deno - [Elysia](https://docs.ourpay.dev/integrate/sdk/adapters/elysia): Payments and Checkouts made dead simple with Elysia - [Express](https://docs.ourpay.dev/integrate/sdk/adapters/express): Payments and Checkouts made dead simple with Express - [Fastify](https://docs.ourpay.dev/integrate/sdk/adapters/fastify): Payments and Checkouts made dead simple with Fastify - [Hono](https://docs.ourpay.dev/integrate/sdk/adapters/hono): Payments and Checkouts made dead simple with Hono - [Laravel](https://docs.ourpay.dev/integrate/sdk/adapters/laravel): Payments and Checkouts made dead simple with Laravel - [Next.js](https://docs.ourpay.dev/integrate/sdk/adapters/nextjs): Payments and Checkouts made dead simple with Next.js - [Nuxt](https://docs.ourpay.dev/integrate/sdk/adapters/nuxt): Payments and Checkouts made dead simple with Nuxt - [Remix](https://docs.ourpay.dev/integrate/sdk/adapters/remix): Payments and Checkouts made dead simple with Remix - [Supabase](https://docs.ourpay.dev/integrate/sdk/adapters/supabase): Payments and Checkouts made dead simple with Supabase - [Sveltekit](https://docs.ourpay.dev/integrate/sdk/adapters/sveltekit): Payments and Checkouts made dead simple with Sveltekit - [TanStack Start](https://docs.ourpay.dev/integrate/sdk/adapters/tanstack-start): Payments and Checkouts made dead simple with TanStack Start - [Python SDK](https://docs.ourpay.dev/integrate/sdk/python): Fully typed Python client for the OurPay API - [TypeScript SDK](https://docs.ourpay.dev/integrate/sdk/typescript): SDK for JavaScript runtimes (Node.js and Browser) - [Handle & monitor webhook deliveries](https://docs.ourpay.dev/integrate/webhooks/delivery): How to parse, validate and handle webhooks and monitor their - [Setup Webhooks](https://docs.ourpay.dev/integrate/webhooks/endpoints): Get notifications asynchronously when events occur instead of - [Webhook Events](https://docs.ourpay.dev/integrate/webhooks/events): Our webhook events and in which context they are useful - [Integrating Webhooks Locally](https://docs.ourpay.dev/integrate/webhooks/locally): Setup Webhook delivery to your local machine - [OurPay: Turn Your Software into a Business](https://docs.ourpay.dev/introduction): The next generation unicorns will be built by smaller teams. OurPay makes that dream possible. - [Acceptable use](https://docs.ourpay.dev/merchant-of-record/acceptable-use): Products and services that can be sold through OurPay. - [account-reviews](https://docs.ourpay.dev/merchant-of-record/account-reviews) - [Fees](https://docs.ourpay.dev/merchant-of-record/fees): Transparent, public pricing — pick the plan that fits - [Merchant of Record](https://docs.ourpay.dev/merchant-of-record/introduction): An open source and transparent Merchant of Record - [supported-countries](https://docs.ourpay.dev/merchant-of-record/supported-countries) - [Migrate Away from OurPay](https://docs.ourpay.dev/migrate-away): Move your customers and saved payment methods to another payment provider whenever you choose. - [Migrate to OurPay](https://docs.ourpay.dev/migrate): Get set up on OurPay in minutes from an existing store - [Support](https://docs.ourpay.dev/support) # API Overview Source: https://docs.ourpay.dev/api-reference/2026-04/introduction `https://api.ourpay.dev/v1` `https://sandbox-api.ourpay.workers.dev/v1` Use an **Organization Access Token (OAT)** in the `Authorization: Bearer` header Use a **Customer Access Token** created via `/v1/customer-sessions/` ## Official SDKs Use our new, fully typed SDKs to integrate with the OurPay API from TypeScript or Python. The SDKs are currently in public preview. Install the pre-release explicitly to try them before the stable release. Create an [organization access token](/integrate/oat), then install the SDK and make your first request: ```bash npm npm install @ourpay-dev/sdk@next ``` ```typescript app.ts import { createOurPay } from "@ourpay-dev/sdk/2026-04"; const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, }); const customerState = await ourpay.customers.getStateExternal("customer_external_id"); console.log(customerState); ``` ```bash uv uv add ourpay-sdk --prerelease allow ``` ```bash pip pip install --pre ourpay-sdk ``` ```python main.py import os from ourpay.v2026_04 import OurPay ourpay = OurPay(os.environ["OURPAY_ACCESS_TOKEN"]) customer_state = ourpay.customers.get_state_external("customer_external_id") print(customer_state) ``` Both clients use production by default. Pass `environment="sandbox"` in Python or `environment: "sandbox"` in TypeScript to use the [sandbox environment](/integrate/sandbox). ## Base URLs | Environment | Base URL | Purpose | | ----------- | --------------------------------- | ------------------------------- | | Production | `https://api.ourpay.dev/v1` | Real customers & live payments | | Sandbox | `https://sandbox-api.ourpay.workers.dev/v1` | Safe testing & integration work | The sandbox environment is fully isolated—data, users, tokens, and organizations created there do not affect production. Create separate tokens in each environment. Read more: [Sandbox Environment](/integrate/sandbox) ## Authentication ### Organization Access Tokens (OAT) Use an **OAT** to act on behalf of your organization (manage products, prices, checkouts, orders, subscriptions, benefits, etc.). ```http Authorization: Bearer ourpay_oat_xxxxxxxxxxxxxxxxx ``` Create OATs in your organization settings. See: [Organization Access Tokens](/integrate/oat) Never expose an OAT in client-side code, public repos, or logs. If leaked, it will be revoked automatically by our secret scanning integrations. ### Customer Access Tokens Do **not** use OATs in the browser. For customer-facing flows, [generate a **Customer Session**](/api-reference/2026-04/customer-sessions/create-customer-session) server-side, then use the returned **customer access token** with the **Customer Portal API** to let a signed-in customer view their own orders, subscriptions, and benefits. ## Core API vs Customer Portal API | Aspect | Core API | Customer Portal API | | -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | | Audience | Your server / backend | One of your customer | | Auth Type | Organization Access Token (OAT) | Customer Access Token | | Scope | Full org resources (products, orders, subscriptions, benefits, checkout) | Only the authenticated customer’s data | | Typical Use | Admin dashboards, internal tools, automation, provisioning | Building a custom customer portal or gated app | | Token Creation | Via dashboard (manual) | Via `/v1/customer-sessions/` (server-side) | | Sensitive Operations | Yes (create/update products, issue refunds, etc.) | No (read/update only what the customer owns) | The Customer Portal API is a *restricted* surface designed for safe exposure in user-facing contexts (after exchanging a session). It cannot perform privileged org-level mutations like creating products or issuing refunds. ## Quick Examples ```bash curl (Production - Core API) curl https://api.ourpay.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```bash curl (Sandbox - Core API) curl https://sandbox-api.ourpay.workers.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT_SANDBOX" \ -H "Accept: application/json" ``` ```bash curl (Customer Portal API) curl https://api.ourpay.dev/v1/customer-portal/orders/ \ -H "Authorization: Bearer $OURPAY_CUSTOMER_TOKEN" \ -H "Accept: application/json" ``` ## Pagination List endpoints in the OurPay API support pagination to help you efficiently retrieve large datasets. Use the `page` and `limit` query parameters to control pagination. ### Query Parameters | Parameter | Type | Default | Max | Description | | --------- | ------- | ------- | --- | ---------------------------------------------------------------- | | `page` | integer | `1` | - | Page number, starting from 1 | | `limit` | integer | `10` | `100` | Number of items to return per page (window size) | The `page` parameter works as a window offset. For example, `page=2&limit=10` means the API will skip the first 10 elements and return the next 10. ### Response Format All paginated responses include a `pagination` object with metadata about the current page and total results: | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------------- | | `total_count` | integer | Total number of items matching your query across all pages | | `max_page` | integer | Total number of pages available, given the current `limit` value | ### Example Let's say you want to fetch products with a limit of 100 items per page: ```bash Request curl https://api.ourpay.dev/v1/products/?page=1&limit=100 \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```json Response { "items": [ { "id": "...", "name": "Product 1", ... }, ... ], "pagination": { "total_count": 250, "max_page": 3 } } ``` In this example: - `total_count=250` indicates there are 250 total products - `limit=100` means each page contains up to 100 products - `max_page=3` means you need to make 3 requests to retrieve all products (pages 1, 2, and 3) To retrieve all pages, increment the `page` parameter from `1` to `max_page`. Our SDKs provide built-in pagination helpers to automatically iterate through all pages. ## Rate Limits OurPay API has rate limits to ensure fair usage and maintain performance. Limits differ between the **Sandbox** and **Production** environments. ### Production - **500 requests per minute** per organization/customer or OAuth2 Client. ### Sandbox - **100 requests per minute** per organization/customer or OAuth2 Client. Unauthenticated [validation](/api-reference/2026-04/customer_portal/validate-license-key), [activation](/api-reference/2026-04/customer_portal/activate-license-key), and [deactivation](/api-reference/2026-04/customer_portal/deactivate-license-key) endpoints are limited to **3 requests per second** in both environments. If you exceed the rate limit, you will receive a `429 Too Many Requests` response. The response will include a `Retry-After` header indicating how long you should wait before making another request. Organizations requiring higher rate limits for production workloads may contact our support team to discuss elevated limits. # API Overview Source: https://docs.ourpay.dev/api-reference/2026-10/introduction `https://api.ourpay.dev/v1` `https://sandbox-api.ourpay.workers.dev/v1` Use an **Organization Access Token (OAT)** in the `Authorization: Bearer` header Use a **Customer Access Token** created via `/v1/customer-sessions/` ## Official SDKs Use our new, fully typed SDKs to integrate with the OurPay API from TypeScript or Python. The SDKs are currently in public preview. Install the pre-release explicitly to try them before the stable release. Create an [organization access token](/integrate/oat), then install the SDK and make your first request: ```bash npm npm install @ourpay-dev/sdk@next ``` ```typescript app.ts import { createOurPay } from "@ourpay-dev/sdk/2026-10"; const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, }); const customerState = await ourpay.customers.getStateExternal("customer_external_id"); console.log(customerState); ``` ```bash uv uv add ourpay-sdk --prerelease allow ``` ```bash pip pip install --pre ourpay-sdk ``` ```python main.py import os from ourpay.v2026_10 import OurPay ourpay = OurPay(os.environ["OURPAY_ACCESS_TOKEN"]) customer_state = ourpay.customers.get_state_external("customer_external_id") print(customer_state) ``` Both clients use production by default. Pass `environment="sandbox"` in Python or `environment: "sandbox"` in TypeScript to use the [sandbox environment](/integrate/sandbox). ## Base URLs | Environment | Base URL | Purpose | | ----------- | --------------------------------- | ------------------------------- | | Production | `https://api.ourpay.dev/v1` | Real customers & live payments | | Sandbox | `https://sandbox-api.ourpay.workers.dev/v1` | Safe testing & integration work | The sandbox environment is fully isolated—data, users, tokens, and organizations created there do not affect production. Create separate tokens in each environment. Read more: [Sandbox Environment](/integrate/sandbox) ## Authentication ### Organization Access Tokens (OAT) Use an **OAT** to act on behalf of your organization (manage products, prices, checkouts, orders, subscriptions, benefits, etc.). ```http Authorization: Bearer ourpay_oat_xxxxxxxxxxxxxxxxx ``` Create OATs in your organization settings. See: [Organization Access Tokens](/integrate/oat) Never expose an OAT in client-side code, public repos, or logs. If leaked, it will be revoked automatically by our secret scanning integrations. ### Customer Access Tokens Do **not** use OATs in the browser. For customer-facing flows, [generate a **Customer Session**](/api-reference/2026-10/customer-sessions/create-customer-session) server-side, then use the returned **customer access token** with the **Customer Portal API** to let a signed-in customer view their own orders, subscriptions, and benefits. ## Core API vs Customer Portal API | Aspect | Core API | Customer Portal API | | -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | | Audience | Your server / backend | One of your customer | | Auth Type | Organization Access Token (OAT) | Customer Access Token | | Scope | Full org resources (products, orders, subscriptions, benefits, checkout) | Only the authenticated customer’s data | | Typical Use | Admin dashboards, internal tools, automation, provisioning | Building a custom customer portal or gated app | | Token Creation | Via dashboard (manual) | Via `/v1/customer-sessions/` (server-side) | | Sensitive Operations | Yes (create/update products, issue refunds, etc.) | No (read/update only what the customer owns) | The Customer Portal API is a *restricted* surface designed for safe exposure in user-facing contexts (after exchanging a session). It cannot perform privileged org-level mutations like creating products or issuing refunds. ## Quick Examples ```bash curl (Production - Core API) curl https://api.ourpay.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```bash curl (Sandbox - Core API) curl https://sandbox-api.ourpay.workers.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT_SANDBOX" \ -H "Accept: application/json" ``` ```bash curl (Customer Portal API) curl https://api.ourpay.dev/v1/customer-portal/orders/ \ -H "Authorization: Bearer $OURPAY_CUSTOMER_TOKEN" \ -H "Accept: application/json" ``` ## Pagination List endpoints in the OurPay API support pagination to help you efficiently retrieve large datasets. Use the `page` and `limit` query parameters to control pagination. ### Query Parameters | Parameter | Type | Default | Max | Description | | --------- | ------- | ------- | --- | ---------------------------------------------------------------- | | `page` | integer | `1` | - | Page number, starting from 1 | | `limit` | integer | `10` | `100` | Number of items to return per page (window size) | The `page` parameter works as a window offset. For example, `page=2&limit=10` means the API will skip the first 10 elements and return the next 10. ### Response Format All paginated responses include a `pagination` object with metadata about the current page and total results: | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------------- | | `total_count` | integer | Total number of items matching your query across all pages | | `max_page` | integer | Total number of pages available, given the current `limit` value | ### Example Let's say you want to fetch products with a limit of 100 items per page: ```bash Request curl https://api.ourpay.dev/v1/products/?page=1&limit=100 \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```json Response { "items": [ { "id": "...", "name": "Product 1", ... }, ... ], "pagination": { "total_count": 250, "max_page": 3 } } ``` In this example: - `total_count=250` indicates there are 250 total products - `limit=100` means each page contains up to 100 products - `max_page=3` means you need to make 3 requests to retrieve all products (pages 1, 2, and 3) To retrieve all pages, increment the `page` parameter from `1` to `max_page`. Our SDKs provide built-in pagination helpers to automatically iterate through all pages. ## Rate Limits OurPay API has rate limits to ensure fair usage and maintain performance. Limits differ between the **Sandbox** and **Production** environments. ### Production - **500 requests per minute** per organization/customer or OAuth2 Client. ### Sandbox - **100 requests per minute** per organization/customer or OAuth2 Client. Unauthenticated [validation](/api-reference/2026-10/customer_portal/validate-license-key), [activation](/api-reference/2026-10/customer_portal/activate-license-key), and [deactivation](/api-reference/2026-10/customer_portal/deactivate-license-key) endpoints are limited to **3 requests per second** in both environments. If you exceed the rate limit, you will receive a `429 Too Many Requests` response. The response will include a `Retry-After` header indicating how long you should wait before making another request. Organizations requiring higher rate limits for production workloads may contact our support team to discuss elevated limits. # API Overview Source: https://docs.ourpay.dev/api-reference/introduction `https://api.ourpay.dev/v1` `https://sandbox-api.ourpay.workers.dev/v1` Use an **Organization Access Token (OAT)** in the `Authorization: Bearer` header Use a **Customer Access Token** created via `/v1/customer-sessions/` ## Official SDKs Use our new, fully typed SDKs to integrate with the OurPay API from TypeScript or Python. The SDKs are currently in public preview. Install the pre-release explicitly to try them before the stable release. Create an [organization access token](/integrate/oat), then install the SDK and make your first request: ```bash npm npm install @ourpay-dev/sdk@next ``` ```typescript app.ts import { createOurPay } from "@ourpay-dev/sdk/2026-10"; const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, }); const customerState = await ourpay.customers.getStateExternal("customer_external_id"); console.log(customerState); ``` ```bash uv uv add ourpay-sdk --prerelease allow ``` ```bash pip pip install --pre ourpay-sdk ``` ```python main.py import os from ourpay.v2026_10 import OurPay ourpay = OurPay(os.environ["OURPAY_ACCESS_TOKEN"]) customer_state = ourpay.customers.get_state_external("customer_external_id") print(customer_state) ``` Both clients use production by default. Pass `environment="sandbox"` in Python or `environment: "sandbox"` in TypeScript to use the [sandbox environment](/integrate/sandbox). ## Base URLs | Environment | Base URL | Purpose | | ----------- | --------------------------------- | ------------------------------- | | Production | `https://api.ourpay.dev/v1` | Real customers & live payments | | Sandbox | `https://sandbox-api.ourpay.workers.dev/v1` | Safe testing & integration work | The sandbox environment is fully isolated—data, users, tokens, and organizations created there do not affect production. Create separate tokens in each environment. Read more: [Sandbox Environment](/integrate/sandbox) ## Authentication ### Organization Access Tokens (OAT) Use an **OAT** to act on behalf of your organization (manage products, prices, checkouts, orders, subscriptions, benefits, etc.). ```http Authorization: Bearer ourpay_oat_xxxxxxxxxxxxxxxxx ``` Create OATs in your organization settings. See: [Organization Access Tokens](/integrate/oat) Never expose an OAT in client-side code, public repos, or logs. If leaked, it will be revoked automatically by our secret scanning integrations. ### Customer Access Tokens Do **not** use OATs in the browser. For customer-facing flows, [generate a **Customer Session**](/api-reference/customer-sessions/create-customer-session) server-side, then use the returned **customer access token** with the **Customer Portal API** to let a signed-in customer view their own orders, subscriptions, and benefits. ## Core API vs Customer Portal API | Aspect | Core API | Customer Portal API | | -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | | Audience | Your server / backend | One of your customer | | Auth Type | Organization Access Token (OAT) | Customer Access Token | | Scope | Full org resources (products, orders, subscriptions, benefits, checkout) | Only the authenticated customer’s data | | Typical Use | Admin dashboards, internal tools, automation, provisioning | Building a custom customer portal or gated app | | Token Creation | Via dashboard (manual) | Via `/v1/customer-sessions/` (server-side) | | Sensitive Operations | Yes (create/update products, issue refunds, etc.) | No (read/update only what the customer owns) | The Customer Portal API is a *restricted* surface designed for safe exposure in user-facing contexts (after exchanging a session). It cannot perform privileged org-level mutations like creating products or issuing refunds. ## Quick Examples ```bash curl (Production - Core API) curl https://api.ourpay.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```bash curl (Sandbox - Core API) curl https://sandbox-api.ourpay.workers.dev/v1/products/ \ -H "Authorization: Bearer $OURPAY_OAT_SANDBOX" \ -H "Accept: application/json" ``` ```bash curl (Customer Portal API) curl https://api.ourpay.dev/v1/customer-portal/orders/ \ -H "Authorization: Bearer $OURPAY_CUSTOMER_TOKEN" \ -H "Accept: application/json" ``` ## Pagination List endpoints in the OurPay API support pagination to help you efficiently retrieve large datasets. Use the `page` and `limit` query parameters to control pagination. ### Query Parameters | Parameter | Type | Default | Max | Description | | --------- | ------- | ------- | --- | ---------------------------------------------------------------- | | `page` | integer | `1` | - | Page number, starting from 1 | | `limit` | integer | `10` | `100` | Number of items to return per page (window size) | The `page` parameter works as a window offset. For example, `page=2&limit=10` means the API will skip the first 10 elements and return the next 10. ### Response Format All paginated responses include a `pagination` object with metadata about the current page and total results: | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------------- | | `total_count` | integer | Total number of items matching your query across all pages | | `max_page` | integer | Total number of pages available, given the current `limit` value | ### Example Let's say you want to fetch products with a limit of 100 items per page: ```bash Request curl https://api.ourpay.dev/v1/products/?page=1&limit=100 \ -H "Authorization: Bearer $OURPAY_OAT" \ -H "Accept: application/json" ``` ```json Response { "items": [ { "id": "...", "name": "Product 1", ... }, ... ], "pagination": { "total_count": 250, "max_page": 3 } } ``` In this example: - `total_count=250` indicates there are 250 total products - `limit=100` means each page contains up to 100 products - `max_page=3` means you need to make 3 requests to retrieve all products (pages 1, 2, and 3) To retrieve all pages, increment the `page` parameter from `1` to `max_page`. Our SDKs provide built-in pagination helpers to automatically iterate through all pages. ## Rate Limits OurPay API has rate limits to ensure fair usage and maintain performance. Limits differ between the **Sandbox** and **Production** environments. ### Production - **500 requests per minute** per organization/customer or OAuth2 Client. ### Sandbox - **100 requests per minute** per organization/customer or OAuth2 Client. Unauthenticated [validation](/api-reference/customer_portal/validate-license-key), [activation](/api-reference/customer_portal/activate-license-key), and [deactivation](/api-reference/customer_portal/deactivate-license-key) endpoints are limited to **3 requests per second** in both environments. If you exceed the rate limit, you will receive a `429 Too Many Requests` response. The response will include a `Retry-After` header indicating how long you should wait before making another request. Organizations requiring higher rate limits for production workloads may contact our support team to discuss elevated limits. # Product Updates Source: https://docs.ourpay.dev/changelog/recent ## Embed hosts Choose which hosts are allowed to embed your checkout, in Settings → Preferences → Embedding. As soon as you list a host, only the hosts on your list can embed. Organizations created from 4 August 2026 need a list before they can embed. Existing organizations carry on as before until they add their first host, and we will email you well ahead of the date this applies to everyone. [Read more](/features/checkout/embed#embed-hosts) ## Per-customer discount limits Discount codes now take a per-customer redemption limit. Set it to one and you can share a code publicly (post it, put it in a newsletter, hand it to an affiliate) knowing each buyer only gets a single use. OurPay identifies a customer by customer ID, email address (ignoring plus-aliases) or payment card, and a match on any one is enough — so a fresh address on the same card doesn't buy a second run. Set it under Products → Discounts, or send `max_redemptions_per_customer` when you create or update a discount through the API. [Read more](/features/discounts#per-customer-limits) ## Pause and resume subscriptions You can now pause a subscription instead of canceling it. Pausing schedules the subscription to stop at the end of its current period: billing halts and benefits are revoked, but the subscription and its payment method stay on file. Resume whenever you're ready: a fresh billing period starts and the customer is charged immediately, with no new checkout. Set an optional resume date when you pause and the subscription comes back on its own. This fits seasonal plans and account "freezes", where a customer steps away and returns later. Drive it from the API through the [Update Subscription](/api-reference/subscriptions/update-subscription) endpoint, or let customers pause themselves from the Customer Portal by turning on **Enable subscription pause** under Settings → Customer portal. Two new webhook events, `subscription.paused` and `subscription.resumed`, tell you when the state changes. [Read more](/features/subscriptions/manage#pause-and-resume) ## Single Sign-On Connect your organization to your own identity provider over OpenID Connect, including Google Workspace, Okta, Microsoft Entra ID, and Keycloak. Your team signs in with the credentials they already use, and anyone who authenticates becomes a member of your organization, so there are no invitations to send. Enforce SSO to make your identity provider the only way in: revoke someone's access there and they lose access to OurPay. Single Sign-On is included in the Scale plan. Set it up under Settings → SSO. [Read more](/features/sso) ## Added support for new local payment methods We've enabled several additional local payment methods. These payment methods are automatically displayed at checkout when they're available for a customer's location and purchase. No action is required from merchants. Newly supported: * 🇧🇪 Bancontact (EUR, one-time purchases) * 🇵🇱 BLIK (EUR, one-time purchases) * 🇦🇹 EPS (EUR, one-time purchases) * 🇳🇱 iDEAL / Wero (EUR, one-time purchases) * 🇵🇱 Przelewy24 (EUR, one-time purchases) * 🇪🇸 Bizum (EUR, one-time purchases) * 🇮🇳 UPI (INR, one-time purchases and recurring subscriptions) ## Pending Subscription Updates Now Surface in the UI Scheduled subscription changes (product swaps, price updates, seat-count changes) now appear in both the merchant dashboard and the customer portal. A clear "Scheduled Update" section shows what will change and when it applies, so there are no surprises at the next billing cycle. ## Filter License Keys and Benefit Grants by Status Merchants can now filter license keys and benefit grants by status (granted, revoked, disabled, pending) directly from the benefit detail page. The license key status filter is also exposed via the API. Finding revoked or pending entitlements across large customer bases is now a single dropdown click instead of scrolling pages of results. ## Invoice Support for Arabic, Hebrew, and CJK Invoice PDFs now render Arabic, Hebrew, and CJK (Chinese, Japanese, Korean) glyphs correctly, with proper right-to-left shaping for Arabic and Hebrew. Merchants invoicing customers in these languages get professional, readable documents out of the box. ## Expanded Currency Support Presentment currency coverage expanded by 80+ currencies, including EGP, KES, NGN, PKR, VND, UAH, and KZT. Merchants can now price products and display checkout in a much wider range of local currencies. ## Smarter Regional Currency Formatting Localized checkout now respects regional IETF subtags (e.g. `en-CA`, `en-AU`) while keeping the merchant's chosen language. Currency symbols render as `CA$` or `A$` instead of a generic `$`, so buyers see the right currency framing for their region. ## Meter Units Meters now support custom units with a value multiplier, so you can ingest raw counts (tokens, requests, bytes) and price in whatever unit makes sense for your customers (millions of tokens, GB, etc.). Configure the unit name and multiplier on each meter in the dashboard, and unit labels flow through to checkout, the customer portal, and invoices. ## Self-Service Payout Account Management Merchants can now create, switch between, and delete payout accounts directly from the Finance page. A new Manage modal lists every account on the user, lets you set the active one for the organization, and blocks deletion when an account still holds a balance or is linked to an organization. Useful for moving payouts to a new bank entity without contacting support. ## Customers Can Update Their Email Address Customers can now change the email address on their account directly from the customer portal, with a verification step on the new address. No more support tickets to fix a typo or move billing to a new mailbox. ## Cost Insights: Customer Ranking and Variance Detection Cost Insights now ranks customers by total cost so you can see which accounts are driving the bill, and a new variance API surfaces cost anomalies (sudden spikes or drops) on individual events. Cost values also render correctly on the leaf event detail page. An enhancement to the Cost Insights feature shipped last October. ## Customizable Dashboard Home Charts The dashboard home page now lets merchants choose which metrics they see on the overview chart and persist their selection. Tailor the first thing you see when you log in to whatever moves the business for you. ## Schedule Subscription Updates for Next Cycle Subscription changes (product swaps, price updates, seat changes) can now be scheduled to apply at the next billing cycle instead of taking effect immediately. The API exposes a new `next_period` proration behavior, and pending updates are returned on subscription objects and webhooks so external systems stay in sync. ## Tax-Inclusive Pricing Merchants can now set a default tax behavior on their organization and price products with tax included in the displayed amount. Useful for regions and product categories where buyers expect to see the all-in price up front. ## Renewal and Trial Conversion Reminders Customers on yearly or longer billing cycles now get an automatic reminder seven days before renewal. Trial subscribers get a heads-up three days before their trial ends (or one day for very short trials). Both reminders are toggleable per organization, helping reduce involuntary churn and surprise charges. ## GDPR Customer Data Export A new Privacy section in the customer portal lets buyers download a complete JSON export of their personal data, subscriptions, orders, and benefit grants in one click. Helps merchants satisfy data portability obligations without manual exports. ## Cancellation Reason Drill-Down Cancellation charts in the metrics dashboard are now interactive. Clicking on any reason or time bucket opens a detail modal showing the underlying subscriptions, customer comments, and dates. Merchants can finally connect a spike on the chart to the actual customers behind it. ## Korean Checkout Checkout is now translated into Korean. Merchants selling to Korean buyers get a native buying experience, with product details, payment fields, and order summary all rendering in Korean. ## Multi-Currency Fixed Discounts Fixed-amount discounts can now hold separate amounts per currency, matching the multi-currency pricing model that shipped in February. Run a $10 / 10 EUR / 10 GBP coupon as a single discount instead of juggling three. ## Multi-Currency Product Pricing Merchants can now create products with prices in multiple currencies side by side. Set a USD price, an EUR price, a GBP price, and more on the same product, and customers see the right currency at checkout based on their region. A new default presentment currency setting at the organization level keeps things consistent across the dashboard. ## Italian and Portuguese Checkout Checkout, customer portal, and emails are now fully translated into Italian and Portuguese. Customers in Italy, Portugal, and Brazil get a native experience end to end, with currency formatting that matches their region. ## Feature Flag Benefit A new Feature Flag benefit type is available for SaaS merchants. Grant it to customers when they should unlock something in your product, then check entitlements through the OurPay API or webhooks. JSON metadata stays invisible to the customer, keeping the experience clean for pure software subscriptions. ## Faster Wallet and Card Checkout When mobile wallets are available, they now appear at the top of the payment list instead of buried under the card form. The cardholder name field is hidden when paying with a wallet, since the name is pulled from the wallet token automatically. Fewer clicks, fewer fields, faster checkout. ## Team Ownership Transfer Team owners can now hand the owner role to another member directly from the customer portal, and admins can transfer ownership through the API. The previous owner is automatically demoted to billing manager so there is always exactly one owner. ## Seat Limits at Checkout Seat-based products now support `min_seats` and `max_seats` on the checkout. Lock customers to a minimum team size for plans that require it, or cap the upper bound for tiered packages, all enforced at the buying step. ## Name Your Webhook Endpoints Webhook endpoints can now have a friendly name like "Production Events" or "Analytics Pipeline". Names show up as the primary label in the dashboard, with the URL as secondary text, so managing more than a couple of endpoints stops feeling like reading a list of URLs. ## Return URL on Static Checkout Links Static checkout links created from the dashboard now support a custom return URL, matching what was already possible through the API. Send buyers back to a thank-you page, an upsell, or a specific app screen after they complete a purchase, no API call required. ## Team Member Management (B2B) Complete team member management system for B2B customers: * Owner and billing managers can add/remove team members * Role-based access control (owner, billing_manager, member) * Member-specific benefit grants and portal access * Team portal page with member list and management * Member sessions (`ourpay_mst_`) preserve identity and permissions * Members tab on customer detail pages Perfect for B2B customers managing team access and permissions. ### Explicit Currency Display Currency symbols now show explicitly (e.g., `USD 123.45`) instead of just `$123.45` to prevent confusion for non-US dollar users. ## Event & Metering Enhancements ### Member-Level Event Attribution Events now support member tracking: * `member_id` and `external_member_id` fields * Member validation during event ingestion * B2B usage tracking at member level ### Timezone Handling Fixed timezone issues in meter quantities endpoint: * Added `timezone` parameter * Set database session timezone properly * Correct date grouping for non-UTC users ## Customer Portal Enhancements ### Benefit Grants UI * Complex benefit grants list with search and pagination * Better display for benefit details * Improved OAuth benefit grant buttons ### Payment Retry Fixes * 3DS modal display (popup instead of redirect) * Retry persistence after 3DS cancellation * Proper dunning management on manual retries ## Discount Management ### Redemption Count Optimization * Added `redemptions_count` column to discounts table ## System Events for Subscriptions Expanded system events to provide detailed subscription lifecycle tracking: * **Subscription cycled** - when billing periods roll over * **Subscription canceled** - when subscriptions are canceled * **Subscription past due** - when payment fails (separate from permanent revocation) * All events include product IDs and relevant metadata * Perfect for analytics and building custom workflows ## iOS Widget New iOS home screen and lock screen widget: * Real-time revenue display * Support for dark and light mode * Multiple widget sizes * Quick glance at your sales without opening the app Built with `@bacons/apple-targets` for native iOS integration. ## Product Management Improvements Major UX improvements for managing products: ### Sorting & Filtering * Sort products by name, creation date, update date, and end date * Custom pagination size (20, 50, 100 items per page) * Product rows are clickable links for cmd+click support ### Editing Experience * Unsaved changes alert prevents accidental data loss * Update button at top right for easier access * Auto-save for most settings * Fixed empty state flashing when sorting/filtering ### File Management * Client-side pagination for downloadable benefit files (10 per page) * Better file upload error messages * Fixed React state issues in benefit forms ### Product Duplication * Re-uploads images when duplicating to prevent shared references * Deleting an image from a duplicate no longer affects the original ## Search Improvements Significant search infrastructure updates: * Search vectors for full-text search * Union queries for better performance * PostgreSQL text search capabilities ## Member Management Enhanced member endpoint functionality: * Added `member_id` to relevant API endpoints * Improved B2B customer management ## Mobile App Improvements ### iOS Updates * iOS widget for home and lock screen * App rating prompts at appropriate times * Login screen animations (fade in, Ken Burns effect) * Fixed multi-account crash on settings * Fixed scroll issues with many organizations * Version bumped to 1.1.0 ### Android Support * Full Android build support * App store ready for both platforms * Consistent design system across iOS and Android ## Customer Meter Enhancements Significant improvements to customer meter calculation: * Speed improvements with optimized queries (avoided cartesian joins) * Better activation logic for meters with events * Fixed organization ID tracking * Increased lock time to 30s for slow queries * Debug information in window event spans * Proper external customer ID handling ## Webhook Management Better webhook health tracking: * Discord rate limit logging * Missing event types added to schema * Improved error handling * Repository-based webhook service ## Checkout Links * Added checkout links to mobile app * Better link management * Share and track checkout performance on mobile ## OAuth2 Improvements * First-party clients auto-save grants * No authorization prompt for trusted apps * Better user experience for internal tools ## Bug Fixes * Fixed undefined organization in dashboard sidebar * Fixed customer meter updates for soft-deleted customers * Fixed seat and trial end updates for seat-based products * Fixed tax calculation error handling * Fixed product empty state flashing * Fixed notifications on iOS simulator * Fixed Switch rendering on Android * Fixed OurPay logo not linking to home * Fixed image duplication on product creation * Handle null Stripe refund reasons * Fixed subscription fields in system events ## Dispute & Chargeback Management Complete dispute tracking and prevention system: * Dispute model to track chargebacks from Stripe * Chargeback Stop integration for rapid dispute resolution (RDR) * Auto-refund on RDR disputes to prevent chargebacks * Link refunds to disputes for better tracking * Improved dispute event handling from Stripe webhooks Helps reduce chargeback fees and protect your account standing. ## Metrics API New metrics filtering system: * `metrics` query parameter to filter specific metrics * Removed deprecated `focus_metrics` parameter * Better performance by only calculating requested metrics ## iOS App Updates * App rating prompts at appropriate times * iOS upsell banner on homepage * Animations on login screen (fade in, Ken Burns effect, staggered elements) * Fixed multi-account crash on settings screen * Fixed onboarding redirect after org creation * Fixed scroll issue with many orgs * Version bumped to 1.1.0 ## Mobile App (Android) Added Android support: * Android targets in build scripts * App store ready for both iOS and Android ## Email Improvements * Updated OurPay logo in all emails * Better email DNS validation * Fixed invalid email domain issues ## Worker Queue Management Better task prioritization: * Low, medium, and high priority queues * Separate workers for different priorities * Schedule tasks moved to high priority worker ## Downloads Page New dedicated downloads page at app.ourpay.workers.dev/downloads for easy access to desktop and mobile apps. ## Customer Balance Refunds * Handle refunds on orders with customer balance applied * Proper balance credit restoration * Block refunds on specific orders via API flag ## Worker Priority Queues Introduced task prioritization system: * Low priority queue for background tasks * Medium priority queue for standard tasks * High priority queue for time-sensitive operations * Separate workers consuming from each queue Ensures critical tasks get processed first. ## Rate Limiting * New restricted rate limit group for abusive patterns * Better rate limit handling across endpoints ## Email Validation Added DNS validation for email addresses across checkout, customer creation, and user management to prevent typos and invalid domains. ## Event Query Optimization Massive performance improvements: * Removed unnecessary ordering from event queries (3x faster) * Better customer latest event queries * New indexes on events table * Optimized IN queries for customer meters ## Mobile App Improvements * Account deletion functionality * Proper logout and login flow fixes * Better error handling * Login screen animations with Ken Burns effect ## Bug Fixes * Fixed settings scroll with many organizations * Fixed app onboarding redirect * Fixed organization service name in Logfire * Fixed customer meter organization ID tracking * Fixed bitwise SQL operator parentheses ## Customer Balance Customer balance system built on top of wallet infrastructure: * Track customer credit balances * Apply balances automatically to invoices * API support for balance management * Foundation for store credit and refund credits ## Members API New members management system for B2B customers: * GET `/v1/members` endpoint with pagination * Filter members by customer ID * Auto-create owner member when customer is created * Override owner member details via API * Members included in customer endpoints * Proper organization scoping and access control Perfect for managing team members across B2B customer accounts. ## Sign in with Apple Added Apple OAuth for authentication: * Apple OAuth integration ## Ad-hoc Checkout Pricing Create checkouts with dynamic pricing that override catalog prices: * Useful for custom deals, usage-based pricing, or testing * Supports all price types (fixed, free, custom) * Full API and SDK support [Read more](/features/checkout/session#ad-hoc-prices) ## Churn Rate New churn rate metric on analytics dashboard: * Track subscription cancellations over time * Understand retention patterns * Available on homepage and customers page ## Event Labels Events now have customizable labels: * System events get readable labels automatically * User events can have custom labels * Labels shown throughout events UI * Edit labels via event type settings Makes events more readable and meaningful for your team. ## Spans View New spans interface for hierarchical event tracking (hidden for now): * List and detail views for event spans * Charts and sparklines for visualization * Time-series data with proper timezone handling * Cost aggregation across event hierarchies ## Metrics Performance Significant performance improvements for analytics: * Optimized metrics queries with window functions * Better date range bounds handling * Indexes on subscription date columns * Cumulative revenue includes full history * Fixed timezone and interval issues ## Invoice Numbering Changed default invoice numbering to per-customer (was organization-wide): * Each customer starts invoice numbering at 1 * Hides sales volume from individual customers * Can still switch to organization-wide in settings ## Tax & Billing * Show taxable amount after discount in checkout * Fixed tax ID formatting (was showing raw strings) * Fixed Stripe coupon name length validation * Fixed conflicting invoice numbering issues ## Checkout Improvements * Persist business name across checkouts * Hide cookie consent banner when logging in via app * Fixed expired checkout intent handling ## Bug Fixes * Fixed 404 after deleting customers, benefits, or checkout links * Fixed product price configurator validation * Fixed custom field number constraint validation * Fixed benefit capitalization in customer portal * Fixed organization cache after profile picture update * Fixed social link parsing ## Mobile App * Continue with Apple support * Disabled tablet support (phone-only for now) * Fixed EAS build configuration * Removed demo mode * Better workspace integration ## Trial Abuse Prevention Prevent customers from redeeming multiple trials with email aliases or different payment methods: * Email normalization strips `+aliases` to detect duplicates * Payment method fingerprinting tracks card usage * Enable via Settings → Subscription → "Prevent trial abuse" * Blocked customers see error and can still subscribe without trial * Checkout parameter `allow_trial` to force disable trials Perfect for controlling trial costs while still allowing new customers to subscribe. ## Event Types & Aggregation New event types system for better organization and analytics: * Event types automatically created for events with the same name * Custom display names for better readability * Statistics endpoint for event aggregation * Event closure tables for efficient hierarchy queries ## Mobile App (Beta) OurPay mobile app added to the monorepo: * React Native implementation with Expo * Expo workflow for development and builds * Shared workspace with web dashboard * Foundation for mobile-first experiences ## Webhook Events Skipped events now properly tracked: * Events skipped when endpoint disabled no longer count as failures * Prevents false positives in health checks * Better webhook reliability metrics ## File Uploads Large file upload support: * Handles files larger than memory limits * Multipart upload handling * SHA256 validation per part * Better progress tracking ## Subscription Management ### Billing Period Control New API to change current billing period end date: * Useful for custom billing arrangements * Prorations calculated automatically * Prevented on already-cancelled subscriptions ### Payout Improvements * Increased minimum payout amounts for certain currencies * Better error logging for Stripe payout failures ## UI & UX ### Analytics Improvements * Fixed timezone issues with date range selection * Better interval formatting in legends * Improved preset intervals ("this year", "last year") * Consistent date handling across dashboard ### Customer Portal * Fixed overflowing invoice action buttons * Improved mobile experience ### Settings * Auto-prefix social media URLs * Removed seat-based billing toggle (now standard) ## Bug Fixes * Fixed `allow_trial` parameter handling * Fixed dark mode colors across dashboard * Fixed tax rate limiting in sandbox environment * Fixed Stripe trial redemption error handling * Fixed member invitation dialog Enter key submission * Fixed large file upload memory issues * Fixed metrics organization filtering ## Hierarchical Events & Spans Events can now be organized hierarchically with parent-child relationships, perfect for tracking complex workflows: * Create event spans with `parent_id` reference * Use `external_id` as idempotency key to prevent duplicate events * View nested events in the dashboard * Navigate event hierarchies with dedicated event detail pages * Query transitive closure for efficient sub-hierarchy analysis This enables use cases like tracking LLM requests with multiple steps, or multi-phase order fulfillment workflows. ## Master-Detail Layout New master-detail layout pattern across the dashboard: * List view on the left, detail view on the right * Proper nested routing for direct linking * Better navigation and state management * Improved querystring handling Applied to customers, subscriptions, events, and more. ## Improved Date Range Picker Better date range selection throughout analytics: * Fixed "this year" and "last year" presets * Smart interval selection based on date range * Consistent behavior across all views * Better timezone handling ## Settings Auto-Save Settings now auto-save as you type instead of requiring manual save button clicks. Includes proper debouncing and error handling for a smoother experience. ## Customer Management Enhancements ### Product Subscription View Product overview now shows active subscriptions table, making it easier to see who's using each product. ### Customer Filtering Filter subscriptions by cancellation status - find subscriptions that will cancel at period end. ### Benefit Grants Added direct links to customers and subscriptions from benefit grants overview for easier navigation. ## Account Management * Email notifications when unlinking accounts from organizations * Fixed OAuth account disconnection for users with multiple accounts on the same platform ## Bug Fixes * Fixed customer update trying to set email to None when explicitly provided * Fixed payment method handling when soft-deleted methods are updated from Stripe * Added maximum length validation to customer names * Fixed customer portal theming * Fixed subscription double cycling with proper locking mechanisms * Fixed Mintlify docs CSP configuration ## Cost Insights We've launched Cost Insights - a powerful way to track and analyze costs associated with usage. Now you can: * Track costs per event with sub-cent precision * View cost analytics across your dashboard * See cost breakdowns by customer and product * Monitor cost trends with dedicated charts Perfect for understanding unit economics and profitability of your usage-based offerings. [Read more](/features/cost-insights/introduction) ## Webhook Management ### Auto-Disable Failing Webhooks Webhooks that continuously fail (10+ consecutive failures by default) are now automatically disabled to save resources. You can manually re-enable them from the dashboard once the issue is fixed. ### Email Notifications for Webhook Failures When webhooks are automatically disabled due to continuous failures, all organization members receive an email notification with details and steps to re-enable. ## Benefit Revocation Grace Period Configure a grace period for benefit revocation when subscription payments fail. Instead of immediately revoking benefits, you can now give customers time to resolve payment issues: * Immediate revocation (default) * Grace period matching payment retry window This applies to both regular benefits and customer seat benefits. ## Customer Portal Enhancements ### Upcoming Charge Display Subscription detail pages now show the upcoming charge amount and date, including: * Trial end dates with first charge amount * Next billing cycle charges for active subscriptions * Automatic updates when subscriptions change ### Usage & Metering View Improved the customer usage view: * Respect archived meters - they no longer appear in usage tracking * Better empty states for customers with granted credits * Create customer meters automatically when meter credits are granted ### Navigation & UX Improvements * Fixed navigation highlighting on mobile * Better handling of expired login codes * Removed unnecessary scrolling * Can't switch plans during trial period (not supported yet) ## Seat Management Updates * API support for assigning seats programmatically * $0/seat pricing now allowed for seat-based tiers * Fixed eager loading issues with customer seats * Better webhooks for customer seat events ## Invoice Numbering Two invoice numbering modes: * **Organization-wide** (default) - Sequential across all customers * **Per-customer** - Each customer has independent invoice numbering starting at 1 Useful when you want to hide sales volume from customers. ## System Events New system events automatically created for: * Orders paid * Orders refunded * Customer creation * Customer updates These events are available in the events explorer with cost tracking where applicable. ## Discount Management Improved discount code selection with a searchable combobox instead of a dropdown, making it easier to find specific codes. ## Bug Fixes * Fixed customer meter matching to include external customer IDs * Fixed Indian GST tax validation * Fixed subscription dunning process when payment is deleted * Fixed order confirmation emails for orders without products ## Seat-Based Pricing for One-Time Products You can now offer seat-based pricing for one-time purchases, not just subscriptions. Customers can select the number of seats during checkout, and you can manage seats for one-time orders through both the dashboard and API. This extends our seat management capabilities to all product types, giving you more flexibility in how you price and package your offerings. ## Ability to Change Subscription Seats Customers can now change the number of seats on their active subscriptions directly from the Customer Portal. The changes are prorated automatically, making it easy for teams to scale up or down as needed. ## Flexible Subscription Intervals We've added support for flexible subscription intervals beyond monthly and yearly. You can now create subscriptions with custom billing periods: * Daily subscriptions * Weekly subscriptions * Custom intervals (e.g., every 2 weeks, every 3 months) Configure the interval count when creating or updating subscription products through both the dashboard and API. ## Subscription Creation API We've launched a new API endpoint to create subscriptions programmatically. This enables you to set up subscriptions directly without requiring customers to go through the checkout flow. [Read more](/api-reference/subscriptions/create-subscription) ## Enhanced Email Confirmations Order confirmation emails now include: * **Invoice PDFs attached** - Invoices are automatically generated and attached to confirmation emails for one-time purchases * **Order details** - Complete breakdown of what was purchased * **Benefits list** - Clear view of all benefits the customer will receive ## Customer Portal Improvements ### Seat Management Customers with seat-based subscriptions can now: * View all active seats * Add or remove seats (with automatic proration) * Manage team members directly from the portal ### URL Encoding Fix Fixed an issue where email addresses with special characters (like `+` signs in email aliases) weren't properly encoded in customer portal links, causing the wrong email to be pre-filled. ## Events & Metering ### Cost Tracking Events can now include cost metadata, enabling you to track costs associated with usage. Costs are displayed throughout the events explorer and analytics. ### Event Explorer Enhancements * Metadata filtering - Search and filter events by metadata fields * Customer filtering - Filter events by specific customers * Query search - Full-text search across event data * Improved event cards with better timestamps and cost visualization ## Order Export You can now export orders to CSV directly from the dashboard. Filter by product and the export will respect your current filters, making it easy to analyze sales data. ## OAuth2 Flow Improvements * If no scope is passed in the authorize request, the client's default scope is now used automatically * Improved organization selector UI in the OAuth2 authorization flow ## Checkout Improvements * Fixed seat selector visibility on mobile devices * Server-side prefilling of checkout link query parameters * Improved discount code validation flow ## Statement Descriptor Enhancement First payments after a trial period now include "TRIAL OVER" in the statement descriptor, making it clearer to customers why they're being charged. ## Bug Fixes * Fixed customer state API for trialing subscriptions * Fixed broken customer links in events explorer * Fixed discount duration calculation for very long durations (capped at 999 months) ## Ability to update subscription to an updated price of the product Merchants can now [update existing subscriptions](https://docs.ourpay.dev/api-reference/subscriptions/update-subscription#subscriptionupdateproduct) from archived pricing schemes to current ones within the same product. - Enables migration from grandfathered pricing to current pricing schemes - Prorations calculated using active subscription prices - In the dashboard, a small badge **Upgrade pricing** will indicate that you can update to the same product, but with the new pricing scheme. ## New IP ranges From **October 27th, 2025**, [new IP ranges](https://docs.ourpay.dev/integrate/webhooks/delivery#ip-allowlist) will be added. ## Improved Subscription Cancellation Flow [Benefits](https://docs.ourpay.dev/features/benefits/introduction) attached to the subscription [are now automatically revoked](https://github.com/sunnycodet/ourpay/pull/7271) when the subscription is canceled. ## Ability to specify External ID [External ID can now be specified](https://github.com/sunnycodet/ourpay/pull/7275) during creation of a customer via the dashboard. ## Ability to set Return URL for Checkouts and Customer Portal A Return URL can now be set while generating a [checkout session](https://docs.ourpay.dev/api-reference/checkouts/create-checkout-session#body-return-url-one-of-0) or a [customer portal session](https://docs.ourpay.dev/api-reference/customer-sessions/create-customer-session#body-one-of-0-return-url-one-of-0). This allows you to preserve the context for the end users who visit either if they wish to go back to the application. ## Ability to search Customers via their External ID The [List Customers API](https://docs.ourpay.dev/api-reference/customers/list-customers) now accepts Customer's External ID [in the **query** parameter](https://docs.ourpay.dev/api-reference/customers/list-customers#parameter-one-of-0). ## Ability to disable automatic customer emails Via the organization settings, you can now disable the emails we automatically send to customers on certain events. This gives you the ability to own the communications with the customers. ## Launched In-Product Chat Support for Merchants We're excited to introduce a chat widget to the OurPay dashboard, making it easier than ever for merchants to get help directly within the product. ## Improved Checkout Flow for Invalid Discount Code Previously, if you entered an invalid discount code during checkout, you couldn't continue even after removing the code. Now, clearing the discount input lets you proceed smoothly with the checkout process. ## Improved Customer Portal Rate Limits We've made several improvements to the Customer Portal to handle authentication rate limits more gracefully: - The portal now clearly shows 401 and 429 errors on the OTP (one-time password) page. - If you hit a 429 (too many requests), you'll be redirected to a clear `/too-many-requests` page. ## Launched Subscription Trials You can now offer [trial periods](/docs/features/trials) for new subscriptions! This highly requested feature allows you to let customers experience your product before their first payment is due. Trials can be configured in both the dashboard and API when creating or updating subscription products. ## Always display taxes line item in the checkout We've improved our checkout experience to always display taxes (vs lazy loading them on country selection), making charges more transparent for your customers regardless of whether taxes apply to their purchase. ## Do not calculate taxes on free or zero-amount orders Orders with a zero amount (such as promotional products) will no longer have taxes calculated, resulting in a clearer and more accurate order summary for your customers. ## Add confirmation modal for deleting discounts When deleting a discount, you'll now see a confirmation modal to help prevent accidental deletions and provide extra clarity on the impact of your actions. ## Fix infinite rendering loop with date picker Resolved a bug where selecting dates in the date picker could cause an infinite rendering loop, improving reliability for date-related forms. ## Require opt-in if you will be charged immediately Users must now explicitly confirm immediate charges or credits when switching subscription intervals, with the UI providing clearer, contextual explanations of invoicing outcomes. ## Check for `expires_at` when activating license keys License key activation now correctly checks the `expires_at` date, ensuring that only valid, non-expired license keys can be activated. ## Fix customer state for trialing subscriptions The [Customer State API](/api-reference/customers/get-customer-state) now properly handles customers with `trialing` subscriptions, so your integrations and dashboards always show an accurate subscription status. ## Improved preview of next invoice in Customer Portal We've enhanced the Customer Portal to provide a clearer and more accurate preview of your next invoice. The overview now updates automatically after subscription changes, and you can preview upcoming charges with all relevant taxes and discounts included. ## Cancellation metrics We've added detailed cancellation metrics, giving you clearer insights into subscription cancellations and their impact on your business performance. ## Webhooks payload now includes timestamp We've updated our webhooks server implementation to [include a timestamp in each payload](https://github.com/sunnycodet/ourpay/pull/6770), in line with the Standard Webhooks specification. This change ensures that every webhook payload contains precise event timing, making it easier to trace and debug webhook deliveries, and to meet integration requirements for external platforms. ## Meter management improvements We've made it easier to manage your meters with new UI functionality for archiving and unarchiving meters directly from the dashboard. You can now archive meters that are no longer needed, which helps keep your meter list organized. Archived meters can be unarchived if you need to use them again. Note that meters cannot be archived if they are still attached to active products or referenced by active benefits. ## Metrics accuracy improvements We've improved the accuracy of our metrics by excluding unpaid orders from all calculations. Previously, orders in pending status were included in metrics, which could lead to inflated numbers. Now, only successfully paid and refunded orders are included in metrics calculations, giving you a more accurate view of your actual business performance. ## Enhanced customer email branding We've improved the branding of emails sent to your customers by using organization-specific 'From' and 'Reply-to' addresses. Customer emails now appear to come from your organization (e.g., "YourOrg (via OurPay)") with replies directed to your organization's email address, providing a more professional and branded experience for your customers. ## Update subscription discount We've added the ability to update the discount on a subscription. This allows you to add, remove, or change the discount applied to a subscription at any time. This feature is both available through the [API](/api-reference/subscriptions/update-subscription) and the dashboard. ## Payout Reverse Invoices We've added the ability to generate reverse invoices for payouts directly from the Payouts page. This feature allows you to easily create an invoice that details the sales made on your behalf, minus our fees. [Read more](/features/finance/payouts#reverse-invoices) ## Business Purchase Option on Checkout We've added a new "I'm purchasing as a business" checkbox to the Checkout flow. When selected, customers are required to provide their business billing name and complete billing address. ## Enhanced Attribution for Checkout Links We've added support for `reference_id` and UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`) as query parameters for Checkout Links. These parameters are automatically stored in the Checkout metadata, allowing you to track the source of your traffic and conversions more effectively. [Read more](/features/checkout/links#store-attribution-and-reference-metadata) ## Checkouts and payments insights We've added a new **Checkouts** tab under the **Sales**, where you can review all the checkout sessions, successful or not. You can filter them by customer email, status, and product. You can also see the payment attempts for each checkout session, including the reason for any failed or declined payments. The payment attempts information is also available on each order. Besides, we've also added new analytics around checkouts: total number of checkouts, successful checkouts, and conversion rate. ## Zapier integration officially launched We're excited to announce the official launch of our [Zapier integration](https://zapier.com/apps/ourpay/integrations)! Get started now and connect OurPay to 2,000+ other web services. We've focused on **triggers** (webhooks) for now, so you can react to events in OurPay and trigger actions in other apps. Need to perform actions in OurPay? Tell us about your use case [here](https://github.com/orgs/sunnycodet/discussions/new?category=integrations&labels=integrations%2Fzapier) and we'll consider adding more actions in the future. ## Customer State Maybe one of our neatest features to date! Customer State is a concept allowing you to query for the current state of a customer, including their **active subscriptions** and **granted [benefits](/features/benefits/introduction)**, in a single [API call](/api-reference/customers/get-customer-state-by-external-id) or single [webhook event](/api-reference/customerstate_changed). Combined with the [External ID](/features/customer-management#external-id) feature, you can get up-and-running in minutes. [Read more](/integrate/customer-state) ## Better Auth Plugin Integrating authentication and billing for your users has never been easier. [Better Auth](https://www.better-auth.com/) is an open source authentication framework for TypeScript that is quickly becoming a favorite amongst developers. Today, we're thrilled to have shipped a OurPay plugin for Better Auth - in collaboration with them. Checkout our [integration guide](/integrate/sdk/adapters/better-auth). ## Customer External ID We've added support for an `external_id` field on Customers. We believe this will greatly simplify the reconciliation process between your system and OurPay. Previously, the recommended way to reconcile with your users was to use `metadata`. However, this was not always the most convenient method, especially if you needed to fetch a Customer from our API. With `external_id`, you can now fetch a Customer directly by their external ID through dedicated `GET`, `PATCH`, and `DELETE` endpoints. You don't even need to store OurPay's internal ID in your system anymore! [Read more](/features/customer-management#external-id) Of course, you can also directly preset `external_customer_id` when creating a Checkout Session, and it will automatically be set on the newly created Customer after a successful checkout. [Read more](/features/checkout/session#external-customer-id) ## OurPay's take on Product variants We've released big changes to how we handle products and pricing, allowing us to support a unique approach to what the industry typically calls **variants** 🔥 We believe having a single product with multiple pricing models and benefits adds unneccessary complexity to the user and to the API. Instead, we chose to treat everything as a product, giving you maximum flexibility about the pricing and benefits you want to offer. Thus, we introduce support for **multiple products** at checkout, allowing customers to switch between them before purchasing. Typically, you can offer a monthly and a yearly product, with specific pricing and benefits for each. {" "} This is available right now using the [Checkout Session API](/features/checkout/session) and [Checkout Links](/features/checkout/links). ### Depreciations - Products can no longer have both a monthly and yearly pricing. Existing products still work, but you'll see a warning like this when trying to edit their pricing: {" "} ### API changes - The `product_id` and `product_price_id` fields are deprecated in the [Checkout Session API](/api-reference/checkouts/create-checkout-session). You should now use the `products` field to specify the products you want to include in the checkout. - The `type` and `recurring_interval` fields on `ProductPrice` are deprecated. `recurring_interval` is now set directly on `Product`. # Analytics Source: https://docs.ourpay.dev/features/analytics OurPay ships with a built-in analytics dashboard so you can stay focused on growing the business rather than wiring up reporting. Each section below maps to one of the dashboards on the **Analytics** page and walks through what its metrics actually mean. ## How metrics are bucketed When you pick a date range, OurPay splits it into equal-width **intervals** — one hour, one day, one week, one month, or one year each — and aggregates every metric inside each interval. Each interval (sometimes called a "bucket") is one point on the chart and one row in the API response. ## Subscriptions - **Monthly Recurring Revenue (MRR)** — Sum of every ongoing subscription's net amount, normalized to a monthly rate (yearly plans are divided by 12). Trialing subscriptions are excluded. Paused and past-due subscriptions are included: a subscriber counts towards MRR until their subscription actually ends. - **Committed MRR** — MRR restricted to subscriptions still inside a committed billing period. Subscriptions that have been canceled but are running out the clock until period end are excluded. As with MRR, paused and past-due subscriptions are included. - **Trial MRR Including Canceled Trials** — Monthly-normalized amount of every subscription currently in a trial, including trials that have already been canceled. - **Trial Committed MRR** — Trial MRR for trials still inside their committed billing period (i.e. canceled trials are excluded). - **Active Subscriptions** — Count of ongoing subscriptions at the end of each interval, based on when each subscription started and ended rather than its current status. A subscription counts until it actually ends: those that have been canceled but are running out the clock until period end, as well as paused and past-due subscriptions, are all still included. - **New Subscriptions** — Subscriptions whose first paid order falls inside the interval. Each subscription is counted once, at creation. - **Committed Subscriptions** — Active subscriptions that have not been canceled, or whose cancellation date is after the interval. These are the subscriptions you can reasonably expect to renew. - **Renewed Subscriptions** — Number of orders with billing reason `subscription_cycle` in the interval — i.e. renewals on existing subscriptions. - **Average Revenue Per User (ARPU)** — MRR divided by the count of distinct paying subscribers in the interval (trials excluded). Returns 0 when there are no paying subscribers. - **Lifetime Value (LTV)** — Estimated revenue per customer over their lifetime: `(ARPU - cost per user) / churn rate`. Returns 0 when churn rate is 0. - **New Subscriptions Revenue** — Gross revenue from the first paid order on every new subscription created in the interval. - **Renewed Subscriptions Revenue** — Gross revenue from subscription renewal orders in the interval. ## Cancellations - **Canceled Subscriptions** — Subscriptions where the customer triggered a cancellation in the interval. The subscription itself may still be active until the end of the paid period. - **Churned Subscriptions** — Subscriptions whose paid period actually ended inside the interval. This is "real" churn — the moment access stops. - **Churn Rate** — Churned subscriptions in the interval divided by the active subscription base at the start of the interval, expressed as a percentage. - **Active Subscriptions** — Same definition as in the [Subscriptions](#subscriptions) dashboard. Shown here to contextualise churn against the underlying base. - **Committed Subscriptions** — Same definition as in the [Subscriptions](#subscriptions) dashboard. - **Cancellation reasons** — Stacked chart of canceled subscriptions broken down by the reason the customer picked at cancellation: *too expensive*, *missing features*, *switched service*, *unused*, *customer service*, *low quality*, *too complex*, or *other* (free-form or no reason given). ## One-Time Products - **One-Time Products** — Number of completed orders for non-recurring products in the interval. Subscription orders are not included. - **One-Time Products Revenue** — Gross revenue (subtotal before fees) from those one-time product orders. ## Orders - **Revenue** — Gross revenue from every completed order in the interval: one-time purchases, new subscription orders, and renewal orders. Computed on the order subtotal, before OurPay fees and tax. - **Orders** — Total number of completed orders in the interval, regardless of billing reason. - **Average Order Value (AOV)** — Revenue divided by the number of orders. Returns 0 when there are no orders. - **Cumulative Revenue** — Running total of revenue from the start of the selected range up to and including the current interval. ## Checkouts - **Checkouts Conversion** — Succeeded checkouts divided by total checkouts created, as a percentage. Returns 0 when no checkouts were created. - **Checkouts** — Every checkout session created in the interval, regardless of outcome (succeeded, expired, or abandoned). - **Succeeded Checkouts** — Checkouts that reached the `succeeded` status, meaning they resulted in an order. ## Net Revenue - **Net Revenue** — Revenue after deducting OurPay's configured fees on each order. For centrally collected PayPal payments, PayPal's processor fee is an operator cost and is not deducted a second time from the seller payable. This figure is a ledger liability, not a completed payout. - **Net Average Order Value** — Net revenue divided by the number of orders in the interval. - **Net Cumulative Revenue** — Running total of net revenue from the start of the selected range up to the current interval. - **New Subscriptions Net Revenue** — Net revenue from the first paid order on every new subscription. Shown when your organization sells recurring products. - **Renewed Subscriptions Net Revenue** — Net revenue from subscription renewal orders. Shown when your organization sells recurring products. - **One-Time Products Net Revenue** — Net revenue from one-time product orders. Shown when your organization sells one-time products. ## Costs The Costs dashboard is powered by [Cost Insights](/features/cost-insights/introduction) — you'll only see data here once you start sending events with a `_cost` annotation. - **Costs** — Total operating costs ingested via Cost Insights in the interval. Stored at sub-cent precision so per-event costs (e.g. token-level inference costs) aren't rounded away. - **Cost Per User** — Cumulative costs in the selected range divided by the count of active subscribers. Returns 0 when there are no active subscribers. - **Gross Margin** — Cumulative revenue minus cumulative costs over the selected range. - **Gross Margin %** — Gross margin expressed as a percentage of cumulative revenue. Returns 0 when cumulative revenue is 0. - **Cashflow** — Revenue minus costs for the interval itself (not cumulative). Useful for spotting periods where costs outpaced revenue. # Credits Benefit Source: https://docs.ourpay.dev/features/benefits/credits The Credits benefit allows you to credit a customer's Usage Meter balance. ## Crediting Usage Meter Balance The Credits benefit will credit a customer's Usage Meter balance at different points in time depending on the type of product purchased. ### Subscription Products The customer will be credited the amount of units specified in the benefit at the beginning of every subscription cycle period — monthly or yearly. ### One-Time Products The customer will be credited the amount of units specified in the benefit once at the time of purchase. ## Rollover unused credits You can choose to rollover unused credits to the next billing cycle. This means that if a customer doesn't use all of their credits in a given billing cycle, the remaining credits will be added to their balance for the next billing cycle. To enable this feature, check the "Rollover unused credits" checkbox when creating or editing the Credits benefit. If you change the rollover setting for a benefit, it will only apply to new credits issued after the change. Existing credits will not be affected. # Custom Benefit Source: https://docs.ourpay.dev/features/benefits/custom You can add a simple, custom benefit, which allows you to attach a note to paying customers. **Custom Notes** ----------------------- Secret message only customers can see, e.g [Cal.com](http://Cal.com) link, private email for support etc. For custom integrations you can also distinguish benefits granted to customers to offer even more bespoke user benefits. ## Sharing links and instructions after purchase Because the **Private note** supports Markdown, a Custom benefit is the simplest way to deliver post-purchase content without writing any code: - A private link (Calendly, Notion, …) - Onboarding instructions or a welcome message - A coupon code for a partner service Set the **Description** to the title customers will see (e.g. *"Your onboarding link"*), put the link or instructions in **Private note**, and attach the benefit to a product. The rendered Markdown appears on the checkout success page, in the purchase confirmation email, and in the [Customer Portal](/features/customer-portal). Previously, we recommended using a Custom benefit without a note as a way to grant access to software or SaaS features — decoupling entitlements from checking directly on products. We now recommend the [Feature Flag](/features/benefits/feature-flags) benefit for this purpose, as it's purpose-built for feature gating and supports key-value metadata. # Automate Discord Invites & Roles Source: https://docs.ourpay.dev/features/benefits/discord-access Automating Discord server invites and roles for customers or subscribers is super easy and powerful with OurPay. * Fully automated Discord server invitations * You can even setup multiple Discord servers, or... * Offer different roles for different subscription tiers or products Create Discord Benefit ----------------------------- Click on `Connect your Discord server`. You'll be redirected to Discord where you can grant the OurPay App for your desired server. Next, you'll be prompted to approve the permissions our app requires to function. It needs all of them. ### **Manage Roles** Access to your Discord roles. You'll be able to select which ones to grant to your customers later. ### **Kick Members** Ability to kick members who have this benefit and connected Discord with OurPay. ### **Create Invite** Ability to invite members who purchase a product or subscribes to a tier with this benefit. You're now redirected back to OurPay and can finish setting up the Discord benefit on our end. ### **Connected Discord server** The Discord server you connected cannot be changed. However, you can create multiple benefits and connect more Discord servers if you want. ### **Granted role** Which Discord role do you want to grant as part of this benefit? Adding Benefit to Product -------------------------------- Head over to the product you want to associate this new Discord benefit with. You should be able to toggle the benefit in the bottom of the Edit Product form. # Feature Flag Benefit Source: https://docs.ourpay.dev/features/benefits/feature-flags The Feature Flag benefit is a lightweight way to grant feature access to customers without any external service integration. If a customer has the benefit grant, they have access — it's that simple. ## Use Cases * Gate premium features behind a subscription tier * Offer early access or beta features to select customers * Differentiate access levels across product tiers * Control API rate limit tiers or usage quotas in your application ## Create Feature Flag Benefit 1. Go to [`Benefits`](https://ourpay.dev/to/dashboard/products/benefits) 2. Click `+ New Benefit` to create a new benefit 3. Choose `Feature Flag` as the `Type` 4. Give it a short description (e.g. "Premium Features" or "Beta Access") ### Metadata You can optionally attach key-value metadata to a feature flag benefit. This is useful for passing additional context to your application, for example: * `role` → `editor` * `max_upload_size` → `10` * `priority` → `elevated` Metadata can be configured when creating or editing the benefit in the dashboard using the **Add Metadata** button. ## Integration The recommended way to check if a customer has a feature flag benefit is through the [Customer State](/integrate/customer-state) API or the [`customer.state_changed`](/api-reference/customerstate_changed) webhook. The customer state object includes all granted benefits. Simply check if the customer has a benefit grant for your feature flag benefit to determine access. ### Lifecycle * **Subscriptions**: The feature flag is granted at the start of each subscription cycle and automatically revoked when the subscription is cancelled. * **One-time purchases**: The feature flag is granted at the time of purchase with lifetime access. # Automate Customer File Downloads Source: https://docs.ourpay.dev/features/benefits/file-downloads Sell Digital Products ---------------------------- You can easily offer customers and subscribers access to downloadable files with OurPay. * Up to 10GB per file * Upload any type of file - from ebooks to full-fledged applications * SHA-256 checksum validation throughout for you and your customers (if desired) * Customers get a signed & personal downloadable URL Create Downloadable Benefit ---------------------------------- 1. Go to [`Benefits`](https://ourpay.dev/to/dashboard/products/benefits) 2. Click `+ Add Benefit` to create a new benefit 3. Choose `File Downloads` as the `Type` You can now upload the files you want to offer as downloadables for customers. 1. Drag & drop files to the dropzone (`Feed me some bytes`) 2. Or click on that area to open a file browser ### Change filename Click on the filename to change it inline. ### Change order of files You can drag and drop the files in the order you want. ### Review SHA-256 checksum Click on the contextual menu dots and then `Copy SHA-256 Checksum` ### Delete a file Click on the contextual menu dots and then `Delete` in the menu. **Active subscribers & customers will lose access too!** Deleting a file permanently deletes it from OurPay and our S3 buckets except for the metadata. Disable the file instead if you don't want it permanently deleted. ### Disable & Enable Files You can disable files at any point to prevent new customers getting access to it. **Existing customers retain their access** Customers who purchased before the file was disabled will still have access to legacy files. Only new customers will be impacted. **Enabling or adding files grants access retroactively** In case you add more files or re-enable existing ones, all current customers and subscribers with the benefit will be granted access. # Automate Private GitHub Repo(s) Access Source: https://docs.ourpay.dev/features/benefits/github-access Sell GitHub Repository Access ------------------------------------ With OurPay you can seamlessly offer your customers and subscribers automated access to private GitHub repositories. * Fully automated collaborator invites * Unlimited repositories (via multiple benefits) from your organization(s) * Users get access upon subscribing & removed on cancellation * Or get lifetime access upon paying a one-time price (product) ### **Use cases** * Sponsorware * Access to private GitHub discussions & issues for sponsors * Early access to new feature development before upstream push * Premium educational materials & code * Self-hosting products * Courses, starter kits, open core software & more... Create GitHub Repository Benefit --------------------------------------- 1. Go to [`Benefits`](https://ourpay.dev/to/dashboard/products/benefits) 2. Click `+ New Benefit` to create a new benefit 3. Choose `GitHub Repository Access` as the `Type` You first need to `Connect your GitHub Account` and install a dedicated OurPay App for this benefit across the repositories you want to use it with. * Click `Connect your GitHub Account` **Why do I need to connect GitHub again and install a separate app?** This feature requires permission to manage repository collaborators. GitHub Apps does not support progressive permission scope requests. So instead of requesting this sensitive permission from all users (unnecessarily) in our core GitHub Login this feature uses a standalone app instead. Once you've authorized our dedicated GitHub App for this feature you'll be redirected back to OurPay and the benefit form - now connected and updated. ### **Repository** Select the desired repository you want to automate collaborator invites for. **Why can I only connect organization repositories vs. personal ones?** GitHub does not support granular permissions for collaborators on personal repositories - granting them all write permissions instead. Since collaborators would then be able to push changes, releases and more, we do not support personal repositories by default.Want this still? Reach out to us and we can enable it. ### **Role** Select the role you want to grant collaborators. * **Read (Default & Highly recommended)** * Triage * Write * Maintain * Admin Read access (read-only) is what 99.9% of cases should use and the others are highly discouraged unless you have special use cases & absolutely know the impact of these permissions. Checkout the [GitHub documentation](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization#permissions-for-each-role) for reference. Anyone with read access to a repository can create a pull request [(source)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). **Additional Costs for Paid GitHub Organizations** GitHub treats collaborators as a seat and they will incurr charges accordingly to your billing unless you're using a free GitHub organization plan. So make sure to confirm you're on a free plan OR charge sufficiently to offset the costs you'll need to pay to GitHub. # Automated Benefits Source: https://docs.ourpay.dev/features/benefits/introduction OurPay offers built-in benefit (entitlements) automation for common upsells within the developer & designer ecosystem with more to come. - [**Credits**](/features/benefits/credits). A simple benefit that allows you to credit a customer's Usage Meter balance. - [**License Keys**](/features/benefits/license-keys). Software license keys that you can customize the branding of. - [**Feature Flags**](/features/benefits/feature-flags). Simple, API-driven feature access flags with optional metadata. - [**File Downloads**](/features/benefits/file-downloads). Downloadable files of any kind up to 10GB each. - [**GitHub Repository Access**](/features/benefits/github-access). Automatically invite subscribers to private GitHub repo(s). - [**Discord Invite**](/features/benefits/discord-access). Automate invitations and granting of roles to subscribers and customers. - [**Shared Slack Channel**](/features/benefits/slack-shared-channel). Give customers a shared Slack channel via Slack Connect. ## Product & Subscription Benefits Product and subscription benefits are standalone resources in OurPay - connected to one or many products or subscription tiers. This approach is a bit different from other platforms, but offers many advantages: - Easy to enable the same benefit across multiple products & subscriptions - You can change a benefit in one place vs. many - No duplicate data or work (error prone) - More intuitive UI for you and your customers **How customers get access to benefits:** - ✅ Active subscribers of tiers with the benefit enabled - ✅ Customers who bought a product with the benefit (lifetime access) - ❌ Subscribers with an expired subscription (cancelled) - ❌ Users who are not customers ## Creating & Managing Benefits You can manage benefits in two ways: 1. Directly within a product create/edit form 2. Or via `Benefits` in your dashboard # Automate Customer License Key Management Source: https://docs.ourpay.dev/features/benefits/license-keys You can easily sell software license keys with OurPay without having to deal with sales tax or hosting an API to validate them in real-time. License keys with OurPay come with a lot of powerful features built-in. * Brandable prefixes, e.g `OURPAY_*****` * Automatic expiration after `N` days, months or years * Limited number of user activations, e.g devices * Custom validation conditions * Usage quotas per license key * Automatic revokation upon cancelled subscriptions Create License Key Benefit --------------------------------- 1. Go to [`Benefits`](https://ourpay.dev/to/dashboard/products/benefits) 2. Click `+ New Benefit` to create a new benefit 3. Choose `License Keys` as the `Type` ### Custom Branding Make your license keys standout with brandable prefixes, e.g `MYAPP_` ### Automatic Expiration Want license keys to expire automatically after a certain time period from when the customer bought them? No problem. ### Activation Limits You can require license keys to be activated before future validation. A great feature in case you want to limit license key usage to a certain number of devices, IPs or other conditions. **Enable user to deactivate instances via OurPay.** Instead of building your own custom admin for customers to manage their activation instances - leave it to OurPay instead. ### Usage Limit Offering OpenAI tokens or anything else with a variable usage cost? You can set a custom usage quota per license key and increment usage upon validation. Customer Experience -------------------------- Once customers buy your product or subscribes to your tier, they will automatically receive a unique license key. It's easily accessible to them under their purchases page. Customers can: * View & copy their license key * See expiration date (if applicable) * See usage left (if applicable) * Deactivate activations (if enabled) * Rotate their license key if it is exposed or compromised ### Rotate License Keys If a key is exposed or compromised, merchants can rotate it from the benefit license keys page in the dashboard, or via `POST /v1/license-keys/{id}/rotate`. Customers can rotate their own key from the customer portal, or via `POST /v1/customer-portal/license-keys/{id}/rotate`. Only keys in `granted` or `disabled` status can be rotated; rotating a revoked key returns a `400` error. Rotation generates a new key string on the same license key record. The previous key stops validating immediately. Status, usage, limits, expiry, and activations are preserved. After rotating, copy the new key from the customer portal or share it from the dashboard. Integrate API -------------------- It's super easy and straightforward to integrate OurPay license keys into your application, library or API. ### Activate License Keys (Optional) In case you've setup license keys to have a maximum amount of activation instances, e.g user devices. You'll then need to create an activation instance prior to validating license keys / activation. **No activation limit?** You can skip this step. ```bash Terminal curl -X POST https://api.ourpay.dev/v1/customer-portal/license-keys/activate -H "Content-Type: application/json" -d '{ "key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA", "organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f", "label": "hello", "conditions": { "major_version": 1 }, "meta": { "ip": "84.19.145.194" } }' ``` Replace with the users license key (from input in your app). Replace with your organization ID here found in your settings. Set a label to associate with this specific activation. JSON object with custom conditions to validate against in the future, e.g IP, mac address, major version etc. JSON object with metadata to store for the users activation. #### **Response (200 OK)** ```json { "id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe", "license_key_id": "508176f7-065a-4b5d-b524-4e9c8a11ed63", "label": "hello", "meta": { "ip": "84.19.145.194" }, "created_at": "2024-09-02T13:48:13.251621Z", "modified_at": null, "license_key": { "id": "508176f7-065a-4b5d-b524-4e9c8a11ed63", "organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f", "user_id": "d910050c-be66-4ca0-b4cc-34fde514f227", "benefit_id": "32a8eda4-56cf-4a94-8228-792d324a519e", "key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA", "display_key": "****-E304DA", "status": "granted", "limit_activations": 3, "usage": 0, "limit_usage": 100, "validations": 0, "last_validated_at": null, "expires_at": "2026-08-30T08:40:34.769148Z" } } ``` ### Validate License Keys For each session of your premium app, library or API, we recommend you validate the users license key via the [`/v1/customer-portal/license-keys/validate`](/api-reference/customer_portal/validate-license-key) endpoint. ```bash Terminal curl -X POST https://api.ourpay.dev/v1/customer-portal/license-keys/validate -H "Content-Type: application/json" -d '{ "key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA", "organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f", "activation_id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe", "conditions": { "major_version": 1 }, "increment_usage": 15 }' ``` Replace with the users license key (from input in your app). Replace with your organization ID here found in your settings. The activation ID to validate - required in case activations limit is enabled and used (above). In case of activation instances. Same exact JSON object as upon registration of the activation. In case you want to increment usage upon validation. #### **Response (200 OK)** ```json { "id": "508176f7-065a-4b5d-b524-4e9c8a11ed63", "organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f", "user_id": "d910050c-be66-4ca0-b4cc-34fde514f227", "benefit_id": "32a8eda4-56cf-4a94-8228-792d324a519e", "key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA", "display_key": "****-E304DA", "status": "granted", "limit_activations": 3, "usage": 15, "limit_usage": 100, "validations": 5, "last_validated_at": "2024-09-02T13:57:00.977363Z", "expires_at": "2026-08-30T08:40:34.769148Z", "activation": { "id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe", "license_key_id": "508176f7-065a-4b5d-b524-4e9c8a11ed63", "label": "hello", "meta": { "ip": "84.19.145.194" }, "created_at": "2024-09-02T13:48:13.251621Z", "modified_at": null } } ``` Validate `benefit_id` in case of multiple license keys We require `organization_id` to be provided to avoid cases of OurPay license keys being used across OurPay organizations erroneously. Otherwise, a valid license key for one organization could be used on another.However, you are required to validate and scope license keys more narrowly within your organization if necessary. Offering more than one type of license key? Be sure to validate their unique benefit\_id in the responses. # Shared Slack Channel Source: https://docs.ourpay.dev/features/benefits/slack-shared-channel The Shared Slack Channel benefit automatically provisions a dedicated Slack channel for each customer and shares it with their workspace through [Slack Connect](https://slack.com/connect). - A new channel is created in your Slack workspace for every customer who gets the benefit - The channel is shared with the customer's own workspace — no need for them to join yours - Channels can be archived automatically when the benefit is revoked The Shared Slack Channel benefit is currently in preview. If you're on a paid plan, you'll be able to create a Shared Slack Channel benefit. ## Create Shared Slack Channel Benefit 1. Go to [`Benefits`](https://ourpay.dev/to/dashboard/products/benefits) 2. Click `+ New Benefit` to create a new benefit 3. Choose `Shared Slack Channel` as the `Type` ### **Connect your Slack workspace** The first time you create this benefit, you'll be prompted to connect the Slack workspace where channels should be created. You can reuse the same workspace across multiple benefits. ### **Channel name template** Channel names are generated from a template, so every customer's channel follows the same naming convention. The template supports the following placeholders: - `{customer_name}` — the customer's name - `{customer_email_local}` — the local part of the customer's email (everything before the `@`) - `{metadata.}` — any value stored in the customer's metadata, e.g. `{metadata.company}` For example, `support-{customer_email_local}` produces a channel like `support-jane` for `jane@acme.com`. ### **Other options** - **Private channel** — create the channel as private. Recommended, and enabled by default. - **Welcome message** — an optional message posted to the channel right after it's created. - **Team invitees** — members of your Slack workspace to automatically invite to every channel created for this benefit. - **Archive on revoke** — archive the channel when the benefit is revoked (for example, when a subscription is canceled). Enabled by default. ## Customer Experience When a customer is granted the benefit, they're asked for the email address of an admin in their own Slack workspace. OurPay creates the channel, invites your team members, posts your welcome message, and sends a Slack Connect invitation to that admin. Once they accept, the shared channel appears in their workspace and you can start talking right away. # Embedded Payment Method Source: https://docs.ourpay.dev/features/checkout/embed-payment-method Let your customers securely add a payment method without leaving your site. ## Getting a session token Every embed is authenticated with a short-lived **customer session token**. Create it **on your server** to keep the OurPay access token secret, then hand the token to the client. ```ts import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN }); const session = await ourpay.customerSessions.create({ customerId: "the-customer-id", }); ``` The token expires after one hour and is scoped to that single customer. ## Implementation Start by installing the SDK. ```bash npm npm install @ourpay-sh/checkout ``` ```bash pnpm pnpm add @ourpay-sh/checkout ``` ```bash yarn yarn add @ourpay-sh/checkout ``` ```bash bun bun add @ourpay-sh/checkout ``` ### Modal `OurPayEmbedPaymentMethod.create()` opens the embed as a full-screen modal overlay. ```ts import { OurPayEmbedPaymentMethod } from "@ourpay-sh/checkout/payment-method"; const embed = await OurPayEmbedPaymentMethod.create({ sessionToken: session.token, }); embed.addEventListener("success", (event) => { console.log({event}); }); ``` `create()` options: | Option | Type | Default | Description | | -------------- | ------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `sessionToken` | `string` | — | **Required.** Customer session token. | | `theme` | `'light' \| 'dark'` | `light` | Colour scheme. | | `setAsDefault` | `boolean` | `true` | Whether the new card should become the customer's default payment method. | | `returnUrl` | `string` | current URL | Where to return the customer after a redirect-based payment method (Amazon Pay, Klarna). Defaults to `window.location.href`. | | `locale` | `string` | `'en'` | BCP47 locale for the embed UI and Stripe Elements (e.g. `'en'`, `'fr-FR'`). Unsupported locales fall back to English. | | `onLoaded` | `(event: CustomEvent) => void` | — | Convenience callback for the `loaded` event. Equivalent to `embed.addEventListener('loaded', …)`. | ### Modal in React Open the modal from a Client Component event handler: ```tsx "use client"; import { OurPayEmbedPaymentMethod } from "@ourpay-sh/checkout/payment-method"; interface Props { sessionToken: string; } export function AddPaymentMethodButton({sessionToken}: Props) { const onClick = async () => { const embed = await OurPayEmbedPaymentMethod.create({ sessionToken }); embed.addEventListener("success", (event) => { console.log({event}); }); }; return ; } ``` ### Inline embed (vanilla JS) Mount a chrome-less, auto-resizing iframe into an element you control: ```ts import { OurPayEmbedPaymentMethod } from "@ourpay-sh/checkout/payment-method"; const embed = OurPayEmbedPaymentMethod.createInline({ sessionToken: session.token, element: document.getElementById("payment-method")!, }); embed.addEventListener("success", (event) => { console.log({event}); }); ``` `createInline()` accepts the same options as `create()` (except `returnUrl`) plus a required `element` (the container to mount into). ### Inline embed (React) Use the `` component: ```tsx import { OurPayPaymentMethod } from "@ourpay-sh/checkout/react/payment-method"; return ( console.log({paymentMethodId})} /> ); ``` ### Code Snippet The simplest integration: add the script and a trigger element with `data-ourpay-payment-method`. Clicking the element opens the modal. ```html ``` The same script also powers embedded checkout triggers — one tag covers every OurPay embed. | Attribute | Value | Description | | ------------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------- | | `data-ourpay-payment-method` | `string` | **Required.** The session token. Clicking the element opens the modal. | | `data-ourpay-payment-method-theme` | `light \| dark` | Optional theme override. | | `data-ourpay-payment-method-set-as-default` | `true \| false` | Optional. Default `true`. Pass `"false"` to add the card without overriding the existing default. | | `data-ourpay-payment-method-return-url` | `string` | Optional. Return URL for redirect-based payment methods. Defaults to the current page. | | `data-ourpay-payment-method-locale` | `string` | Optional. BCP47 locale (e.g. `'en'`, `'fr-FR'`). Unsupported locales fall back to English. | ## Localization The embed is fully localized, pass a BCP47 code via the `locale` option (or `data-ourpay-payment-method-locale` attribute): ```ts const embed = await OurPayEmbedPaymentMethod.create({ sessionToken: session.token, locale: "fr-FR", }); ``` When omitted, the embed defaults to English. Unsupported locales also fall back to English. See [Localization](/features/checkout/localization) for the full list of supported languages. ## Events All events are dispatched as cancelable `CustomEvent`s on the `embed` instance. Call `event.preventDefault()` to opt out of the SDK's default action. | Event | Detail | Default action | | ----------- | ----------------------------- | ------------------------------------------------------------------- | | `loaded` | — | Removes the loader spinner once the iframe is ready. | | `close` | — | Tears down the iframe (unless locked by a pending `confirmed`). | | `confirmed` | — | Marks the modal as non-closable while Stripe is processing. | | `success` | `{ paymentMethodId: string }` | **Auto-closes the modal.** Call `preventDefault()` to keep it open. | | `error` | `{ code: 'invalid_request' \| 'unauthorized' \| 'processing_failed' \| 'unknown' }` | Re-enables closing the modal after a failure. | ## Redirect-based payment methods Some payment methods authorise on the provider's own site. The browser navigates the whole tab away and back to `returnUrl` (defaults to the page the SDK was opened from), so the modal can't survive the round-trip. Read the outcome on the returned page with the static `getRedirectResult()`: ```ts import { OurPayEmbedPaymentMethod } from "@ourpay-sh/checkout/payment-method"; const result = OurPayEmbedPaymentMethod.getRedirectResult(); // result: { status: 'succeeded' | 'failed' } | null if (result?.status === "succeeded") { // refresh the customer's payment methods } ``` In React, use the `usePaymentMethodRedirectResult` hook to avoid writing your own effect: ```tsx import { usePaymentMethodRedirectResult } from "@ourpay-sh/checkout/react/payment-method"; usePaymentMethodRedirectResult({ onSuccess: () => console.log("Payment method added"), onError: () => console.error("Could not add payment method"), }); ``` Either way, the status query param is stripped from the URL so a refresh won't surface a stale result. Card payments (3DS) complete inside the modal and never trigger this path. # Embedded Checkout Source: https://docs.ourpay.dev/features/checkout/embed You can either copy and paste our code snippet to get up and running in a second or use our JavaScript library for more advanced integrations. Our embedded checkout allows you to provide a seamless purchasing experience without redirecting users away from your site. ## Code Snippet The code snippet can be used on any website or CMS that allows you to insert HTML. First, create a [Checkout Link](/features/checkout/links) as described in the previous section. The code snippet can directly be copied from there by clicking on `Copy Embed Code`. The snippet looks like this: ```typescript Purchase ``` This will display a `Purchase` link which will open an inline checkout when clicked. You can style the trigger element any way you want, as long as you keep the `data-ourpay-checkout` attribute. ## Import Library If you have a more advanced project in JavaScript, like a React app, adding the ` ``` Replace `YOUR_AFFONSO_PROGRAM_ID` with the unique program ID provided by Affonso. This script should be placed on all pages of your website, including: - Your main marketing website - Your application domain - Any subdomains where users might land or make purchases ### 4. Track User Signups (Optional) For better conversion insights, you can track when users sign up through an affiliate link: ```javascript // After successful registration window.Affonso.signup(userEmail); ``` ### 5. Pass Referral Data to OurPay Checkout To ensure proper commission attribution, pass the referral data when creating checkout sessions: ```javascript // Get the referral ID from the Affonso global variable const referralId = window.affonso_referral; // Create checkout session with OurPay const checkout = await ourpay.checkouts.create({ products: ["your_product_id"], success_url: "https://your-app.com/success", metadata: { affonso_referral: referralId, // Include referral ID from Affonso } }); // Redirect to checkout window.location.href = checkout.url; ``` ## How It Works 1. When a user visits your site through an affiliate link, Affonso's script stores a unique identifier in a cookie 2. If you've implemented signup tracking, Affonso records when the user creates an account 3. When the user makes a purchase, the referral ID is passed to OurPay as metadata 4. OurPay's webhook notifies Affonso about the purchase 5. Affonso attributes the sale to the correct affiliate and calculates the commission ## Benefits of the Integration - **Automated Tracking**: No manual work required to track affiliate-driven sales - **Real-Time Analytics**: Both you and your affiliates get immediate insights into performance - **Seamless User Experience**: The integration works behind the scenes without affecting your checkout flow - **Flexible Commission Structures**: Set up complex commission rules based on product, subscription duration, etc. ## Getting Help More details about the integration: [OurPay Affiliate Program](https://affonso.io/ourpay-affiliate-program) If you need assistance with your Affonso integration, contact Affonso's support team: - Email: hello@affonso.io - Live chat: Available directly in the Affonso dashboard # OurPay Integration in Fernand Source: https://docs.ourpay.dev/features/integrations/fernand ## What is Fernand? [Fernand](https://getfernand.com/) is a modern customer support tool designed for SaaS — it’s fast, calm, and built to reduce the anxiety of answering support requests. ## How it works After connecting your [OurPay](https://ourpay.dev/) account to Fernand, you’ll be able to see customer payment information and product access details directly within each customer conversation. This enables you to: - Instantly verify if someone is an active customer - Prioritize conversations from high-tier plans - View product purchases and payment history in context --- ## How to connect Fernand with OurPay 1. Open [Integrations](https://app.getfernand.com/settings/organization/integrations) in your Fernand organization settings. 2. Click on **Connect OurPay**. 3. You'll be redirected to OurPay to authorize the connection. 4. Once approved, Fernand will begin syncing customer data automatically. That’s it! You’ll now see OurPay customer info directly in Fernand's conversation list and sidebar. --- ## How to automate your inbox with OurPay data Once OurPay is connected, you can create automation rules in Fernand based on OurPay data. Let’s walk through a basic example: auto-replying to all customers on your `Pro` plan. ### Create a new rule 1. Go to [Rules](https://app.getfernand.com/settings/organization/rules) in Fernand. 2. Click `Add rule` and give it a descriptive name. This ensures the rule runs on each new customer message. Now add a condition based on OurPay data. For example: - `Contact is a customer...` - `Contact has paid plan...` You can target specific plans (e.g. `Pro`, `Business`) or specific products to personalize support or automate prioritization. Now define what happens when the rule matches. For example: - Send an auto reply (with variables) - Assign the conversation to a specific agent - Tag the conversation with `priority` or `paid` - Trigger a webhook for external automation ### Disconnecting the integration If you ever want to disconnect OurPay from your Fernand workspace: Deleting your organization on Fernand will also remove the OurPay integration automatically. # OurPay for Framer Source: https://docs.ourpay.dev/features/integrations/framer Introducing the official OurPay plugin for Framer. Allowing you to sell products on your site without having to build a custom checkout flow. ![](https://www.framer.com/marketplace/_next/image/?url=https%3A%2F%2Fy4pdgnepgswqffpt.public.blob.vercel-storage.com%2Fplugins%2F174-egCWZYwZbpLc42xnGQIY42F1KqtNDk&w=1920&q=100) Getting Started ---------------------- [Get your hands on the OurPay plugin in the Framer Marketplace](https://www.framer.com/marketplace/plugins/ourpay/) # Purchase Power Parity with ParityDeals Source: https://docs.ourpay.dev/features/integrations/paritydeals Want to offer different prices in different countries? [ParityDeals](https://www.paritydeals.com/) offers [automatic pricing optimizations depending on customers geolocation](https://www.paritydeals.com/features/purchasing-power-parity-discounts/) and a seamless integration with OurPay. Simple Integration, Powerful Deals ----------------------------------------- * You can easily and securely (OAuth 2.0) connect OurPay to ParityDeals * Select products on OurPay to offer deals for * Configure deals by country or holidays * ParityDeals automatically creates and manages discounts on OurPay * Showing them to customers based on time and geolocation (unless VPN is detected) * Offering great & local deals internationally with ease Setup Guide ------------------ ### Signup to ParityDeals Go to [app.paritydeals.com](http://app.paritydeals.com) and sign up. ### Connect OurPay on ParityDeals In your ParityDeals dashboard, click `Create Deals` > `Create Deals with OurPay`. ### Grant ParityDeals Access (OAuth 2.0) No need to create API access keys and share them externally. Just connect securely and grant the necessary permissions using OurPay OAuth 2.0. ### Choose Products Now, let's select the OurPay products you want to offer deals for. ### Configure Deals Let's configure our deal settings. * Enter your website URL (requires your own site vs. OurPay storefront) * Enter a targeted URL path, e.g `/pricing` to only show deals on that page Now we can configure the deals for different countries. ParityDeals offers great defaults, but you can of course change them. ### Configure Banner You can then customize the ParityDeals banner to suit your site and design. ### Embed Banner Finally, we're all setup over at ParityDeals. Just copy the script to their banner and embed it on your site. You're now done 👏🏼 Questions & Help ----------------------- Checkout the [ParityDeals documentation](https://www.paritydeals.com/docs/) for more guides and information. # OurPay for Raycast Source: https://docs.ourpay.dev/features/integrations/raycast Install Extension ------------------------ [Head over to OurPay on the Raycast Store, and install it from there.](https://www.raycast.com/emilwidlund/ourpay) ### View Orders Easily view orders across organizations. ![](https://files.raycast.com/acvj8yffxqxbnv82lhtsnf7u7x29) ### View Subscriptions View all active subscriptions across your organizations. ![](https://files.raycast.com/y6he77j6ig6hchxbpxdcsd2i1yjf) ### View Customers Keep track of all your customers. # OurPay for Zapier Source: https://docs.ourpay.dev/features/integrations/zapier import { ZapierEmbed } from "/snippets/zapier-embed.mdx"; [Zapier](https://zapier.com/apps/ourpay/integrations) lets you connect OurPay to 2,000+ other web services. Automated connections called Zaps, set up in minutes with no coding, can automate your day-to-day tasks and build workflows between apps that otherwise wouldn't be possible. Each Zap has one app as the **Trigger**, where your information comes from and which causes one or more **Actions** in other apps, where your data gets sent automatically. We've focused on **triggers** (webhooks) for now, so you can react to events in OurPay and trigger actions in other apps. Need to perform actions in OurPay? Tell us about your use case [here](https://github.com/orgs/sunnycodet/discussions/new?category=integrations&labels=integrations%2Fzapier) and we'll consider adding more actions in the future. ## Getting Started with Zapier Sign up for a free [Zapier](https://zapier.com/apps/ourpay/integrations) account, from there you can jump right in. To help you hit the ground running, you'll find popular pre-made Zaps below. ## How do I connect OurPay to Zapier? Log in to your [Zapier account](https://zapier.com/sign-up) or create a new account. Navigate to "My Apps" from the top menu bar. Now click on "Connect a new account..." and search for "OurPay" Use your credentials to connect your OurPay account to Zapier. Once that's done you can start creating an automation! Use a pre-made Zap or create your own with the Zap Editor. Creating a Zap requires no coding knowledge and you'll be walked step-by-step through the setup. Need inspiration? See everything that's possible with [OurPay and Zapier](https://zapier.com/apps/OurPay/integrations). If you have any additional questions, you can open a ticket with Zapier Support from https://zapier.com/app/get-help ## Popular use cases # Orders Source: https://docs.ourpay.dev/features/orders An **order** is the record of a single paid transaction on OurPay. Every successful checkout, subscription cycle, and subscription change generates an order — it's where the money, the tax, the invoice, and the link back to the [product](/features/products) and [customer](/features/customer-management) live. If [subscriptions](/features/subscriptions/introduction) are the _relationship_ with a customer, orders are the _individual payments_ inside that relationship. ## When orders are created OurPay creates an order in all of these cases: - **One-time purchase** — when a customer checks out a non-recurring product. - **Subscription created** — the initial order generated when a customer subscribes. - **Subscription renewal** — every billing cycle of an active subscription. - **Subscription change** — when a plan or seat update is prorated with `invoice` behavior and charges the difference immediately. See [Proration](/features/subscriptions/proration). Each order carries a `billing_reason` that tells you which of these it is: `purchase`, `subscription_create`, `subscription_cycle`, or `subscription_update`. ## Arbitrary charges Sometimes you need to charge a customer an amount that doesn't originate from a checkout or a subscription renewal — a usage overage, a one-off professional-services fee, or a manual top-up. OurPay lets you run these **off-session charges** against a customer's saved payment method, without the customer being present. Off-session charges are currently in preview. You'll only be able to run off-session charges if you are on a paid plan. An off-session charge is a two-step flow: you create a **draft order**, then **finalize** it to attempt the charge. Both endpoints require the `orders:write` scope and the sales-management permission on the organization. ### 1. Create a draft order `POST /v1/orders/` creates an order in `draft` status with no invoice number — nothing is charged yet. You reference an existing customer and a one-time product, and optionally override the amount and description: | Field | Required | Description | | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `customer_id` | Yes | The customer to charge. Must belong to the organization. | | `product_id` | Yes | A one-time product to charge for. Only fixed-price and free products are supported. | | `amount` | No | A custom amount in the smallest currency unit (e.g. `2500` for $25.00). Overrides the product's price; defaults to it. | | `currency` | No | ISO 4217, lowercase (e.g. `usd`). Defaults to the organization's currency. | | `description` | No | The line-item text shown on the invoice and receipt. Defaults to the product name. | | `organization_id` | No | Required unless you authenticate with an organization token. | The customer must have a complete billing address (so OurPay can calculate tax) and at least one saved payment method. ```bash cURL curl --request POST \ --url https://api.ourpay.dev/v1/orders/ \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": "", "product_id": "", "amount": 2500, "description": "5,000 extra tokens" }' ``` The response is the draft order, including its `id`. Creating a draft fires the `order.created` webhook but does not yet email the customer. ### 2. Finalize and charge `POST /v1/orders/{id}/finalize` synchronously attempts the off-session charge. By default it uses the customer's default payment method; pass `payment_method_id` to charge a specific one. ```bash cURL curl --request POST \ --url https://api.ourpay.dev/v1/orders//finalize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{}' ``` On success, the order transitions to `paid`, an invoice number is assigned, any [benefits](/features/benefits/introduction) attached to the product are granted, the customer is emailed their confirmation, and the [`order.paid`](/api-reference/order_paid) webhook fires. If the charge fails, the API returns an error and the order is reverted to `draft` so you can fix the problem and finalize the same order again — no invoice number is consumed by a failed attempt: | Status | When | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `402` | The card was declined, the customer has no payment method, or the charge needs a 3DS / SCA challenge that can't be completed off-session. | | `403` | Off-session charges aren't enabled for the organization, or its account can't currently accept payments. | | `412` | The order is no longer in `draft` status (for example, it was already finalized). | ## Order status An order moves through a small set of statuses over its lifetime: | Status | Meaning | | -------------------- | ---------------------------------------------------------------------------------------- | | `pending` | The order has been created and OurPay is attempting to collect payment. | | `paid` | The payment succeeded. | | `refunded` | The order has been fully refunded. | | `partially_refunded` | Part of the order has been refunded. See [Refunds](/features/refunds). | | `void` | The order will not be collected (for example, it's been voided after repeated failures). | Free orders — those with a total of zero, typically from a $0 subscription or a 100% discount — are marked `paid` immediately with no payment step. ## What's on an order Every order carries: - **Amounts**: subtotal, discount, tax, net, and total, with the currency it was billed in. - **Billing details**: customer billing name and address (editable until the invoice is generated). - **Tax details**: taxability reason, tax rate, and the amount collected — useful if you're operating as merchant of record on your own, or reconciling against OurPay's [MoR tax handling](/merchant-of-record/introduction). - **A link to the product and customer**, and to the subscription if the order came from a subscription. - **The invoice**, once it's been generated. - **[Custom field data](/features/custom-fields)** captured at checkout. ## Invoices OurPay generates a PDF invoice for every paid order. You can: - **Download it** from the order detail page in the dashboard. - **Trigger generation** programmatically via [Generate Order Invoice](/api-reference/orders/generate-order-invoice), then fetch the URL with [Get Order Invoice](/api-reference/orders/get-order-invoice). Customers can download and edit their own invoices — adding a company name, VAT number, or billing address — from the [Customer Portal](/features/customer-portal/introduction), without pulling you into a support thread. Once an invoice has been generated, its billing details are frozen. If you need to correct a name or address after the fact, the customer should edit their invoice from the Customer Portal, which regenerates it. ## Receipts A **receipt** is the proof of payment for an order — what was charged, how, and what's been refunded since. OurPay issues one for every paid order and assigns a per-customer number in the form `RCPT-{customer-id}-{NNNN}`. Each receipt includes: - The **payment method** used (e.g. `Visa — 4242`), the **date paid**, and the **amount**. - Any **customer balance** applied to the order. - Any **refunds** issued against the order, with dates and amounts. - The same line items, taxes, totals, and linked invoice number as the order's invoice. You can **download receipts** from the order detail page in the dashboard, and customers download receipts from the [Customer Portal](/features/customer-portal/introduction) — a **Download Receipt** button appears on each paid order. Programmatically, use [Get Order Receipt](/api-reference/orders/get-order-receipt) for the merchant API or its [customer-portal counterpart](/api-reference/customer_portal/get-order-receipt). The first request for a given order may return `202 Accepted` while the PDF renders. Retry shortly after for a presigned download URL. The Customer Portal handles this for you. ## Refunds Orders can be refunded in full or in part from the dashboard, or programmatically via the [Refunds API](/api-reference/refunds/create-refund). Refunds are a separate resource linked to the order — see [Refunds](/features/refunds) for the rules around what's refundable and how it interacts with payouts. ## Webhooks If you're integrating orders into your own system, OurPay emits an event on every state transition: - [`order.created`](/api-reference/order_created) — a new order exists (not necessarily paid yet). - [`order.paid`](/api-reference/order_paid) — the order has been collected. This is the one most integrations care about. - [`order.updated`](/api-reference/order_updated) — something changed on the order. - [`order.refunded`](/api-reference/order_refunded) — a refund was issued against the order. ## Next steps The recurring relationship that generates most orders. How to refund an order, fully or partially. List and fetch orders, update billing details, and generate invoices. Where customers view their own orders and download invoices and receipts. # Payment providers Source: https://docs.ourpay.dev/features/payment-providers OURPAY keeps checkout, customers, orders, subscriptions, invoices, payments, refunds, disputes, fees, and accounting entries in its own provider-neutral domain. Stripe remains the card rail. PayPal can be enabled as a wallet and recurring-payment rail, with every PayPal capture collected in one central Business account controlled by the deployment operator. The organization — whether it represents a company or an individual — remains the customer-facing seller and invoice issuer. OURPAY is identified as the payment collector. ## PayPal account model PayPal is a first-party integration. OURPAY authenticates with the REST app belonging to the operator Business account that receives all customer money. It does not create Partner Referrals, impersonate sellers, add a PayPal partner fee, or ask PayPal to disburse to a seller. Every organization still has a `PaymentProcessorAccount` so it can enable or disable PayPal and choose its default checkout rail. Those organization-level records all point to the same deployment-managed PayPal credentials. Activating PayPal for an organization authorizes that seller's checkouts to use the central collection account; it does not connect or settle to the seller's own PayPal account. This is a central collection arrangement, not PayPal multiparty seller processing. Enabling an organization in OURPAY does not make that organization a PayPal sub-merchant and does not prove that PayPal or a tax authority has approved the operator's agency arrangement. The deployment operator must obtain the account, contractual, tax, and regulatory approvals required for the countries and products it serves. Do not enable this model in production merely because live credentials work. PayPal's current [Application Guidelines](https://developer.paypal.com/api/rest/reference/policies-and-guidelines/) say the merchant or seller of record must be the primary payment recipient and prohibit payment aggregation. Because this design names the organization as seller while a central OURPAY account receives the funds, treat it as sandbox-only unless PayPal approves this exact arrangement in writing and qualified legal and tax advisers approve it. Without that approval, the commercial model must change: either OURPAY must become the genuine merchant/reseller of record or PayPal funds must flow directly to onboarded seller accounts. ## Invoice issuer and tax records Before PayPal can be enabled, the organization must save its legal invoice name and address under **Settings → Payment providers → Invoice issuer**. This supports company organizations and individual organizations with the same profile: - The saved legal name and address appear as the seller on customer invoices and receipts. - Optional tax registrations appear below the seller address. - Optional invoice notes appear at the bottom of the document. - Every document states that payment was collected by OURPAY on behalf of that seller. - The PDF author is the organization, while OURPAY remains the document generator and collector. The generated PDF is the historical artifact. Changing issuer details changes the invoice checksum for a later explicit regeneration; it never silently substitutes OURPAY's address under the organization's name. Legacy records without a complete issuer profile can still be read, but a new PayPal activation, re-enable, checkout, or renewal is rejected before money moves. Tax amounts and jurisdiction breakdowns continue to come from the configured tax provider and are stored on the canonical order. The corresponding provider tax transaction is recorded by the OURPAY deployment as part of its central collection workflow; seller payable excludes collected tax. An organization name on the PDF does not by itself transfer tax registrations, filing duties, or merchant-of-record status. Configure and validate the legal tax treatment for every invoice issuer before taking live payments. A PayPal Personal account is not sufficient for a live REST app that accepts payments. Use a verified PayPal Business account. The account must be able to receive the currencies offered by your products. ## Supported billing lifecycle - One-time wallet payments use PayPal Orders v2 and immediate capture. - Paid subscription checkouts capture the first payment and save the approved PayPal method. - Free trials use a Vault setup token, so no charge is made until the first billing cycle. - Renewals use the saved Vault payment token as a merchant-initiated subsequent payment. - Full and partial refunds return through the original PayPal capture. - PayPal dispute webhooks update the canonical OURPAY dispute and order records. - Provider-originated refunds and capture reversals are reconciled even when the operator starts them in PayPal rather than in OURPAY. - Capture details store the PayPal payer ID, payer email, and payer name when PayPal supplies them. - Every successful checkout follows the normal OURPAY Order, organization-issued invoice number, matching receipt, benefit, ledger, and email pipeline. Initial PayPal orders use the globally unique checkout ID (plus a genuine retry number when needed) as the PayPal purchase-unit invoice ID. Renewals use an ID composed from the OURPAY order ID and the seller-facing invoice number, plus a dunning-attempt suffix after a real failed charge. - PayPal's seller-receivable breakdown remains the authoritative source for processor fees. ## Cash versus seller payable OURPAY deliberately keeps two records that must not be confused: - **PayPal cash received** is the capture's `receivable_amount` when PayPal converts currencies, otherwise its `net_amount`: customer gross minus the PayPal fee and any reported platform fee. That cash belongs to the central operator account until the operator sends a payout. - **Seller payable** is the organization's internal ledger balance: captured product revenue excluding tax, minus the seller's configured OURPAY payment and subscription fees, refunds, lost disputes, and any payout transactions already present in the ledger, plus fee credits. The PayPal capture fee is stored as an operator processor-cost transaction. A PayPal fee returned on a refund is stored as a positive processor-fee credit. Neither is silently passed through to the seller because the published OURPAY transaction fee is the configured seller charge. If that commercial policy changes, change the account fee configuration and ledger rules explicitly instead of deriving seller payables from the PayPal account balance. Finance labels the current liability as **Seller Payable**. A successful capture does not become a seller payable until the canonical order and balance entries exist. Pending captures and free trials contribute zero. Open disputes do not reduce it until they are lost, so the amount can still change. If the operator has already sent money outside OURPAY, that external transfer is not silently treated as a ledger settlement. ## Configure Stripe Stripe is enabled by default and keeps its existing configuration: ```dotenv OURPAY_STRIPE_ENABLED=true OURPAY_STRIPE_SECRET_KEY= OURPAY_STRIPE_PUBLISHABLE_KEY= OURPAY_STRIPE_WEBHOOK_SECRET= OURPAY_STRIPE_CONNECT_WEBHOOK_SECRET= ``` Set `OURPAY_STRIPE_ENABLED=false` for a PayPal-only deployment. ## Configure PayPal Create a REST app under the operator PayPal Business account that should receive every payment, then set: ```dotenv OURPAY_PAYPAL_ENABLED=true OURPAY_PAYPAL_ENVIRONMENT=sandbox OURPAY_PAYPAL_CLIENT_ID= OURPAY_PAYPAL_CLIENT_SECRET= OURPAY_PAYPAL_MERCHANT_ID= OURPAY_PAYPAL_WEBHOOK_ID= OURPAY_PAYPAL_VAULTING_ENABLED=false OURPAY_PAYPAL_PAYOUTS_ENABLED=false ``` Use sandbox app credentials until the full billing flow passes. Change the environment to `live` and use that same Business account's live app credentials only when you are ready to accept real money. Before replacing credentials from an older connected-seller or partner setup, finish or separately reconcile every existing PayPal order, refund, dispute, and disbursement. The central account's credentials cannot capture or refund an order owned by a different PayPal merchant account. Turn on **Save payment methods** for the REST app and obtain PayPal's required live approval before setting `OURPAY_PAYPAL_VAULTING_ENABLED=true`. Set `OURPAY_PAYPAL_MERCHANT_ID` to the receiving Business account's merchant ID. Both settings are required before OURPAY exposes saved methods, free trials, merchant-initiated renewals, or subscriptions. One-time payments do not require them. The merchant ID is the PayPal account ID (payer ID), not the REST app client secret. OURPAY disables vaulting and removes the value from public checkout responses when both settings are identical. OURPAY labels fixed, undiscounted plans as `SUBSCRIPTION_PREPAID`. Metered, seat-based, unit-based, custom-priced, or discounted plans use `RECURRING_PREPAID`, and that consent pattern is stored with the vaulted method and reused on every merchant-initiated renewal. Complex plan terms remain visible in OURPAY checkout; PayPal's approval screen uses its generic recurring flow where a complete fixed billing-plan representation would be inaccurate. PayPal also requires Risk Data Acquisition for customer-initiated transactions that save a PayPal payment method. OURPAY loads FraudNet on PayPal checkout, generates its per-attempt client metadata ID in the buyer's browser, and sends that same ID to PayPal when it creates the order or setup token. The checkout Content Security Policy allows only the required PayPal FraudNet hosts. Vault and reference-transaction availability is account- and country-dependent, so a successful sandbox flow does not replace PayPal's live approval. `OURPAY_PAYPAL_PAYOUTS_ENABLED` should remain false. Seller payout is intentionally a separate, operator-controlled process. Standard Payouts is a different PayPal product and is outside this central checkout collection flow. The current seller-payable ledger is denominated in the organization's account currency (USD in the standard deployment). OURPAY rejects a PayPal checkout in a different currency before charging the customer rather than inventing an exchange rate or overstating what the seller will receive. It also rejects currencies outside PayPal's published REST-payment set and fractional HUF, JPY, or TWD amounts, which PayPal does not accept. For local or private gateways, `OURPAY_PAYPAL_API_BASE_URL` can override the PayPal API host. Leave it unset for PayPal sandbox or live. ## Webhooks and return URLs Register this webhook URL on the PayPal REST app, replacing the host with the public OURPAY API host: ```text POST https://api.example.com/v1/integrations/paypal/webhook ``` Subscribe it to these implemented events: - `CHECKOUT.ORDER.APPROVED` and `CHECKOUT.PAYMENT-APPROVAL.REVERSED` - `PAYMENT.CAPTURE.COMPLETED`, `PAYMENT.CAPTURE.PENDING`, `PAYMENT.CAPTURE.DECLINED`, `PAYMENT.CAPTURE.DENIED`, `PAYMENT.CAPTURE.REFUNDED`, and `PAYMENT.CAPTURE.REVERSED` - `PAYMENT.REFUND.PENDING` and `PAYMENT.REFUND.FAILED` - `CUSTOMER.DISPUTE.CREATED`, `CUSTOMER.DISPUTE.UPDATED`, and `CUSTOMER.DISPUTE.RESOLVED` - `VAULT.PAYMENT-TOKEN.CREATED` and `VAULT.PAYMENT-TOKEN.DELETED` The order-approved event lets OURPAY capture an approved payment even if the buyer closes the browser before returning. OURPAY verifies every webhook through PayPal's signature-verification endpoint before durably enqueueing it and ignores duplicate PayPal event IDs. Capture and Vault operations also use stable idempotency keys so an uncertain network response can be retried without intentionally creating a second charge or token. A later dunning attempt uses a new key and a new PayPal order, while a transport retry of the same attempt retains its original key. Checkout and Vault approval calls send these return URLs dynamically: ```text GET https://api.example.com/v1/integrations/paypal/checkout/return GET https://api.example.com/v1/integrations/paypal/checkout/cancel ``` ## Enable PayPal for an organization Open **Settings → Payment providers** and choose **Enable PayPal**. OURPAY validates the configured invoice-issuer profile, client ID, and secret, creates or refreshes the direct PayPal processor record, and exposes the capabilities allowed by the deployment configuration. No seller PayPal onboarding redirect is involved. When Stripe and PayPal are both active, checkout shows both choices. Selecting a rail locks the checkout to that processor before creating an external order or intent. ## Settlement and reporting PayPal places captures in the central Business account according to PayPal's normal availability lifecycle. Finance separately shows the seller payable and the sales, configured fees, refunds, disputes, payer identity, and invoice records behind it. Automatic PayPal seller payouts are not offered. Sending seller money and recording that settlement against the payable are intentionally left for the later payout phase, so this release leaves the liability outstanding in OURPAY. Stripe settlements remain independent. ## Test safely 1. Use a PayPal sandbox Business account for the REST app and a separate sandbox Personal account as the buyer. 2. Enable PayPal under **Settings → Payment providers**. 3. Complete a one-time checkout and verify the Payment, Order, invoice, receipt, payer metadata, PayPal gross/fee/net breakdown, and seller-payable ledger entries. 4. Enable Vaulting, then test a paid subscription, a free trial, and at least one automatic renewal. 5. Exercise full and partial refunds and a dispute. 6. Replay a webhook event ID and confirm it does not duplicate a payment, fee, order transition, or seller payable. 7. Confirm pending captures and free trials add no payable, then confirm each successful refund, capture reversal, and lost dispute reduces seller payable exactly once. 8. Repeat the checklist with low-value live products before opening normal sales. See PayPal's official documentation for [REST app setup](https://developer.paypal.com/api/rest/), [Orders v2](https://developer.paypal.com/api/rest/integration/orders-api/), [saved payment methods](https://developer.paypal.com/docs/checkout/save-payment-methods/), [Vault setup tokens](https://developer.paypal.com/api/payment-tokens/v3/definitions/setup_token_request/), and [webhook verification](https://developer.paypal.com/api/webhooks/v1/verify-webhook-signature-post/). # Products Source: https://docs.ourpay.dev/features/products **Everything is a product** Subscriptions and one-time purchases are both products in OurPay — same API, same data model, just different pricing and billing logic. They live together in the Products dashboard, filterable by pricing model. ## Billing cycle A product is either a **one-time purchase** or **recurring**. One-time products charge the customer once and grant access forever. Recurring products bill on an interval — daily, weekly, monthly, or yearly — and you can extend any of those with an interval count to express things like "every 2 weeks" or "every 3 months". The billing cycle and recurring interval are locked in at creation. If you need to change them later, create a new product instead. ## Pricing OurPay supports several pricing models, and you pick one per product: - **Fixed price.** Set an amount and that's what customers pay. - **Pay what you want.** Customers choose the amount. You can set a minimum and a default that's pre-filled at checkout. - **Free.** No charge. Useful for lead magnets, free tiers, or gating benefits behind a sign-up. - **Metered pricing.** Charge based on usage — bill per API call, per token, per anything you can count. [Learn more about usage-based billing](/features/usage-based-billing/introduction). - **Seat-based pricing.** Sell a number of seats with optional volume tiers, and let the buyer assign them to teammates. [Learn more about seat-based billing](/features/seat-based-pricing). Metered prices stack on top of the others. You can pair one with a fixed base fee for a classic "base + usage" plan, and you can attach multiple metered prices to the same product if you want to bill on more than one dimension at once — say, per API call *and* per GB of storage. Pricing type is locked in at creation, but for fixed-price products you can change the amount at any time. Existing subscribers are **grandfathered** onto the price they signed up at, so a price change only affects new purchases. If you do want to migrate someone onto the new price, you can do it [per subscription](/features/subscriptions/manage) from the dashboard or the API. Whether the amount you enter includes tax or has tax added on top depends on your tax behavior setting. By default, OurPay picks the convention that matches the customer's country (inclusive in most of the world, exclusive in the US, Canada, and India). You can set your own default under **Settings**. See [Tax Inclusive Pricing](/features/tax-inclusive-pricing) for how the amount you set translates to what customers actually pay. ### Multiple payment currencies Products can be priced in several currencies at once so customers pay in their local currency. Your organization has a default payment currency that acts as the fallback, and you can add more on top. A price in the default currency is **required** — if you leave it empty, the product is treated as free. To price a product only in another currency, change your organization's default payment currency under **Settings** first. The price structure (price type, metered prices, etc.) must match across every currency you enable. OurPay picks the currency based on the customer's geolocation at checkout. If their currency isn't enabled on the product, it falls back to your organization's default. **Creating checkout sessions from a backend or proxy?** OurPay reads the customer's geolocation from the IP address of the request that creates the session. If you create sessions server-side (an API, a Cloudflare Worker, etc.), OurPay sees *your server's* IP and may pick the wrong currency. Forward the customer's IP as [`customer_ip_address`](/features/checkout/session#customer-ip-address) when creating the session. OurPay supports 130+ currencies for product pricing: | Code | Currency | | --- | --- | | `AED` | United Arab Emirates Dirham | | `ALL` | Albanian Lek | | `AMD` | Armenian Dram | | `AOA` | Angolan Kwanza | | `ARS` | Argentine Peso | | `AUD` | Australian Dollar | | `AWG` | Aruban Florin | | `AZN` | Azerbaijani Manat | | `BAM` | Bosnia-Herzegovina Convertible Mark | | `BBD` | Barbadian Dollar | | `BDT` | Bangladeshi Taka | | `BIF` | Burundian Franc | | `BMD` | Bermudan Dollar | | `BND` | Brunei Dollar | | `BOB` | Bolivian Boliviano | | `BRL` | Brazilian Real | | `BSD` | Bahamian Dollar | | `BWP` | Botswanan Pula | | `BZD` | Belize Dollar | | `CAD` | Canadian Dollar | | `CDF` | Congolese Franc | | `CHF` | Swiss Franc | | `CLP` | Chilean Peso | | `CNY` | Chinese Yuan | | `COP` | Colombian Peso | | `CRC` | Costa Rican Colón | | `CVE` | Cape Verdean Escudo | | `CZK` | Czech Koruna | | `DJF` | Djiboutian Franc | | `DKK` | Danish Krone | | `DOP` | Dominican Peso | | `DZD` | Algerian Dinar | | `EGP` | Egyptian Pound | | `ETB` | Ethiopian Birr | | `EUR` | Euro | | `FJD` | Fijian Dollar | | `FKP` | Falkland Islands Pound | | `GBP` | British Pound | | `GEL` | Georgian Lari | | `GIP` | Gibraltar Pound | | `GMD` | Gambian Dalasi | | `GNF` | Guinean Franc | | `GTQ` | Guatemalan Quetzal | | `GYD` | Guyanaese Dollar | | `HKD` | Hong Kong Dollar | | `HNL` | Honduran Lempira | | `HTG` | Haitian Gourde | | `HUF` | Hungarian Forint | | `IDR` | Indonesian Rupiah | | `ILS` | Israeli New Shekel | | `INR` | Indian Rupee | | `ISK` | Icelandic Króna | | `JMD` | Jamaican Dollar | | `JPY` | Japanese Yen | | `KES` | Kenyan Shilling | | `KGS` | Kyrgystani Som | | `KHR` | Cambodian Riel | | `KMF` | Comorian Franc | | `KRW` | South Korean Won | | `KYD` | Cayman Islands Dollar | | `KZT` | Kazakhstani Tenge | | `LAK` | Laotian Kip | | `LKR` | Sri Lankan Rupee | | `LRD` | Liberian Dollar | | `LSL` | Lesotho Loti | | `MAD` | Moroccan Dirham | | `MDL` | Moldovan Leu | | `MGA` | Malagasy Ariary | | `MKD` | Macedonian Denar | | `MNT` | Mongolian Tugrik | | `MOP` | Macanese Pataca | | `MUR` | Mauritian Rupee | | `MVR` | Maldivian Rufiyaa | | `MWK` | Malawian Kwacha | | `MXN` | Mexican Peso | | `MYR` | Malaysian Ringgit | | `MZN` | Mozambican Metical | | `NAD` | Namibian Dollar | | `NGN` | Nigerian Naira | | `NIO` | Nicaraguan Córdoba | | `NOK` | Norwegian Krone | | `NPR` | Nepalese Rupee | | `NZD` | New Zealand Dollar | | `PAB` | Panamanian Balboa | | `PEN` | Peruvian Sol | | `PGK` | Papua New Guinean Kina | | `PHP` | Philippine Peso | | `PKR` | Pakistani Rupee | | `PLN` | Polish Zloty | | `PYG` | Paraguayan Guarani | | `QAR` | Qatari Riyal | | `RON` | Romanian Leu | | `RSD` | Serbian Dinar | | `RWF` | Rwandan Franc | | `SAR` | Saudi Riyal | | `SBD` | Solomon Islands Dollar | | `SCR` | Seychellois Rupee | | `SEK` | Swedish Krona | | `SGD` | Singapore Dollar | | `SHP` | St. Helena Pound | | `SOS` | Somali Shilling | | `SRD` | Surinamese Dollar | | `SZL` | Swazi Lilangeni | | `THB` | Thai Baht | | `TJS` | Tajikistani Somoni | | `TOP` | Tongan Paʻanga | | `TRY` | Turkish Lira | | `TTD` | Trinidad & Tobago Dollar | | `TWD` | New Taiwan Dollar | | `TZS` | Tanzanian Shilling | | `UAH` | Ukrainian Hryvnia | | `UGX` | Ugandan Shilling | | `USD` | US Dollar | | `UYU` | Uruguayan Peso | | `UZS` | Uzbekistani Som | | `VND` | Vietnamese Dong | | `VUV` | Vanuatu Vatu | | `WST` | Samoan Tala | | `XAF` | Central African CFA Franc | | `XCD` | East Caribbean Dollar | | `XCG` | Caribbean Guilder | | `XOF` | West African CFA Franc | | `XPF` | CFP Franc | | `YER` | Yemeni Rial | | `ZAR` | South African Rand | | `ZMW` | Zambian Kwacha | ## Trial period For recurring products, toggle **Enable trial period** to give customers a window where they aren't charged. Pick a number and a unit (days, weeks, months, or years) and OurPay handles the rest. [Learn more about trials](/features/subscriptions/trials). ## Metadata You can attach arbitrary key–value metadata to a product. It's not shown to customers, but it travels along on every order, subscription, and webhook tied to the product, which makes it useful for keeping track of internal IDs or categories that live outside OurPay. ## Automated Benefits Benefits are what your customers actually get when they buy: license keys, Discord roles, GitHub repository access, file downloads, feature flags, or a custom benefit you wire up yourself. OurPay grants and revokes benefits automatically as customers purchase, renew, or cancel. [Learn more about benefits](/features/benefits/introduction). ## Checkout Page How your product is represented during checkout. ### Description Optional copy that appears on the checkout page. Use it to pitch the product, list what's included, or anything else that helps the customer commit. Markdown is supported. ### Product media Upload images to display on the checkout page. Images can be up to 10MB each, and you can re-arrange or remove them at any time. ### Checkout fields Collect extra information from customers at checkout — phone numbers, terms-of-service agreements, custom data you need for fulfillment, anything you want. Fields are defined once at the organization level and then enabled per product, where you also choose whether each one is required. Supported field types: text, number, date, checkbox, and select. A required checkbox blocks confirmation until the customer ticks it. Handy for legal terms. The collected values show up on the resulting order or subscription. ## Update a product Most things on a product can be edited after the fact, except for the billing cycle and pricing type — those are locked in at creation. To change either, create a new product. A few things to know: - **Existing subscribers stay on their original price.** Changing a fixed price only affects new purchases. - **Benefit changes propagate.** Add a benefit and existing customers get it automatically. Remove one and they lose access. - **Need a similar product?** Use **Duplicate Product** from the product menu to clone an existing one as a starting point — handy for spinning up a yearly variant of a monthly plan, or for A/B testing pricing. ## Archive a product Products can be archived but not permanently deleted. Click **Archive** from the product menu and the product disappears from new checkouts. Existing customers keep their access, and active subscriptions keep renewing. You can unarchive at any time from the same menu to make the product available again. ## FAQ OurPay takes a different approach to what the industry typically calls **variants**. Each product has a single pricing model, and instead of bolting variants onto one product, you create one product per pricing model and showcase them together at checkout. So a "monthly" and "yearly" plan are two products, each with their own pricing and benefits, presented side-by-side via [Checkout Links](/features/checkout/links) or the [Checkout Session API](/features/checkout/session). It keeps the API and the data model clean, and gives you full freedom over what each option includes. Yes, for fixed-price products. Existing subscribers are grandfathered onto their original price and only new purchases see the updated amount — but you can move individual subscribers onto the new price [per subscription](/features/subscriptions/manage) if you want. Billing cycle and pricing type can't change. Yes. Set the price type to **Metered** and link it to a meter that tracks the events you care about. Full walkthrough in the [usage-based billing guide](/features/usage-based-billing/introduction). Yes, via [seat-based pricing](/features/seat-based-pricing). The buyer purchases a number of seats and assigns them to teammates, who each get the product's benefits. If you've enabled additional payment currencies on the product, yes — OurPay matches the customer's geolocation to one of the enabled currencies. If there's no match, it falls back to your organization's default. See [Multiple payment currencies](#multiple-payment-currencies) above. Yes. From the product list or the product menu, pick **Duplicate Product** to clone all the settings into a new draft you can tweak before saving. # Manage Refunds Source: https://docs.ourpay.dev/features/refunds No matter what refund policy you offer to customers, OurPay makes it easy to issue both full and partial refunds, so you can deliver the customer experience and refund policy you want. **OurPay can issue refunds on your behalf** OurPay reserves the right to issue refunds within 60 days of purchase, at its own discretion, in order to prevent chargebacks. We integrate with credit card networks to receive early chargeback signals before a dispute is officially filed, and for lower-value transactions we'll automatically refund the order — and cancel any related subscription — on your behalf to head off the chargeback. This applies even if you have a "no refunds" policy: keeping chargebacks low protects your account, so OurPay may still refund proactively. See [Chargeback Management](/merchant-of-record/account-reviews#chargeback-management) for the wider context. ## What's refundable The maximum refundable amount on an order is the **net amount** (excluding tax) minus anything that's already been refunded, with any customer balance applied to the order taken into account. The corresponding **tax** is refunded automatically alongside it — fully on a full refund, prorated on a partial one. **Payment fees are not refunded** Credit card networks and payment processors charge us for the underlying transaction regardless of whether it's later refunded (industry standard). We therefore can't return our fees, since the cost remains. Example: an order of \$30 costs ~\$1.60 in fees to OurPay. You can still refund the customer \$30, but the ~\$1.60 fee stays deducted from your balance. ## Issuing a refund 1. Go to the order details page for the specific order you want to refund. 2. Scroll down to the "Refunds" section. 3. Click "Refund order". ### Amount Specify the **net amount** to refund — i.e. the amount excluding tax. It defaults to the maximum refundable amount, but you can lower it to issue a partial refund. OurPay **automatically calculates the corresponding tax** to refund based on the order's tax rate. For partial refunds, tax is prorated against the refunded portion; for a full refund, the entire remaining tax is returned. ### Reason Select the reason for the refund — helpful for future reference. ### Revoking benefits **One-time purchases.** You can revoke the customer's access to product benefits — e.g. file downloads, license keys, or Discord/GitHub invites. This is selected by default, since we default to a full refund, but it can be disabled. **Subscriptions.** You can't revoke access by refunding an order tied to a subscription — refunding the order returns the money but does not end the relationship. To end access, [cancel the subscription](/features/subscriptions/manage) instead. OurPay revokes the associated benefits automatically once the subscription itself is revoked. # Seat-Based Pricing Source: https://docs.ourpay.dev/features/seat-based-pricing Seat-based pricing allows you to sell products where a billing manager purchases a specific number of seats and can assign them to team members. Each seat holder gets their own access to the product benefits, making it perfect for team subscriptions, perpetual licenses, and multi-user products. **Seat-based pricing is ideal for:** - Team subscriptions where one billing manager pays for multiple users - Perpetual team licenses with one-time payment - Organizational licenses with per-seat pricing - Products with flat, graduated, or volume-discounted seat pricing ## Customers and members Seat-based products separate who pays from who uses, modeled through three entities: - A **Customer** is the billing entity — who pays. They own subscriptions, orders, and payment methods. On their first seat-based purchase, the customer is permanently upgraded to `type: "team"`, which enables members and team management. - A **Member** is a person under a customer — who uses. Each member has their own email, role (`owner`, `billing_manager`, or `member`), and receives benefit grants independently. The purchaser becomes an `owner` member. Both `owner` and `billing_manager` roles can manage seats and the subscription. - A **CustomerSeat** is the link between a product and a member. It tracks assignment status (`pending`, `claimed`, `revoked`) and holds the invitation token. Because benefits are granted to members, not to the billing customer, always identify the end user by their member — not the customer who paid. ## How it works With seat-based pricing, a billing manager purchases a product (subscription or one-time) with a specific number of seats. They can then: 1. **Assign seats** to team members via email or external customer ID 2. **Manage seats** by resending invitations or revoking access 3. **Scale up** by purchasing additional seats (or a new order for one-time products) 4. **Track usage** by viewing which seats are claimed, pending, or available Team members receive an invitation email with a claim link. Once they claim their seat, benefits are automatically granted. ### Subscriptions vs One-Time Purchases | Feature | Subscriptions | One-Time Purchases | |---------|--------------|-------------------| | **Payment** | Recurring (monthly/yearly) | Single payment | | **Seat Duration** | Active while subscribed | Perpetual (never expire) | | **Adding Seats** | Modify subscription | Purchase new order | | **Benefits** | While subscription active | Forever after claim | Use **subscriptions** for ongoing team access. Use **one-time purchases** for perpetual team licenses. ## Creating a seat-based product From your dashboard, [create a new product](https://ourpay.dev/to/dashboard/products/new). Set your product name, description, and media as usual. Under **Pricing**, select: - **Product type**: Subscription or One-time - **Billing cycle** (subscriptions only): Monthly or Yearly - **Pricing type**: Seat-based Under **Tiering model**, select how seats are priced: | Model | Description | |-------|-------------| | **Fixed price per seat** | Every seat costs the same flat rate. Simple and predictable. | | **Graduated** | Seats are priced per tier range independently — seats in tier 1 cost one rate, seats in tier 2 cost another. Total price is the sum across all tiers. | | **Volume discounts** | The per-seat price is determined by the total seats purchased, and that rate applies to all seats. Crossing a tier threshold lowers the price for everyone. | **Fixed price per seat** is the default and the simplest option — just enter a single price per seat. For **Graduated** and **Volume discounts**, define tiers using a threshold (the seat count at which a new rate begins) and a price per seat for that range. **Example** with tiers at 1–10 seats: \$10/seat and 11+ seats: \$8/seat: - Graduated: 14 seats = 10 × \$10 + 4 × \$8 = \$132 — each range is billed at its own rate - Volume discounts: 14 seats = 14 × \$8 = \$112 — all seats use the lowest matching rate Configure the benefits that seat holders will receive. These are only granted when a seat is claimed, not when purchased. When using the default OurPay confirmation page (no custom `success_url`), the buyer's seat is automatically claimed during checkout, granting them immediate access to benefits. Any remaining seats can be assigned to teammates. If you set a custom `success_url`, the buyer will need to manually assign themselves a seat through the Customer Portal or API if they also want benefits. ## Managing seats After purchase, the billing manager can assign and manage seats from the **Customer Portal** or via the API. ### Seat statuses - **Pending**: Seat assigned, invitation sent, awaiting claim - **Claimed**: Seat claimed by team member, benefits granted - **Revoked**: Seat revoked, benefits removed, can be reassigned ### Key actions - **Assign seats** by email, external customer ID, or existing OurPay customer ID - **Resend invitations** for pending seats if the link expired (valid for 24 hours) - **Revoke seats** to remove benefits and free the seat for reassignment - **Reduce seat count** to lower the number of seats on the subscription (triggers a prorated credit) **Revoking a seat** and **reducing the seat count** are different actions: - **Revoking a seat** removes a specific user's access and frees that seat for reassignment. It does **not** reduce the number of seats on the subscription, and the billing manager continues to pay for the same total. - **Reducing the seat count** changes the subscription quantity itself, which results in a prorated credit for the remainder of the billing period. To stop paying for an unused seat, you must reduce the seat count — not just revoke the assignment. ### Proration and billing adjustments When the seat count on a subscription changes mid-billing cycle, charges are prorated automatically: - **Adding seats**: The billing manager is charged immediately for the new seats, prorated for the remainder of the current billing period. The full per-seat price applies from the next billing cycle onward. - **Reducing seat count**: A prorated credit is applied for the removed seats, covering the unused portion of the current billing period. Proration ensures billing managers only pay for seats during the time they are active. Encourage customers to adjust their seat count rather than leaving unused seats idle. ## Limitations - Seats must be assigned individually (no bulk import via dashboard, use API instead) - Claim links expire after 24 hours - Billing manager does not receive product benefits - Maximum of 1,000 seats per subscription - Metadata limited to 10 keys and 1KB total size per seat ## Next steps For implementation details including API integration, webhook handling, and code examples, see the [Implementing Seat-Based Pricing](/guides/seat-based-pricing) guide. # Single Sign-On Source: https://docs.ourpay.dev/features/sso Single Sign-On is included in the **Scale** plan. Upgrade from [**Settings → Billing**](https://ourpay.dev/to/dashboard/settings/billing) — SSO becomes available as soon as the subscription is active. Single Sign-On connects your organization to your own identity provider. Your team signs in with the credentials they already use, and anyone who authenticates becomes a member of your organization. OurPay supports any provider that implements OpenID Connect, including Google Workspace, Okta, Microsoft Entra ID, and Keycloak. SSO lives under [**Settings → SSO**](https://ourpay.dev/to/dashboard/settings/sso) in your dashboard. ## How people become members Anyone who signs in through your connection becomes a member of your organization. You don't invite them first — authenticating against your identity provider is what grants membership. New members join with the **Member** role. To grant more access, [change their role](/features/team-management#changing-a-members-role) after their first sign-in. Your identity provider decides who can authenticate, so it also decides who can join. Grant and revoke access to the OurPay application there. **Removing a member in OurPay does not keep them out.** If they can still authenticate against your identity provider, they rejoin on their next sign-in. To remove someone's access, revoke it in your identity provider. OurPay only accepts an identity whose email address your provider marks as verified. If the email is unverified, the sign-in is refused and no account is created. ## Setting up a connection Go to [**Settings → SSO**](https://ourpay.dev/to/dashboard/settings/sso) and click **Add connection**. Pick **Google Workspace** or **Custom OIDC provider**. Copy the **Callback URL** shown at the top of the form and register it as a redirect URI in your identity provider. Enter the **Issuer URL** and **Client ID** from your provider. You can paste the discovery URL into the issuer field — OurPay strips the `/.well-known/openid-configuration` suffix. For **Client secret** authentication, paste the secret. For **Private key JWT**, copy the **JWKS URL** into your provider so it can fetch OurPay's public keys. Connections are created disabled. Once your provider is configured, click **Enable** on the connection row. Share the **Login link** from the SSO settings page with your team. It takes them straight to your organization's sign-in page. ### Authorization parameters You can append extra parameters to the authorization request. The most common is `hd`, which pins a Google Workspace domain so users outside it can't sign in. OurPay sets `response_type`, `client_id`, `redirect_uri`, `scope`, `state`, `nonce`, and the PKCE parameters itself. You can't override them. ## Enforcing SSO By default, members can sign in either through SSO or through the standard methods — email code, Google, GitHub, or Apple. Enforcing SSO removes the alternatives: members reach your organization only through your identity provider. To enforce it, you need an enabled connection, and you must already be signed in through this organization's SSO. That proves the connection works before it becomes the only way in. Enforcing SSO disconnects every member and every authorized third-party app. They must sign in again through your identity provider. Enforcement makes your identity provider the only door. Anyone who can't authenticate there loses access, including members you invited by email — they keep their membership, but they can't reach the organization until you add them to your provider. You can stop enforcing at any time from the same screen. Enforcement applies to dashboard sessions. Personal access tokens and organization access tokens keep working, so scripts and integrations aren't interrupted. ## Frequently asked questions No. OurPay creates the account on their first sign-in through your connection, and adds them to your organization. They keep it. Signing in through your connection adds your organization to their existing account — it doesn't create a second one, and it doesn't give your organization access to anything else they own. Yes, as long as you don't enforce SSO. [Invitations](/features/team-management#inviting-a-member) work alongside it, which is useful for contractors who aren't in your identity provider. Once you enforce SSO, invitations stop being a way in. The invited person becomes a member, but reaching your organization requires signing in through your identity provider. Add them there instead. Yes. Add a connection per provider. Each one appears as a separate button on your organization's sign-in page, so give them names. Revoke their access to the OurPay application in your identity provider. Removing them in OurPay alone doesn't stop them from rejoining on their next sign-in. SSO turns off, and enforcement is lifted with it, so your members sign in with the standard methods again. Your connections are kept — upgrading to Scale again restores them as they were. Everyone who joined through SSO stays a member. # Recovering failed payments Source: https://docs.ourpay.dev/features/subscriptions/failed-payments When a subscription renews, OurPay advances it to the next billing cycle first, then attempts to charge the customer's default payment method for the new order. If that charge fails, the subscription moves to `past_due` and enters OurPay's automated **payment recovery** (dunning) flow instead of being canceled straight away. This page explains exactly what happens during that window, how to give yourself breathing room before benefits are revoked, and what levers you have to help the customer recover. ## The retry schedule As soon as the first renewal charge fails: 1. The subscription's status moves from `active` to `past_due`, and `past_due_at` is stamped with the time of the failure. 2. OurPay emails the customer to let them know the charge failed and links them to the [Customer Portal](/features/customer-portal/introduction) so they can update their default payment method. 3. The renewal order stays open with `next_payment_attempt_at` set to the next retry time. OurPay then retries the charge on a fixed schedule, starting from the time of the first failure: | Attempt | Delay from previous | Cumulative time from first failure | | ------- | ------------------- | ---------------------------------- | | 1st retry | 2 days | 2 days | | 2nd retry | 5 days | 7 days | | 3rd retry | 7 days | 14 days | | 4th retry | 7 days | 21 days | If a retry succeeds, the failed order is paid. The subscription returns to `active` once all of its pending orders have been paid (past-due subscriptions keep cycling, so there can be more than one). If all four retries fail — or the payment's decline code indicates the method will never succeed (for example, `lost_card`) — OurPay stops retrying and **revokes the subscription**. Its status moves to `canceled` and benefits are revoked (subject to the grace period below). ## Benefit revocation grace period By default, benefits follow the subscription's status strictly: the moment the subscription leaves `active`, benefits are revoked. For many businesses that's harsh — a single expired card shouldn't instantly lock a paying customer out while they update their details. OurPay has an organization-level **grace period** that holds off benefit revocation while a subscription is in `past_due`. You can set it under **Settings → Subscriptions → Grace period for benefit revocation**. The available values are: - **Immediately** (default) — revoke benefits as soon as the subscription leaves `active`. - **After 2 days** - **After 7 days** - **After 14 days** - **After 21 days** — benefits stay granted for the full length of the retry schedule. The grace period is measured from `past_due_at`. While it's in effect the subscription is still `past_due` (so you can differentiate it in your own app), but the customer retains access to their benefits. Once the grace period expires — or the subscription is revoked for good — benefits are revoked on the next check. The grace period only delays **benefit revocation**. It does not change the retry schedule, and it does not keep the subscription `active`. If you want to treat past-due subscribers specially (for example, with a banner or a reduced feature set), listen for the [`subscription.updated`](/api-reference/subscriptionupdated) webhook and branch on `status === "past_due"`. ## Helping customers recover The most reliable way for a customer to get back into `active` is to update their default payment method from the [Customer Portal](/features/customer-portal/introduction). As soon as the payment method is updated, OurPay retries the charge immediately rather than waiting for the next scheduled attempt. Things you can do from your side: - Link prominently to the Customer Portal from your own app when a customer is `past_due`. - [Issue a refund](/features/refunds) on the original failed order if you want to credit the customer for the lost time while keeping the subscription. - [Reschedule the renewal](/features/subscriptions/manage#reschedule-the-next-renewal) to give the customer extra time before the next attempt. - [Revoke the subscription](/features/subscriptions/manage#revoke-immediately) manually if you've decided not to pursue recovery. # Subscriptions Source: https://docs.ourpay.dev/features/subscriptions/introduction A **subscription** is the recurring relationship between a customer and one of your [products](/features/products). It's created automatically whenever a customer checks out a product that has a recurring price, and it keeps generating orders on each renewal until the customer or you decide to end it. ## How subscriptions work 1. **A customer subscribes.** When a customer completes a [checkout](/features/checkout/session) for a product that has a recurring price, OurPay creates a `subscription` and the first [order](/features/orders). 2. **OurPay renews it automatically.** At the end of each billing period, OurPay advances the subscription to the next cycle and creates a new order, then attempts to charge the customer's default payment method. If the payment fails, the subscription moves to `past_due` and OurPay runs an automated [payment recovery flow](/features/subscriptions/failed-payments). 3. **Benefits stay in sync.** As long as the subscription is active or trialing, the customer keeps access to all the [benefits](/features/benefits/introduction) attached to the product. When the subscription ends, those benefits are revoked. 4. **Customers self-serve from the portal.** The [Customer Portal](/features/customer-portal/introduction) lets customers update their payment method, download invoices, cancel, and — if you allow it — change plans or manage seats. ## Recurring pricing Subscriptions are driven entirely by the way you price the underlying product. Recurring pricing is configured when you [create a product](/features/products#pricing): - **Billing interval.** Daily, weekly, monthly or yearly. - **Interval count.** The number of units per cycle. Setting this to `2` with a `month` interval gives you "every 2 months". - **Pricing type.** Fixed, pay-what-you-want, or free. Free recurring products are still modeled as subscriptions — they just never generate a payment. - **Currency.** A product can have prices in multiple currencies; the subscription locks in the currency used at checkout. The billing interval and pricing type are fixed once a product is created. If you need a different interval (for example, a yearly plan next to a monthly one), create a separate product and show both at checkout. See [Products → Variants](/features/products#variants). ## Renewal reminders For subscriptions on long billing cycles — six months or more — OurPay emails the customer **7 days before the renewal date** to let them know an upcoming charge is coming. This helps with regulations (like the EU's Consumer Rights Directive) that require advance notice on long-interval renewals, and avoids surprise charges that often end in chargebacks. The threshold is six months, however it is expressed: yearly plans, monthly plans with an interval of 6 or more, weekly plans of 25 or more, and daily plans of 180 or more all get the reminder. Reminders are skipped for free subscriptions and for subscriptions already scheduled to cancel at period end. You can turn renewal reminders off under [**Settings → Customer notifications**](https://ourpay.dev/to/dashboard/settings) if you prefer to handle that communication yourself. ## Cancellation A subscription can end in one of two ways, both available to you and your customers: - **Cancel at period end** — the subscription keeps working until `current_period_end`, then transitions to `canceled`. You can reverse this (uncancel) until the end date is reached. - **Revoke immediately** — the subscription moves to `canceled` right away and benefits are revoked. This is irreversible. See [Managing subscriptions](/features/subscriptions/manage) for how to perform these actions and everything else merchants can do from the dashboard or the API. ## Creating subscriptions There are two ways a subscription can come into existence: - **Through checkout.** The default. Works for any paid recurring product. See [Checkout](/features/checkout/session). - **Through the API, for free recurring products only.** You can call [Create Subscription](/api-reference/subscriptions/create-subscription) to subscribe an existing customer to a free product — no order, no email, no charge. This is useful for freemium onboarding flows where you want every signup to have a subscription tied to it from day one. Paid products always go through checkout so that OurPay can collect and validate the payment method. ## Next steps Change plans, seats, trials, billing dates, cancel or revoke — from the dashboard or the API. Control how the price difference is handled when a subscription is upgraded or downgraded. How OurPay retries failed renewals and when benefits are revoked. List, create, update, and revoke subscriptions programmatically. ## FAQ A product is the thing you're selling — name, description, benefits, pricing. A subscription is one specific customer's ongoing relationship with that product. OurPay treats one-time purchases and recurring subscriptions as the same kind of object (both are "products"); the only difference is whether the price is recurring. See [Products](/features/products) for the full model. By default, no — a customer can only have one active subscription per organization at a time. This keeps the common case simple. If your product genuinely needs multiple parallel subscriptions per customer, you can opt in under **Organization Settings → Subscriptions** by toggling on **Allow multiple subscriptions**. At the end of each billing cycle. When a subscription renews, OurPay advances the cycle, creates a new order with tax and any applicable discount, and charges the customer's default payment method. If the charge fails the subscription moves to `past_due` and OurPay starts the [payment recovery flow](/features/subscriptions/failed-payments). The first charge happens at checkout (or when the [trial](/features/subscriptions/trials) ends, if one is configured). Not the amount on an existing subscription directly — subscriptions lock in the price they were created with so existing subscribers aren't surprised by changes. If you update the price on a product, the change applies only to **new** subscriptions. To move an existing subscriber to different pricing, either create a new product and run an [update to switch them to it](/features/subscriptions/manage#change-the-plan), or ask them to change plans themselves from the Customer Portal. Benefits are tied to the subscription's status, not to the cancellation action itself: - If you **cancel at period end**, the customer keeps their benefits until `current_period_end` — they paid for that period. - If you **revoke immediately**, the subscription becomes `canceled` right away and all benefits are revoked. - If a renewal payment can't be recovered after OurPay's retries, the subscription moves to `unpaid` and benefits are revoked the same way as a revoke — subject to the optional [grace period](/features/subscriptions/failed-payments#benefit-revocation-grace-period) you can configure on your organization. # Managing subscriptions Source: https://docs.ourpay.dev/features/subscriptions/manage Once a subscription exists, you'll want to adjust it over time — change plans, extend trials, tweak seats, or end it. This page covers everything a merchant can do to a subscription, both from the dashboard and through the API. All of these actions are available under **Sales → Subscriptions** in the dashboard, and via the [Update Subscription](/api-reference/subscriptions/update-subscription) endpoint. Customers can also perform a subset of them from the [Customer Portal](/features/customer-portal/introduction) — which ones is controlled by your [portal settings](/features/customer-portal/settings). ## Change the plan Switch a subscription to a different recurring product — the standard upgrade or downgrade flow. - When the change takes effect depends on the [proration behavior](/features/subscriptions/proration): `invoice` and `prorate` apply the new product immediately, while `next_period` schedules a pending update that's only applied at the start of the next billing cycle. - The new product must share the subscription's currency. - You can upgrade a non-seat subscription to a seat-based product — the billing customer is auto-promoted to a `team` customer and claims a seat — but you can't switch a seat-based subscription back to a non-seat product. Non-seat → seat changes must apply immediately, so `next_period` proration isn't allowed for that transition. - You can't change the plan on a subscription that's already canceled or scheduled to cancel — uncancel first. - Plan changes on a **trialing** subscription are allowed. The trial carries over with its end recomputed from the new product's trial length (anchored to the original `trial_start`). If the new product has no trial — or its trial would already have elapsed — the trial ends immediately and a fresh billing cycle starts on the new product. - Custom-priced products (pay-what-you-want) aren't valid destinations for a plan change. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "product_id": "", "proration_behavior": "prorate" }' ``` ```py Python ourpay.subscriptions.update( id="", subscription_update={ "product_id": "", "proration_behavior": "prorate", }, ) ``` Read more in [Proration](/features/subscriptions/proration). ## Change the number of seats For [seat-based subscriptions](/features/seat-based-pricing), update how many seats the customer is paying for. Like plan changes, when the seat count actually takes effect depends on the [proration behavior](/features/subscriptions/proration): immediate with `invoice` or `prorate`, scheduled for the next cycle with `next_period`. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "seats": 25, "proration_behavior": "invoice" }' ``` ## Apply or change a discount Attach a [discount](/features/discounts) to an active subscription, or remove the current one by passing `null`. The change is applied to the **next** billing cycle — it doesn't retroactively re-bill the current period. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "discount_id": "" }' ``` ## Manage the trial You can add, extend, or end a [trial](/features/subscriptions/trials) on any subscription: - **Add or extend** a trial by setting `trial_end` to a future date. If the subscription is currently active, its status switches to `trialing` and the next charge is postponed to the new date. - **End a trial immediately** by setting `trial_end` to `"now"`. The subscription becomes `active` and a new billing cycle — and charge — starts on the spot. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "trial_end": "2026-06-01T00:00:00Z" }' ``` ## Reschedule the next renewal If you need to move a subscription's renewal date — for example, to align several subscriptions on the same day, or to extend the current period as a goodwill gesture — you can set a new `current_billing_period_end`. The new date has to be in the future, and this operation isn't available on canceled subscriptions. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "current_billing_period_end": "2026-07-15T00:00:00Z" }' ``` ## Pause and resume Pausing stops billing without ending the subscription. Use it for seasonal plans or account "freezes", where a customer steps away for a while and comes back to the same subscription and payment method. ### Pause at period end Calling pause sets `pause_at_period_end = true`. The subscription stays **active** and keeps its benefits until its `current_period_end`, then it moves to the `paused` status: benefits are revoked and no further orders are generated. Pausing never charges the customer and never takes effect mid-period. Pass an optional `resumes_at` date to schedule an automatic resume. It has to be after the current period end. Leave it out to pause indefinitely and resume by hand later. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "pause_at_period_end": true, "resumes_at": "2026-11-01T00:00:00Z" }' ``` You can only pause a subscription that's active, with no pending cancellation or pause. To cancel a scheduled pause before it takes effect, set `pause_at_period_end` back to `false`. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "pause_at_period_end": false }' ``` ### Resume Resuming a paused subscription takes effect **right now**: status moves back to `active`, a new billing period starts from the resume date, and the customer is charged immediately. The payment method stays on file, so there's no second checkout. An automatic resume on the `resumes_at` date does exactly the same thing. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "resume": true }' ``` ## Cancel or revoke OurPay distinguishes between **canceling** and **revoking** a subscription. Both end the customer's access eventually — the difference is when. ### Cancel at period end Calling cancel (or toggling "Cancel at period end" in the dashboard) sets `cancel_at_period_end = true` and schedules the subscription to end on its `current_period_end`. Until then: - The subscription stays **active** and the customer keeps their benefits — they paid for that period. - No further orders are generated after the current one. - You can **uncancel** at any time before the end date, which reverts the scheduled cancellation. This is the gentler option and the one customers trigger themselves from the [Customer Portal](/features/customer-portal/introduction). It's also what satisfies the "cancel the way you signed up" requirement in jurisdictions like California's [Automatic Renewal Law](https://oag.ca.gov/consumers/auto-renewing-subscriptions). ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "cancel_at_period_end": true, "customer_cancellation_reason": "too_expensive" }' ``` You can optionally record a **cancellation reason** and **comment** — useful for tracking churn. The supported reasons are `too_expensive`, `missing_features`, `switched_service`, `unused`, `customer_service`, `low_quality`, `too_complex`, and `other`. Only set the cancellation reason and comment when they actually come from the customer (for example, from a cancellation survey or a support conversation). Customers can see their `customer_cancellation_comment` in their purchase history, so don't put internal notes there. ### Revoke immediately Revoking ends the subscription **right now**: status moves to `canceled`, `ended_at` is set to the current time, and all benefits are revoked. There's no refund — if you need to issue one, do it separately from [Refunds](/features/refunds). Use this when access needs to stop immediately — a terms-of-service violation, a chargeback, or an explicit customer request. ```bash cURL curl --request DELETE \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' ``` Revoking is irreversible. If you want the option to reverse the decision, cancel at period end instead. ### Uncancel If a subscription is set to cancel at period end and hasn't ended yet, you (or the customer) can reverse the decision. The `cancel_at_period_end` flag is cleared, `ends_at` and `canceled_at` are unset, and the subscription goes back to renewing normally. ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "cancel_at_period_end": false }' ``` Uncancelling is not possible once the subscription has actually ended. ## What customers can do The [Customer Portal](/features/customer-portal/introduction) exposes a subset of these actions to the customer, gated by your [portal settings](/features/customer-portal/settings): - **Cancel at period end** is always available — this is the self-service guarantee the portal provides. - **Update the default payment method** is always available — the primary way customers recover from a failed renewal. - **Change plan** is available when **Enable subscription plan changes** is on. - **Change seats** (for seat-based subscriptions) is available when **Enable subscription seat management** is on. - **Pause and resume** is available when **Enable subscription pause** is on. Everything else on this page is merchant-only: revoking a subscription, applying or changing a discount, extending or ending a trial, and rescheduling the renewal date are not exposed to customers. # Proration Source: https://docs.ourpay.dev/features/subscriptions/proration Whenever a subscription's price changes mid-cycle — typically because the customer switched to a different product or changed seats — there's an unused portion of the current billing period that has already been paid for. **Proration** is how OurPay reconciles that difference. You pick the proration behavior either at the organization level (as the default) or per API call. ## Proration behaviors OurPay supports three standard proration behaviors: ### Prorate and charge now (`invoice`) The subscription is updated **immediately** and OurPay invoices the prorated difference right away. If the update is an upgrade the customer is charged; if it's a downgrade they're credited on the new invoice. Use this when you want money to change hands at the same time as the change — for example, on upgrades where you want to collect the extra revenue now. ### Prorate on next invoice (`prorate`) The subscription is updated **immediately**, but the prorated difference is carried over and applied on the **next scheduled invoice** instead of triggering a charge now. The customer's billing cycle is unchanged. This is typically the smoothest experience for the customer: their plan changes instantly, and they see the adjustment on their regular renewal invoice. If the billing interval changes (for example, monthly to yearly), `prorate` is promoted to `invoice` automatically — there's no "next invoice" on the old cycle to defer the difference to. For `invoice` and `prorate`, the subscription update is applied only if the immediate payment (if any) succeeds. If the payment fails, the API returns an error and the subscription stays unchanged. ### Schedule for next cycle (`next_period`) The change is **not applied immediately**. It's scheduled as a pending update and applied at the start of the next billing period. No proration charge or credit is issued — the new plan simply takes effect on renewal. While a `next_period` update is pending, the subscription's `pending_update` field describes the scheduled change. Submitting a new update always supersedes the pending one: if you scheduled a `next_period` change and then make another update with `invoice` or `prorate`, the pending update is discarded and the new change is applied right away. This behavior is the safer default for downgrades where you don't want to issue credits, and for any case where you want the current period's terms to stay intact. ### Charge full amount and reset cycle (`reset`) The subscription switches to the new plan **immediately**, OurPay invoices the **full price of the new plan** right away, and the **billing cycle is reset** to start now. No proration is applied: the customer isn't credited for the unused portion of the old period, and they aren't charged a prorated amount — they pay the full new price and begin a fresh billing period. Because the cycle restarts, the new period's anchor day becomes the day the change was made, and all future renewals follow the new schedule. Use this when a plan change should behave like a brand-new subscription — for example, when an upgrade should restart the commitment period rather than carry over the current one. The `reset` proration behavior is currently in preview. You'll only be able to use the `reset` proration behavior if you are on a paid plan. ## Setting the default behavior Each organization has a default proration behavior that applies whenever you don't pass an explicit `proration_behavior` on an API call — including plan changes customers initiate from the [Customer Portal](/features/customer-portal/introduction). You can change it from **Settings → Subscriptions** in the dashboard, or via the [Update Organization](/api-reference/organizations/update-organization) API. ## Overriding per update Every subscription update that changes the price — a product change or a seat change — accepts an optional `proration_behavior` that overrides the organization default for that single call: ```bash cURL curl --request PATCH \ --url https://api.ourpay.dev/v1/subscriptions/{subscription_id} \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "product_id": "", "proration_behavior": "invoice" }' ``` ```py Python from ourpay_sdk import OurPay with OurPay(access_token="") as ourpay: res = ourpay.subscriptions.update( id="", subscription_update={ "product_id": "", "proration_behavior": "invoice", # or "prorate" or "next_period" }, ) ``` Valid values are `invoice`, `prorate`, and `next_period`. If your organization has access to the preview `reset` behavior, you can pass `reset` here as well. ## How the prorated amount is calculated OurPay prorates on a **per-second** basis. If `S` seconds remain out of `T` total seconds in the current billing period: - Unused credit on the old plan: `old_plan_price * S / T` - Charge on the new plan for the remainder: `new_plan_price * S / T` - Prorated difference: the new-plan remainder minus the old-plan unused credit For an upgrade the difference is positive, so the customer is charged; for a downgrade it's negative, so the customer is credited. ### Upgrade A customer subscribed to a \$5/month plan on a 30-day month (2,592,000 seconds total). After 1 day (86,400 seconds elapsed, 2,505,600 seconds remaining) they upgrade to a \$20/month plan. - Unused credit on the $5 plan: `\$5 * 2,505,600 / 2,592,000 = \$4.83` - New charge for the remaining 2,505,600 seconds on the $20 plan: `\$20 * 2,505,600 / 2,592,000 = \$19.33` - **Prorated difference: $14.50** With `invoice`, that \$14.50 is charged immediately. With `prorate`, it's added to the next monthly invoice (which is also the new \$20 charge for the next cycle). ### Downgrade A customer subscribed to a \$20/month plan on a 30-day month (2,592,000 seconds total). After 1 day (2,505,600 seconds remaining) they downgrade to a \$5/month plan. - Unused credit on the $20 plan: `\$20 * 2,505,600 / 2,592,000 = \$19.33` - New charge for the remaining 2,505,600 seconds on the $5 plan: `\$5 * 2,505,600 / 2,592,000 = \$4.83` - **Prorated difference: -$14.50** (credit to the customer) With `invoice`, a credit invoice for $14.50 is issued immediately. With `prorate`, the credit is applied on the next invoice. Because proration is computed from the exact number of seconds remaining, the change time-of-day matters: upgrading at noon credits a different amount than upgrading at midnight. OurPay always uses the real length of the current billing period (so 28-, 29-, 30-, and 31-day months are all handled exactly). # Trials Source: https://docs.ourpay.dev/features/subscriptions/trials Trials are a great way to let potential customers experience your product before committing to a subscription. With OurPay, you can easily set up free trials for your subscription products. ## Setting up a trial You can set up a trial period through the following means: - When creating or editing a [product](/features/products). - When creating or editing a [checkout link](/features/checkout/links). - When creating a Checkout Session through the [API](/api-reference/checkouts/create-checkout-session). If you set a trial period on the Checkout Link or Checkout Session, it will **override the trial period set on the product**. The trial period consists of two parameters: - **A unit**: day, week, month, or year. - **A duration**: a number representing how many units the trial will last. ## Starting a trial When a customer checks out a subscription product with a trial period, they will not be charged immediately. Instead, they will have access to the product for the duration of the trial period. We'll still collect their payment information at checkout, but they won't be charged until the trial period ends. This means that if they decide to cancel before the trial ends, they won't be charged at all. Once the trial period ends, the customer will be automatically charged for the subscription, and their billing cycle will begin. ## Adding, extending or canceling a trial For existing subscriptions, you can add, extend or cancel a customer's trial period at any time through the dashboard, from the subscription details page. Click on **Update Subscription**, then click on the **Trial** tab. To add or extend a trial, set a new trial end date in the future. If the subscription was active, its status will be changed to **trialing**, and the billing will be postponed until the end of the trial. To cancel a trial, click on the **End trial** button. The subscription will become active immediately, and the customer will be charged immediately for a new billing cycle. ## Trial conversion reminders Before a trial ends, OurPay emails the customer to remind them that they're about to be charged. This keeps conversions transparent and avoids surprise charges that often end in chargebacks. When the reminder goes out depends on the length of the trial: - **Trials of 3 days or more** — reminder sent **3 days before** the trial ends. - **Trials between 1 and 3 days** — reminder sent **1 day before** the trial ends. - **Trials shorter than 1 day** — no reminder is sent (there isn't enough runway). Reminders are skipped for fully free subscriptions and for trialing subscriptions already scheduled to cancel at period end. You can turn trial conversion reminders off under [**Settings → Customer notifications**](https://ourpay.dev/to/dashboard/settings) if you prefer to handle that communication yourself. ## Preventing trial abuse To protect your business from customers repeatedly signing up for trials, you can enable the **Prevent trial abuse** feature from your organization's subscription settings. ### How it works When this feature is enabled, OurPay tracks trial redemptions using: - **Email addresses**: We automatically detect and normalize email aliases (e.g., `user+alias@example.com` is treated as `user@example.com`) to prevent abuse through simple email variations. - **Payment method fingerprints**: We track the unique fingerprint of payment methods (credit cards) used during checkout to identify returning customers even if they use a different email address. A customer will be blocked from starting a new trial if they have previously redeemed a trial for any of your products and match either: - The same unaliased email address, OR - The same payment method fingerprint ### Enabling the feature 1. Open your organization's [**Settings**](https://ourpay.dev/to/dashboard/settings) page 2. Navigate to the **Subscription** section 3. Toggle on **Prevent trial abuse** Once enabled, the feature will apply to all future trial checkout attempts across all your products. ### Customer experience When a customer who has already used a trial attempts to check out again: 1. They will see an error message: "You have already used a trial for this product. Trials can only be used once per customer." 2. The checkout will automatically refresh without the trial period 3. The customer can still complete their purchase and subscribe at the regular price This approach ensures a smooth experience while protecting your business from trial abuse. # Tax Inclusive Pricing Source: https://docs.ourpay.dev/features/tax-inclusive-pricing When displaying a price to a customer, there are two common conventions: either the displayed price **already includes tax** (inclusive), or tax is **calculated and added on top** at checkout (exclusive). OurPay gives you fine-grained control over this behavior at both the organization level and on individual product prices. ## Tax Behavior Options There are three options available: | Option | Description | | ------------------ | -------------------------------------------------------------------------------------------------- | | **Location-based** | OurPay automatically picks the right behavior based on the customer's country. This is the default. | | **Inclusive** | The price shown to the customer already includes tax. Tax is extracted from the total at checkout. | | **Exclusive** | The price shown to the customer is before tax. Tax is calculated and added on top at checkout. | **Location-based** is the recommended option for most businesses. It follows common regional conventions — customers in the United States, Canada, and India see exclusive pricing, while customers in the rest of the world (e.g. EU, UK) see inclusive pricing. ### How it affects checkout - **Inclusive**: If a product is priced at \$12.00 and the tax rate is 20%, the customer pays \$12.00 — \$2.00 of which is tax and \$10.00 is the net amount. - **Exclusive**: If a product is priced at \$10.00 and the tax rate is 20%, the customer pays \$12.00 — \$10.00 net plus \$2.00 tax added on top. In both cases the tax amount is fully visible and itemized on the checkout page and on receipts. ## Setting the Default Tax Behavior The default tax behavior applies to all products in your organization that don't have a price-level override. To change it: 1. Go to your organization's settings. 2. Find the **Default tax behavior** selector under the **Payments** section. 3. Choose **Location-based**, **Inclusive**, or **Exclusive**. 4. The setting is saved automatically. ## Location-based Behavior in Detail When the tax behavior is set to **Location-based**, OurPay resolves the actual inclusive/exclusive behavior at checkout time based on the customer's billing address: | Countries | Behavior | | ---------------------------- | ----------------------------------------------------------- | | United States, Canada, India | **Exclusive** — tax is added on top of the listed price | | All other countries | **Inclusive** — tax is already included in the listed price | This follows prevailing regional conventions: North American and Indian consumers generally expect prices before tax, while European and most other international customers expect VAT-inclusive pricing. ## Relationship with Merchant of Record As a [Merchant of Record](/merchant-of-record/introduction), we handle the calculation, collection, and remittance of sales taxes globally on your behalf. Tax behavior only controls **how the price is presented** to your customer — it does not affect whether tax is collected or how much is owed. Regardless of the tax behavior chosen, OurPay always: - Calculates the correct tax amount for the customer's jurisdiction. - Displays the tax breakdown clearly on the checkout page. - Issues a compliant receipt that itemizes the tax amount. - Remits the collected tax to the appropriate tax authorities. Changing the tax behavior on a price does **not** change the effective tax rate or your tax obligations. It only changes whether the stated price is presented as tax-included or tax-excluded to the customer. # Team Management Source: https://docs.ourpay.dev/features/team-management OurPay organizations support multiple users. You can invite teammates to collaborate on products, orders, customers, and other parts of your business — each with a role that determines what they're allowed to do. Team management lives under [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members) in your dashboard. On the Scale plan you can connect your own identity provider instead of inviting people one by one. See [Single Sign-On](/features/sso). ## Roles Every member of an organization has one of three roles: | Role | Description | | --- | --- | | **Owner** | There is always exactly one owner per organization. Has the same permissions as an admin. | | **Admin** | Full access to the organization — including managing members, finances, and organization settings. | | **Member** | Operational access to day-to-day work (products, customers, orders, analytics) but cannot manage other members, finances, or organization settings. | ### Permissions The exact permissions granted by each role: | Permission | Member | Admin | Owner | | --- | :---: | :---: | :---: | | View & manage products | ✓ | ✓ | ✓ | | View & manage customers | ✓ | ✓ | ✓ | | View & manage custom fields | ✓ | ✓ | ✓ | | View & manage sales (orders, subscriptions, refunds) | ✓ | ✓ | ✓ | | View & manage analytics | ✓ | ✓ | ✓ | | View other members | ✓ | ✓ | ✓ | | Invite, remove & change member roles | | ✓ | ✓ | | View & manage finances (balance, payouts) | | ✓ | ✓ | | Manage organization settings (including webhooks & API keys) | | ✓ | ✓ | `owner` and `admin` carry the same permissions — the owner is distinguished by the fact that every organization always has exactly one, not by additional capabilities. ## Inviting a member 1. Go to [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members) in your organization's dashboard. 2. Click **Invite**. 3. Enter the email address of the person you want to add. 4. Click **Send Invite**. The invitee receives an email with a link to join your organization. If they don't already have a OurPay account, one is created for them when they accept the invitation. New members join with the **Member** role by default. To grant additional access, change their role after they've been added. **Already a member?** If you invite an email that's already part of your organization, no new invite is sent — the existing membership is shown instead. **Enforcing [SSO](/features/sso)?** Invitations stop being a way in. The invited person becomes a member, but reaching the organization requires signing in through your identity provider. Add them there instead. ## Changing a member's role Only **admins** and **owners** can change roles. 1. Go to [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members). 2. Click the **⋯** menu on the member's row. 3. Choose **Change role**. 4. Select **Admin** or **Member** and click **Save**. The role picker shows a side-by-side permissions matrix so you can confirm what each role grants before saving. **The owner's role can't be changed from this menu.** Every organization has exactly one owner at all times. Ownership can only be transferred to another member of the organization who has completed identity verification — please [contact support](/support) to request an ownership transfer. ## Removing a member Only **admins** and **owners** can remove other members. 1. Go to [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members). 2. Click the **⋯** menu on the member's row. 3. Choose **Remove from organization**. 4. Confirm the removal. The removed member immediately loses access to the organization. They keep their OurPay account and any other organizations they're a member of. **You can't remove the owner.** Every organization always has exactly one owner — to remove the current owner, ownership must first be transferred to another member who has completed identity verification. Please [contact support](/support) to request an ownership transfer. **Using [SSO](/features/sso)?** Removing a member here doesn't keep them out. Anyone who can still authenticate against your identity provider rejoins on their next sign-in. Revoke their access in your identity provider instead. ## Leaving an organization If you want to leave an organization yourself: 1. Go to [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members). 2. Click the **⋯** menu on your own row. 3. Choose **Leave organization** and confirm. You can't leave an organization if you are: - The **owner** — ownership must be transferred first. - The **only remaining member** of the organization. ## Frequently asked questions No. If the invited email doesn't have a OurPay account yet, one is created automatically. The user signs in to claim it when they accept the invitation. Yes, if your organization uses [Single Sign-On](/features/sso). Anyone who authenticates through your identity provider becomes a member with the Member role — no invitation needed. Your identity provider controls who can do that. Yes. A user can be a member of any number of organizations and may have different roles in each one. You can switch between them from the organization picker in the dashboard. Ownership can only be transferred to another member of the organization who has completed identity verification. Every organization always has exactly one owner — the previous owner becomes an admin once the transfer completes. Please [contact support](/support) to request an ownership transfer. # Billing Source: https://docs.ourpay.dev/features/usage-based-billing/billing ## Metered Pricing Metered Pricing is a pricing model where you charge your customers based on the usage of your application. There are a few different pricing models unique to Usage Based Billing: - Unit Pricing - Volume Pricing _(coming soon)_ ### Unit Pricing Unit pricing is a simple pricing model where you charge a fixed amount for each unit of usage. For example: | Product Meter | Price per unit | | ------------------- | -------------- | | `prompt-tokens` | $0.10 | | `completion-tokens` | $0.18 | This means that every unit of `prompt-tokens` consumed by a customer will be charged at \$0.10 and every unit of `completion-tokens` will be charged at \$0.18. It's a linear pricing model, where the price per unit is fixed. ### Volume Pricing _(coming soon)_ Volume pricing is a pricing model where you charge a fixed amount for a certain volume of usage. Volume pricing is not yet available, but will be coming soon. ## Invoicing Customers for Usage Our Usage Based Billing infrastructure is built to work with Subscription products out of the box. ### Add a metered price to your product To charge your customers for usage, you need to add a metered price to your product. You'll need to select the **Meter** and the **amount per unit**. The price is always entered as the cost per **single unit**. How it is displayed to customers — for example as `$20 / 1M tokens` — is controlled by the [Unit](/features/usage-based-billing/meters#unit) setting on the meter. Optionally, you can set a **cap**. The customer will be charged the cap amount if they exceed it, regardless of the usage. ### Monthly Invoicing If a customer has a subscription with a monthly billing period, usage is aggregated monthly and invoiced at the end of the month with the rest of the subscription. ### Yearly Invoicing If a customer has a subscription with a yearly billing period, usage is aggregated yearly and invoiced at the end of the year with the rest of the subscription. ### Usage Charges and Subscription Cancellation When a subscription is canceled, it generally remains active until the end of the current billing period (known as the grace period). During this grace period, all accumulated usage-based charges continue to be tracked. A final invoice will be issued at the end of that period to cover the consumed usage, even if the subscription will not be renewed. This ensures no pending usage charges are lost. If a [discount](/features/discounts) is applied on the subscription, it'll be applied on the **whole invoice**, including metered usage. ## Customer Portal Customers can view their estimated charges for each meter in the Customer Portal. # Credits Source: https://docs.ourpay.dev/features/usage-based-billing/credits Credits is the way to pre-pay for usage in OurPay. It allows you to give your customers the ability to pre-pay for usage instead of risk getting a hefty bill at the end of the month. ## How Credits Work When you ingest events into a Usage Meter, customers will be charged for the usage based on the product's pricing model. However, sometimes you may want to give your customers the ability to pre-pay for usage instead of risk getting a hefty bill at the end of the month. When you issue Credits to a customer, we first deduct the Credits from their Usage Meter balance. If the Usage Meter balance reaches 0, the customer will be charged for the overage. ### Credits-only spending To avoid any overage charges, don't create any Metered price on your product. This way, billing won't be triggered at all for the meter ## Issuing Credits with the Credits Benefit The Credits benefit will credit a customer's Usage Meter balance at different points in time depending on the type of product the benefit is attached to. ### Subscription Products The customer will be credited the amount of units specified in the benefit at the beginning of every subscription cycle period — monthly or yearly. ### One-Time Products The customer will be credited the amount of units specified in the benefit once at the time of purchase. ## Tracking customer's balance In your application, you'll likely need to track the customer's balance for a given meter. The easiest way to do this is to use the [Customer State](/integrate/customer-state), which will give you the overview of the customer, including the balance for each of their active meters. You can also specifically query the meters balance using the [Customer Meters API](/api-reference/customer_meters/list-customer-meters). OurPay doesn't block usage if the customer exceeds their balance. You're responsible for implementing the logic you need to prevent usage if they exceed it. # Event Ingestion Source: https://docs.ourpay.dev/features/usage-based-billing/event-ingestion import Events from "/snippets/usage/events.mdx"; ## Ingest events using the OurPay SDK To ingest events, you can use the OurPay SDKs. ### TypeScript Example ```typescript import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env["OURPAY_ACCESS_TOKEN"] ?? "", }); await ourpay.events.ingest({ events: [ { name: "", externalCustomerId: "", metadata: { key: "value", }, }, ], }); ``` You are always responsible for checking the balance of your customers' Usage Meter. As events always are ingested, we will never prohibit any customer's action based on their Usage Meter balance. ## Ingestion Strategies To make it easier to ingest events, we have created a set of ingestion strategies for common event sources. Learn more about our [Ingestion Strategies](/features/usage-based-billing/ingestion-strategies). ## Good to know ### Events are immutable Once an event is ingested, it cannot be changed, nor can it be deleted. ### Backdated events You can set the `timestamp` field on an event to any point in the past — useful for batched ingestion or replaying from your own queue. The timestamp does not need to be part of the current billing cycle. OurPay attributes events to billing periods based on **when they were received by OurPay**, not the supplied `timestamp`. An event ingested today with a `timestamp` from a previous billing period is included on the **current cycle** — it is not added to the already-closed invoice for the period it logically belongs to. OurPay never issues retroactive invoices or credits for late events. The supplied `timestamp` is still used to: - Place events on the time series shown in the dashboard and the Customer Portal. - Set the date range displayed on the metered line item of the next invoice. # Delta Time Strategy Source: https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/delta-time-strategy ## Javascript SDK Ingest delta time of arbitrary execution. Bring your own now-resolver. ``` pnpm add @ourpay-sh/ingestion ``` ```typescript import { Ingestion } from "@ourpay-sh/ingestion"; import { DeltaTimeStrategy } from "@ourpay-sh/ingestion/strategies/DeltaTime"; const nowResolver = () => performance.now(); // const nowResolver = () => Number(hrtime.bigint()) // const nowResolver = () => Date.now() // Setup the Delta Time Ingestion Strategy const deltaTimeIngestion = Ingestion({ accessToken: process.env.OURPAY_ACCESS_TOKEN, }) .strategy(new DeltaTimeStrategy(nowResolver)) .ingest("execution-time"); export async function GET(request: Request) { try { // Get the wrapped start clock function // Pass Customer Id to properly annotate the ingestion events with a specific customer const start = deltaTimeIngestion.client({ customerId: request.headers.get("X-OurPay-Customer-Id") ?? "", }); const stop = start(); await sleep(1000); // { deltaTime: xxx } is automatically ingested to OurPay const delta = stop(); return Response.json({ delta }); } catch (error) { return Response.json({ error: error.message }); } } ``` #### Ingestion Payload ```json { "customerId": "123", "name": "execution-time", "metadata": { "deltaTime": 1000 } } ``` # Strategy Introduction Source: https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/ingestion-strategy OurPay offers an ingestion framework to work with OurPay's event ingestion API. Want to report events regarding Large Language Model usage, S3 file uploads or something else? Our Ingestion strategies are customized to make it as seamless as possible to fire ingestion events for complex needs. - [LLM Strategy](/features/usage-based-billing/ingestion-strategies/llm-strategy) - [S3 Strategy](/features/usage-based-billing/ingestion-strategies/s3-strategy) - [Stream Strategy](/features/usage-based-billing/ingestion-strategies/stream-strategy) - [Delta Time Strategy](/features/usage-based-billing/ingestion-strategies/delta-time-strategy) ### Help us improve We're always looking for ways to improve our ingestion strategies. Feel free to contribute — [OurPay Ingestion SDK](https://github.com/sunnycodet/ourpay-ingestion). # LLM Strategy Source: https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/llm-strategy ## Javascript SDK ### LLM Strategy Wrap any LLM model from the `@ai-sdk/*` library, to automatically fire prompt- & completion tokens used by every model call. ``` pnpm add @ourpay-sh/ingestion ai @ai-sdk/openai ``` ```typescript import { Ingestion } from "@ourpay-sh/ingestion"; import { LLMStrategy } from "@ourpay-sh/ingestion/strategies/LLM"; import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; // Setup the LLM Ingestion Strategy const llmIngestion = Ingestion({ accessToken: process.env.OURPAY_ACCESS_TOKEN }) .strategy(new LLMStrategy(openai("gpt-4o"))) .cost((ctx) => ({ amount: 123, currency: "usd" })) // Optional: Set the cost of the LLM usage .ingest("openai-usage"); export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); // Get the wrapped LLM model with ingestion capabilities // Pass Customer Id to properly annotate the ingestion events with a specific customer const model = llmIngestion.client({ customerId: "xxx", }); const { text } = await generateText({ model, system: "You are a helpful assistant.", prompt, }); return Response.json({ text }); } ``` #### Ingestion Payload ```json { "customerId": "123", "name": "openai-usage", "metadata": { "inputTokens": 100, "outputTokens": 200, "cachedInputTokens": 10, "totalTokens": 300, "model": "gpt-4o", "provider": "openai.responses", "strategy": "LLM", "_cost": { "amount": 123, // Amount is expected to be in cents. $1.23 should be represented as 123 "currency": "usd" }, "_llm": { ... // } } } ``` ## Python SDK Our Python SDK includes an ingestion helper and strategies for common use cases. It's installed as part of the OurPay SDK. ```bash pip pip install ourpay-sdk ``` ```bash uv uv add ourpay-sdk ``` ### Ingestion helper The ingestion helper is a simple wrapper around the OurPay events ingestion API. It takes care of batching and sending events to OurPay in the background, without blocking your main thread. ```python import os from ourpay_sdk.ingestion import Ingestion ingestion = Ingestion(os.getenv("OURPAY_ACCESS_TOKEN")) ingestion.ingest({ "name": "my-event", "external_customer_id": "CUSTOMER_ID", "metadata": { "usage": 13.37, } }) ``` ### PydanticAI Strategy [PydanticAI](https://ai.pydantic.dev) is an AI agent framework for Python. A common use-case with AI applications is to track the usage of LLMs, like the number of input and output tokens, and bill the customer accordingly. With our PydanticAI strategy, you can easily track the usage of LLMs and send the data to OurPay for billing. ```python import os from ourpay_sdk.ingestion import Ingestion from ourpay_sdk.ingestion.strategies import PydanticAIStrategy from pydantic import BaseModel from pydantic_ai import Agent ingestion = Ingestion(os.getenv("OURPAY_ACCESS_TOKEN")) strategy = ingestion.strategy(PydanticAIStrategy, "ai_usage") class MyModel(BaseModel): city: str country: str agent = Agent("gpt-4.1-nano", output_type=MyModel) if __name__ == '__main__': result = agent.run_sync("The windy city in the US of A.") print(result.output) strategy.ingest("CUSTOMER_ID", result) ``` _This example is inspired from the [Pydantic Model example](https://ai.pydantic.dev/examples/pydantic-model/) of PydanticAI documentation._ #### Ingestion Payload ```json { "name": "ai_usage", "external_customer_id": "CUSTOMER_ID", "metadata": { "requests": 1, "total_tokens": 78, "request_tokens": 58, "response_tokens": 20 } } ``` # S3 Strategy Source: https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/s3-strategy ## Javascript SDK Wrap the official AWS S3 Client with our S3 Ingestion Strategy to automatically ingest bytes uploaded. ``` pnpm add @ourpay-sh/ingestion @aws-sdk/client-s3 ``` ```typescript import { Ingestion } from "@ourpay-sh/ingestion"; import { S3Strategy } from "@ourpay-sh/ingestion/strategies/S3"; import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; const s3Client = new S3Client({ region: process.env.AWS_REGION, endpoint: process.env.AWS_ENDPOINT_URL, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }); // Setup the S3 Ingestion Strategy const s3Ingestion = Ingestion({ accessToken: process.env.OURPAY_ACCESS_TOKEN }) .strategy(new S3Strategy(s3Client)) .ingest("s3-uploads"); export async function POST(request: Request) { try { // Get the wrapped S3 Client // Pass Customer Id to properly annotate the ingestion events with a specific customer const s3 = s3Ingestion.client({ customerId: request.headers.get("X-OurPay-Customer-Id") ?? "", }); await s3.send( new PutObjectCommand({ Bucket: process.env.AWS_BUCKET_NAME, Key: "a-random-key", Body: JSON.stringify({ name: "John Doe", age: 30, }), ContentType: "application/json", }) ); return Response.json({}); } catch (error) { return Response.json({ error: error.message }); } } ``` #### Ingestion Payload ```json { "customerId": "123", "name": "s3-uploads", "metadata": { "bytes": 100, "bucket": "my-bucket", "key": "my-key", "contentType": "application/text" } } ``` # Stream Strategy Source: https://docs.ourpay.dev/features/usage-based-billing/ingestion-strategies/stream-strategy ## Javascript SDK Wrap any Readable or Writable stream of choice to automatically ingest the bytes consumed. ``` pnpm add @ourpay-sh/ingestion ``` ```typescript import { Ingestion } from '@ourpay-sh/ingestion'; import { StreamStrategy } from '@ourpay-sh/ingestion/strategies/Stream'; const myReadstream = createReadStream(...); // Setup the Stream Ingestion Strategy const streamIngestion = Ingestion({ accessToken: process.env.OURPAY_ACCESS_TOKEN }) .strategy(new StreamStrategy(myReadstream)) .ingest("my-stream"); export async function GET(request: Request) { try { // Get the wrapped stream // Pass Customer Id to properly annotate the ingestion events with a specific customer const stream = streamIngestion.client({ customerId: request.headers.get("X-OurPay-Customer-Id") ?? "" }); // Consume stream... stream.on('data', () => ...) return Response.json({}); } catch (error) { return Response.json({ error: error.message }); } } ``` #### Ingestion Payload ```json { "customerId": "123", "name": "my-stream", "metadata": { "bytes": 100 } } ``` # Introduction Source: https://docs.ourpay.dev/features/usage-based-billing/introduction import Events from "/snippets/usage/events.mdx"; import Meters from "/snippets/usage/meters.mdx"; import MeteredPrice from "/snippets/usage/metered-price.mdx"; import MeterCredits from "/snippets/usage/meter-credits.mdx"; Usage Based Billing is a new feature. We have a lot in store and welcome feedback! ## Overview OurPay has a powerful Usage Based Billing infrastructure that allows you to charge your customers based on the usage of your application. This is done by ingesting events from your application, creating Meters to represent that usage, and then adding metered prices to Products to charge for it. ## Concepts ### Events ### Meters ### Metered Price ### Meter Credits benefit ## Quickstart Get up and running in 5 minutes Meters consist of filters and an aggregation function. The filter is used to filter the events that should be included in the meter and the aggregation function is used to compute the usage. To enable usage based billing for a Product, you need to add a metered price to the Product. Metered prices are only applicable to Subscription Products. Now you're ready to ingest events from your application. Sending events which match the meter's filter will increment the meter's usage for the customer. Customers can view their estimated charges for each meter in the Customer Portal. # Meters Source: https://docs.ourpay.dev/features/usage-based-billing/meters import Meters from "/snippets/usage/meters.mdx"; ## Creating a Meter To create a meter, [open the Meters page](https://ourpay.dev/to/dashboard/products/meters) and click the "Create Meter" button. ## Filters A filter is a set of clauses that are combined using conjunctions. They're used to filter events that you've ingested into OurPay. ### Clauses A clause is a condition that an event must meet to be included in the meter. #### Property Properties are the properties of the event that you want to filter on. If you want to match on a metadata field, you can use the metadata key directly. No need to include a `metadata.` prefix. #### Operator Operators are the operators that you want to use to filter the events. - **Equals** - **Not equals** - **Greater Than** - **Greater Than or Equals** - **Less Than** - **Less Than or Equals** - **Contains** - **Does Not Contain** #### Value Values are automatically parsed in the filter builder. They're parsed in the following order: 1. Number — Tries to parse the value as number 2. Boolean — Checks if value is "true" or "false" 3. String — Treats value as string as fallback ### Conjunctions A conjunction is a logical operator that combines two or more clauses. - **and** — All clauses must be true for the event to be included. - **or** — At least one clause must be true for the event to be included. ## Aggregation The aggregation is the function that is used to aggregate the events that match the filter. For example, if you want to count the number of events that match the filter, you can use the **Count** aggregation. If you want to sum the value of a metadata field, you can use the **Sum** aggregation. - **Count** — Counts the number of events that match the filter. - **Sum** — Sums the value of a property. - **Average** — Computes the average value of a property. - **Minimum** — Computes the minimum value of a property. - **Maximum** — Computes the maximum value of a property. - **Unique** — Counts the number of unique values of a property. Consider the following events: ```json [ { "name": "ai_usage", "external_customer_id": "cus_123", "metadata": { "total_tokens": 10 } }, { "name": "ai_usage", "external_customer_id": "cus_123", "metadata": { "total_tokens": 20 } }, { "name": "ai_usage", "external_customer_id": "cus_123", "metadata": { "total_tokens": 30 } }, { "name": "ai_usage", "external_customer_id": "cus_123", "metadata": { "total_tokens": 30 } } ] ``` Here is the result of each aggregation function, over the `total_tokens` metadata property: - **Count**: 4 units - **Sum**: 90 units - **Average**: 22.5 units - **Minimum**: 10 units - **Maximum**: 30 units - **Unique**: 3 units If you want to use a metadata property in the aggregation, you can use the metadata property directly. No need to include a `metadata.` prefix. ## Unit The unit controls how prices for this meter are **formatted and displayed** to customers — on invoices, in the customer portal, and in your checkout. It does not affect billing calculation; it is purely presentational. | Unit | Display format | Best for | | -------- | -------------------------- | ----------------------------------------- | | Scalar | $0.05 / unit | Generic counts (API calls, events, seats) | | Token | $20.00 / 1M tokens | LLM token consumption | | Custom | Configurable (see below) | Any unit not covered above | ### Custom unit Select **Custom** to define your own display format. Two additional fields appear: - **Unit label** — The singular name shown after the price, e.g. `gigabyte` displays as `$0.023 / gigabyte`. - **Unit multiplier** — Scales the displayed price so you can show a more readable denomination. For example, a multiplier of `1000` shows the price per 1 000 units rather than per single unit. The unit multiplier only affects how the price is shown. The raw `unit_amount` you set is still the price per single event unit — the multiplier scales the display amount for readability. ## Example The following Meter Filter & Aggregation will match events that have the name `openai-usage` and sum units over metadata property `completionTokens`. You can **Preview** the events matched by the meter while creating it. ## Good to know A few things to keep in mind when creating and managing meters: ### Updating a Meter You may update a meter's filters or aggregation function as long as the meter doesn't have any processed events or does not have any customer purchase associated with it. # Integrate OurPay with Encore Source: https://docs.ourpay.dev/guides/encore [Encore](https://encore.dev) is an open source backend framework for TypeScript with built-in infrastructure automation and observability. When you define services, databases, and APIs in code, Encore automatically provisions and manages the underlying infrastructure, both locally and in the cloud. Because Encore defines infrastructure in code and provides built-in distributed tracing, AI coding assistants can both build and debug your backend end-to-end, including your OurPay integration. They can read the code to understand the full system, then inspect traces to diagnose exactly what happened when a checkout completes or a webhook arrives. Consider following this guide while using the OurPay Sandbox Environment. This will allow you to test your integration without affecting your production data. ## Examples - [With Encore](https://github.com/sunnycodet/examples/tree/main/with-encore) ## Install the OurPay JavaScript SDK To get started, install the OurPay JavaScript SDK and the webhook verification library: ```bash Terminal npm install @ourpay-dev/sdk standardwebhooks ``` ## Setting up Secrets Encore has built-in secrets management that works consistently across local development and cloud environments. ### OurPay Access Token and Mode To authenticate with OurPay, you need to create an access token and specify if it's production or sandbox mode. You can create an [organization access token from your organization settings](/integrate/oat). Add it to Encore using the secrets manager: ```bash Terminal encore secret set --type local OURPAY_ACCESS_TOKEN encore secret set --type local OURPAY_MODE ``` ### OurPay Webhook Secret You'll need a webhook secret to verify incoming webhook events. We'll set this up in the webhook section below. ```bash Terminal encore secret set --type local OURPAY_WEBHOOK_SECRET ``` When deploying to Encore Cloud, you'll set production secrets through the dashboard or CI/CD pipeline. ## Configuring a OurPay API Client To interact with the OurPay API, create a new instance of the `OurPay` class using Encore's secret management. ```tsx title="payments/ourpay.ts" import { OurPay } from "@ourpay-dev/sdk"; import { secret } from "encore.dev/config"; const OURPAY_MODE = secret("OURPAY_MODE"); const OURPAY_ACCESS_TOKEN = secret("OURPAY_ACCESS_TOKEN"); export const ourpay = new OurPay({ accessToken: OURPAY_ACCESS_TOKEN(), server: OURPAY_MODE as "sandbox" | "production", }); ``` Remember to set `OURPAY_MODE` as `production` when you're ready to switch to the production environment. ## Generating OurPay Checkout Sessions Create a checkout endpoint that creates a OurPay checkout session and redirects the user. ```tsx title="payments/payments.ts" import { api, APIError } from "encore.dev/api"; import { ourpay } from "./ourpay"; export const checkout = api.raw( { expose: true, path: "/checkout", method: "GET" }, async (req, resp) => { const url = new URL(req.url!, `http://${req.headers.host}`); const productId = url.searchParams.get("product"); if (!productId) { resp.writeHead(400); resp.end("Missing product query parameter"); return; } try { const result = await ourpay.checkouts.create({ productId, successUrl: "https://your-app.com/confirmation?checkout_id={CHECKOUT_ID}", }); resp.writeHead(302, { Location: result.url }); resp.end(); } catch (error) { console.error("[OurPay] Checkout error:", error); resp.writeHead(500); resp.end("Failed to create checkout session"); } } ); ``` You can now create a checkout session by visiting `/checkout?product=YOUR_PRODUCT_ID`. ## Handling OurPay Webhooks OurPay can send you events about various things happening in your organization. This is useful for keeping your database in sync with OurPay checkouts, orders, subscriptions, etc. Configuring a webhook is simple. Head over to your organization's settings page and click on the "Add Endpoint" button to create a new webhook. ### Tunneling webhook events to your local development environment Encore runs your app locally with full infrastructure support. To receive webhooks during local development, you can use the [OurPay CLI](https://docs.ourpay.dev/integrate/webhooks/locally) to tunnel webhook events to your local environment. ```bash Terminal ourpay listen http://localhost:4000/webhooks/ourpay ``` Make sure to copy the webhook secret and set it using: ```bash Terminal encore secret set --type local OURPAY_WEBHOOK_SECRET ``` ### Add Webhook Endpoint 1. Point the Webhook to `your-app.com/webhooks/ourpay`. This must be an absolute URL which OurPay can reach. If you use the OurPay CLI tunnel, it will handle this for you. 2. Select which events you want to be notified about. You can read more about the available events in the [Events section](/api-reference#webhooks). 3. Generate a secret key to sign the requests. This will allow you to verify that the requests are truly coming from OurPay. 4. Add the secret key to Encore: ```bash Terminal encore secret set --type local OURPAY_WEBHOOK_SECRET ``` ### Setting up the Webhook handler Encore webhook handlers use `api.raw()` to access the raw request body, which is required for signature verification. ```tsx title="payments/payments.ts" import { api } from "encore.dev/api"; import { secret } from "encore.dev/config"; import { Webhook } from "standardwebhooks"; const OURPAY_WEBHOOK_SECRET = secret("OURPAY_WEBHOOK_SECRET"); export const webhooks = api.raw( { expose: true, path: "/webhooks/ourpay", method: "POST" }, async (req, resp) => { // Read the raw request body const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } const body = Buffer.concat(chunks).toString("utf-8"); // Convert headers for standardwebhooks const headers: Record = {}; for (const [key, value] of Object.entries(req.headers)) { headers[key] = Array.isArray(value) ? value[0] : (value || ""); } // Verify the webhook signature let payload: any; try { const base64Secret = Buffer.from( OURPAY_WEBHOOK_SECRET().trim(), "utf-8" ).toString("base64"); const wh = new Webhook(base64Secret); payload = wh.verify(body, headers); } catch (error: any) { console.error("[OurPay] Invalid webhook signature:", error?.message); resp.writeHead(403); resp.end(JSON.stringify({ error: "Invalid signature" })); return; } // Handle the payload switch (payload.type) { case "order.created": console.log("[OurPay] Order created:", payload.data.id); break; case "subscription.active": console.log("[OurPay] Subscription active:", payload.data.id); break; case "subscription.canceled": console.log("[OurPay] Subscription canceled:", payload.data.id); break; case "customer.state_changed": console.log("[OurPay] Customer state changed:", payload.data.id); break; default: console.log("[OurPay] Unhandled event:", payload.type); } resp.writeHead(200); resp.end(JSON.stringify({ received: true })); } ); ``` The `standardwebhooks` library expects the secret to be base64-encoded. OurPay provides a raw string (starting with `ourpay_whs_`), so you must base64-encode the entire secret before passing it to `new Webhook()`. ## Local Development Start your Encore app: ```bash Terminal encore run ``` Encore provides a local development dashboard at `localhost:9400` where you can: - View distributed traces for all requests, including checkout redirects and webhook deliveries - Inspect API calls between services - Query local databases - View logs and errors in real-time This makes debugging payment flows straightforward since you can trace exactly what happens when a checkout completes or a webhook arrives. ## Deploying to Production When you're ready to go live, deploy to Encore Cloud or your own cloud account: ```bash Terminal git push encore ``` Encore automatically provisions all the infrastructure your app needs. Remember to: 1. Set production secrets in the Encore Cloud dashboard 2. Update the `server` parameter from `"sandbox"` to `"production"` in your OurPay client 3. Update your webhook URL in OurPay to point to your production domain 4. Update `successUrl` to your production URL # How to Grant Meter Credits After Purchase Source: https://docs.ourpay.dev/guides/grant-meter-credits-after-purchase ## Overview When building usage-based billing systems, you may want to give new customers an initial credit balance (e.g., 10 free units) when they purchase a product. This is useful for: - Offering free trials with credits - Onboarding bonuses - Promotional credits for new signups or purchases There are two ways to grant initial meter credits in OurPay: 1. **Meter Credits Benefit (Recommended)** - Automatically grant credits when a customer purchases a product 2. **Webhook + Events API** - Programmatically grant credits for advanced use cases ### Which Method Should I Use? | Feature | Meter Credits Benefit | Webhook + Events API | |---------|----------------------|---------------------| | **Setup Complexity** | ✅ Simple - No code required | ⚙️ Advanced - Requires coding | | **Automatic Credits** | ✅ Yes | ❌ No - Manual implementation | | **Custom Logic** | ❌ No | ✅ Yes - Full control | | **One-time Products** | ✅ Credits granted at purchase | ✅ Credits granted at purchase | | **Recurring Products** | ❌ No | ✅ Can be credited every cycle (with code) | | **Best For** | Most use cases | Complex crediting rules | **Start with Method 1** (Meter Credits Benefit) unless you need custom logic or complex crediting rules. It's simpler and requires no code. --- ## Method 1: Using Meter Credits Benefit (Recommended) The simplest way to grant initial credits is to use the built-in **Meter Credits** benefit. This automatically grants credits (once) to customers when they purchase a product. ### Step 1: Create a Meter with Sum Aggregation First, [create a meter](/features/usage-based-billing/meters) that will track your customers usage. ### Step 2: Create a Meter Credits Benefit Now [create a Meter Credits benefit](/features/benefits/credits) that will grant the initial credits. ### Step 3: Create a Product with the Benefit Create a product and attach the Meter Credits benefit to it. Open [**Products → New Product**](https://ourpay.dev/to/dashboard/products/new) in the dashboard. - Set a name and description - Choose your product type (Recurring) - Set the price (can be $0 for free signup, or any amount) - Click "Add Additional Price" - Attach your meter to the product and set the per unit cost Scroll to the **Automated Benefits** section and toggle ON the Meter Credits benefit you created. Click **Create Product** to save. That's it! Now when a customer purchases this product, they will automatically receive the specified amount of meter credits. --- ## Method 2: Using Webhooks + Events API (Advanced) For more complex scenarios where you need custom logic or want to grant credits outside the purchase flow, you can use webhooks and the Events API. ### When to Use This Method - You need custom logic to determine credit amounts - You want to grant credits based on external events - You need to grant credits to existing customers programmatically - You want to implement complex crediting rules ### How It Works This approach involves: 1. Creating a product with a meter attached (using sum aggregation) 2. Setting up webhooks to listen for purchases 3. When a purchase is made, ingesting a negative event value to grant credits **Why negative values?** When you ingest an event with a negative value (e.g., `-10`) to a meter using **Sum** aggregation, it effectively grants the customer 10 units of credit, reducing their usage meter balance. ### Prerequisites - A OurPay account with an organization - A meter created with **Sum** aggregation - A product with the meter attached - Webhooks enabled - A OurPay access token for API calls ### Step 1: Create a Meter First, create a meter that will track your customers' usage. Open [**Products → Meters**](https://ourpay.dev/to/dashboard/products/meters) in the dashboard. Click **Create Meter** and configure: - **Name**: Give your meter a descriptive name (e.g., "API Calls" or "Storage Usage") - **Filter**: Add filters to match your usage events (e.g., name equals "api_usage") - **Aggregation**: Select **Sum** and enter the property to sum (e.g., `units`) The meter **must use Sum aggregation** for this approach to work. Save your meter and note down the meter name - you'll need this when ingesting events. Learn more about [creating meters](/features/usage-based-billing/meters). ### Step 2. Create a Product Follow the same steps as Method 1 to create your product (you don't need to create or attach the Meter Credits benefit for this approach). ### Step 3: Set Up Webhooks Configure webhooks to receive notifications when users make purchases. Follow our [Setup Webhooks](/integrate/webhooks/endpoints) guide to create a new webhook endpoint. When configuring your webhook, make sure to subscribe to the `order.paid` event. This event is triggered when a customer successfully completes a purchase. Store your webhook secret securely in your environment variables. ```bash Terminal OURPAY_ACCESS_TOKEN="ourpay_pat_..." OURPAY_WEBHOOK_SECRET="whsec_..." PRODUCT_ID="prod_..." # The product ID to grant credits for ``` ### Step 4: Implement the Webhook Handler Create a webhook handler that listens for `order.paid` events and grants initial credits by ingesting negative event values. #### Next.js Example ```typescript icon="square-js" title="app/api/webhook/ourpay/route.ts" import { OurPay } from "@ourpay-dev/sdk"; import { Webhooks } from "@ourpay-sh/nextjs"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN }); export const POST = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET, onOrderPaid: async (order) => { // Check if this is the product we want to grant credits for const targetProductId = process.env.PRODUCT_ID; if (order.data.product_id === targetProductId) { // Grant 10 credits by ingesting a negative event await ourpay.events.ingest({ events: [ { name: "meter-name", customerId: order.data.customer_id, metadata: { units: -10, // Negative value grants credits reason: "initial_signup_bonus", }, }, ], }); console.log(`Granted 10 credits to customer ${order.data.customer_id}`); } }, }); ``` ### Step 5: Test Your Integration Test that credits are properly granted when a customer makes a purchase. Test your integration in OurPay's [sandbox environment](/integrate/sandbox) to avoid affecting production data. Create a checkout session and complete a test purchase. Check your server logs to confirm the `order.paid` webhook was received. Use the [Customer Meters API](/api-reference/customer_meters/list-customer-meters) to verify the credits were applied: ```typescript const meters = await ourpay.customerMeters.list({ customerId: "cus_...", }); console.log(meters.items[0].balance); // Should show -10 (or your credit amount) ``` ### Important Considerations for Webhook Method #### Negative Balance vs. Positive Usage When using negative events to grant credits: - A **negative balance** (e.g., `-10`) means the customer has 10 credits available - As the customer uses your service, positive events reduce this negative balance - When the balance reaches `0`, the customer has used all their credits - Positive balances indicate usage beyond the granted credits #### Example Flow ```typescript // Initial state: Customer has 0 balance // You grant 10 credits: balance = -10 // Customer uses 3 units: balance = -7 (7 credits remaining) // Customer uses 5 more units: balance = -2 (2 credits remaining) // Customer uses 3 more units: balance = 1 (1 unit of overage, if metered pricing is enabled) ``` #### Preventing Double Credits To avoid granting credits multiple times, consider: 1. **Check order status** - Only grant credits for new orders 2. **Use idempotency** - Track which orders you've already processed 3. **Database records** - Store a record of credit grants ```typescript // Example with idempotency check const hasGrantedCredits = await checkIfAlreadyGranted(order.id); if (!hasGrantedCredits) { await ourpay.events.ingest({ events: [{ name: "meter-name", customerId: order.data.customer_id, metadata: { units: -10 }, }], }); await recordCreditGrant(order.id); } ``` # How to Grant Meter Credits Before Purchase Source: https://docs.ourpay.dev/guides/grant-meter-credits-before-purchase ## Overview You may want to grant usage credits to customers even before they make a purchase. This is useful for: - **Free trial credits** - Give new signups free credits to try your service - **Promotional campaigns** - Grant credits as part of marketing initiatives - **Testing and demos** - Provide credits for product demonstrations This guide shows you how to grant credits to customers who don't have an active subscription or purchase yet. ## How It Works To grant credits before a purchase, you need to: 1. Create a customer in OurPay (if they don't exist) 2. Create a meter with **Sum** aggregation to track usage 3. Ingest an event with a **negative value** to grant credits **Why negative values?** When you ingest an event with a negative value (e.g., `-10`) to a meter using **Sum** aggregation, it grants the customer credits. A negative balance means available credits, which get reduced as they use your service. ## Step 1: Create a Meter First, create a meter that will track your customers' usage. Open [**Products → Meters**](https://ourpay.dev/to/dashboard/products/meters) in the dashboard. Click **Create Meter** and configure: - **Name**: Give your meter a descriptive name (e.g., "API Calls" or "Storage Usage") - **Filter**: Add filters to match your usage events (e.g., name equals "api_usage") - **Aggregation**: Select **Sum** and enter the property to sum (e.g., `units`) The meter **must use Sum aggregation** for this approach to work. Save your meter and note down the meter name - you'll need this when ingesting events. Learn more about [creating meters](/features/usage-based-billing/meters). ## Step 2: Create a Customer If your customer doesn't already exist in OurPay, you need to create them first. ### Option A: Create via Dashboard Open [**Customers**](https://ourpay.dev/to/dashboard/customers) in the dashboard. Click **Add Customer** and fill in: - **Email**: Customer's email address (required) - **Name**: Customer's full name (optional) - **External ID**: Your internal user ID for easy reference (optional but recommended) Click **Save** and note down the Customer ID. ### Option B: Create via API Use the OurPay SDK or API to create a customer programmatically: ```typescript icon="square-js" title="create-customer.ts" import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN, }); // Create a new customer await ourpay.customers.create({ email: "user@example.com", name: "John Doe", externalId: "user_123", // Your internal user ID (optional) }); ``` Use the `externalId` field to link OurPay customers with your internal user system. This allows you to reference customers without storing OurPay's internal ID. ### Using External ID If you set an `externalId` when creating the customer, you can use it in event ingestion instead of the OurPay customer ID: ```typescript // Instead of using customerId, use externalCustomerId await ourpay.events.ingest({ events: [{ name: "api_usage", externalCustomerId: "user_123", // Your internal ID metadata: { units: -10 } }] }); ``` Learn more about [customer management](/features/customer-management). ## Step 3: Grant Credits by Ingesting a Negative Event Now that you have a customer and a meter, grant credits by ingesting an event with a negative value. ### Using the OurPay SDK ```typescript icon="square-js" title="grant-credits.ts" import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN, }); async function grantCredits(customerId: string, credits: number) { await ourpay.events.ingest({ events: [ { customerId, name: "api_usage", // Must match your meter's filter name metadata: { units: -credits, // Negative value grants credits }, }, ], }); console.log(`Granted ${credits} credits to customer ${customerId}`); } // Grant 10 credits to a customer await grantCredits("cus_abc123", 10); ``` ### Using External Customer ID If you're using `externalId` for customer management: ```typescript icon="square-js" title="grant-credits-external.ts" import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN, }); async function grantCreditsToExternalUser(externalUserId: string, credits: number) { await ourpay.events.ingest({ events: [ { name: "api_usage", // Must match your meter's filter name externalCustomerId: externalUserId, // Use your internal ID metadata: { units: -credits, // Negative value grants credits }, }, ], }); console.log(`Granted ${credits} credits to user ${externalUserId}`); } // Grant 10 credits using your internal user ID await grantCreditsToExternalUser("user_123", 10); ``` ## Step 4: Verify Credits Were Granted Check that the credits were successfully granted to the customer. ### Using Customer Meters API ```typescript import { OurPay } from "@ourpay-dev/sdk"; const ourpay = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN, }); // Check customer's meter balance const meters = await ourpay.customerMeters.list({ customerId: "cus_abc123", }); meters.items.filter((balance) => balance > 0).forEach((meter) => { console.log(`${Math.abs(meter.balance)} credits available for ${meter.meter_id}.`); }); ``` ### Example Usage Flow ```typescript // Initial state: Customer created, 0 balance // Grant 10 credits: balance = 10 // Customer uses 3 units await ourpay.events.ingest({ events: [{ name: "api_usage", customerId: "cus_abc123", metadata: { units: 3 } // Positive value for usage }] }); // Balance is now 7 (7 credits remaining) // Customer uses 8 more units await ourpay.events.ingest({ events: [{ name: "api_usage", customerId: "cus_abc123", metadata: { units: 8 } }] }); // Balance is now -1 (used 1 unit beyond credits) ``` # Guides Source: https://docs.ourpay.dev/guides/introduction These guides go beyond what the feature docs cover — full implementation walkthroughs for patterns that need more than a paragraph. ## Available guides - **[Implementing seat-based pricing](/guides/seat-based-pricing)** — sell team products where one customer buys seats and assigns them to members. Covers the Customer / Member / CustomerSeat model, checkout, scaling, and webhooks. - **[Granting meter credits before purchase](/guides/grant-meter-credits-before-purchase)** — give free credits to customers (trials, promotions, demos) by ingesting negative events against a Sum meter. - **[Granting meter credits after purchase](/guides/grant-meter-credits-after-purchase)** — compare the built-in Meter Credits benefit with a webhook-driven approach for custom crediting logic. ## Framework guides Looking to wire OurPay into a specific stack? See the [Next.js](/guides/nextjs), [Laravel](/guides/laravel), and [Encore](/guides/encore) guides under **Integrate**. ## Requesting a new guide Missing something? [Open an issue on sunnycodet/ourpay](https://github.com/sunnycodet/ourpay/issues/new/choose) and tell us what you're trying to build. # Integrate OurPay with Laravel Source: https://docs.ourpay.dev/guides/laravel Consider following this guide while using the OurPay Sandbox Environment. This will allow you to test your integration without affecting your production data. OurPay Laravel Example App -------------------------------- We've created a simple example Laravel application that you can use as a reference. [View Code on GitHub](https://github.com/sunnycodet/ourpay-laravel) Setting up environment variables --------------------------------------- ### OurPay API Key To authenticate with OurPay, you need to create an access token, and supply it to Laravel using a `OURPAY_API_KEY` environment variable. You can create an organization access token from your organization settings. Fetching OurPay Products for display ------------------------------------------ ### Creating the Products Controller Go ahead and add the following entry in your `routes/web.php` file: ```php // routes/web.php Route::get('/products', [ProductsController::class, 'handle']); ``` Next up, create the `ProductsController` class in the `app/Http/Controllers` directory: ```php // app/Http/Controllers/ProductsController.php api.ourpay.workers.dev when ready to go live // And don't forget to update the .env file with the correct OURPAY_ORGANIZATION_ID and OURPAY_WEBHOOK_SECRET $data = Http::get('https://sandbox-api.ourpay.workers.dev/v1/products', [ 'is_archived' => false, ]); $products = $data->json(); return view('products', ['products' => $products['items']]); } } ``` Displaying Products -------------------------- Finally, create the `products` view in the `resources/views` directory: ```php // resources/views/products.blade.php @foreach ($products as $product)

{{ $product['name'] }}

Buy
@endforeach ``` Notice that we create a link to `/checkout` with a query parameter `productId`. This is the ID of the product that the user will be purchasing. We will configure this route in the next section. That's it for the products page. You can now display the products to your users, and they will be able to buy them. Let's now create the checkout endpoint. Generating OurPay Checkout Sessions ----------------------------------------- This endpoint will be responsible for creating a new checkout session, redirecting the user to the OurPay Checkout page & redirect back to a configured confirmation page. Go ahead and create a new entry in your `routes/web.php` file: ```php // routes/web.php Route::get('/checkout', [CheckoutController::class, 'handle']); ``` Next, create the `CheckoutController` class in the `app/Http/Controllers` directory: ```php // app/Http/Controllers/CheckoutController.php query('productId', ''); // OurPay will replace {CHECKOUT_ID} with the actual checkout ID upon a confirmed checkout $confirmationUrl = $request->getSchemeAndHttpHost() . '/confirmation?checkout_id={CHECKOUT_ID}'; // Change from sandbox-api.ourpay.workers.dev -> api.ourpay.workers.dev when ready to go live // And don't forget to update the .env file with the correct OURPAY_ORGANIZATION_ID and OURPAY_WEBHOOK_SECRET $result = Http::withHeaders([ 'Authorization' => 'Bearer ' . env('OURPAY_API_KEY'), 'Content-Type' => 'application/json', ])->post('https://sandbox-api.ourpay.workers.dev/v1/checkouts/custom/', [ 'products' => [$productId], 'success_url' => $confirmationUrl, 'payment_processor' => 'stripe', ]); $data = $result->json(); $checkoutUrl = $data['url']; return redirect($checkoutUrl); } } ``` We can now easily create a checkout session & redirect there by creating a link to `/checkout?productId={productId}`. Just like we did when displaying the products above. Upon Checkout success, the user will be redirected to the confirmation page. Creating the Confirmation Page ------------------------------------- Create a new entry in your `routes/web.php` file: ```php // routes/web.php Route::get('/confirmation', [ConfirmationController::class, 'handle']); ``` Next, create the `ConfirmationController` class in the `app/Http/Controllers` directory: ```php // app/Http/Controllers/ConfirmationController.php api.ourpay.workers.dev when ready to go live // And don't forget to update the .env file with the correct OURPAY_ORGANIZATION_ID and OURPAY_WEBHOOK_SECRET $data = Http::withHeaders([ 'Authorization' => 'Bearer ' . env('OURPAY_API_KEY'), 'Content-Type' => 'application/json', ])->get('https://sandbox-api.ourpay.workers.dev/v1/checkouts/custom/' . $request->query('checkout_id')); $checkout = $data->json(); Log::info(json_encode($checkout, JSON_PRETTY_PRINT)); return view('confirmation', ['checkout' => $checkout]); } } ``` The checkout is not considered "successful" yet however. It's initially marked as `confirmed` until you've received a webhook event `checkout.updated` with a status set to `succeeded`. We'll cover this in the next section. Handling OurPay Webhooks ------------------------------ OurPay can send you events about various things happening in your organization. This is very useful for keeping your database in sync with OurPay checkouts, orders, subscriptions, etc. Configuring a webhook is simple. Head over to your organization's settings page and click on the "Add Endpoint" button to create a new webhook. ### Tunneling webhook events to your local development environment If you're developing locally, you can use a tool like [ngrok](https://ngrok.com/) to tunnel webhook events to your local development environment. This will allow you to test your webhook handlers without deploying them to a live server. Run the following command to start an ngrok tunnel: ```bash Terminal ngrok http 3000 ``` ### Add Webhook Endpoint 1. Point the Webhook to `your-app.com/api/webhook/ourpay`. This must be an absolute URL which OurPay can reach. If you use ngrok, the URL will look something like this: `https://.ngrok-free.app/api/webhook/ourpay`. 2. Select which events you want to be notified about. You can read more about the available events in the [Events section](/api-reference#webhooks). 3. Generate a secret key to sign the requests. This will allow you to verify that the requests are truly coming from OurPay. 4. Add the secret key to your environment variables. ```bash Terminal # .env OURPAY_API_KEY="ourpay_oat..." OURPAY_WEBHOOK_SECRET="..." ``` ### Setting up the Webhook handler First, we need to install the standard-webhooks package to properly decode the incoming webhook payloads. ```bash Terminal composer require standard-webhooks/standard-webhooks:dev-main ``` Go and add a `routes/api.php` file and add the following entry: ```php // routes/api.php Route::webhooks('/webhook/ourpay'); ``` Make sure that it is included in the Bootstrap file. ```php // bootstrap/app.php withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { // })->create(); ``` We will use Spatie's Webhook Client to handle the webhook events. It will automatically verify the signature of the requests, and dispatch the payload to a job queue for processing. ```bash Terminal composer require spatie/laravel-webhook-client ``` Let's publish the config: ```bash Terminal php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config" ``` This will create a new file called webhook-client.php in the config folder. We need to adjust it to properly verify the signature of the requests. ```php // config/webhook-client.php [ [ /* * This package supports multiple webhook receiving endpoints. If you only have * one endpoint receiving webhooks, you can use 'default'. */ 'name' => 'default', /* * We expect that every webhook call will be signed using a secret. This secret * is used to verify that the payload has not been tampered with. */ 'signing_secret' => env('OURPAY_WEBHOOK_SECRET'), /* * The name of the header containing the signature. */ 'signature_header_name' => 'webhook-signature', /* * This class will verify that the content of the signature header is valid. * * It should implement \Spatie\WebhookClient\SignatureValidator\SignatureValidator */ // 'signature_validator' => \Spatie\WebhookClient\SignatureValidator\DefaultSignatureValidator::class, 'signature_validator' => App\Handler\OurPaySignature::class, /* * This class determines if the webhook call should be stored and processed. */ 'webhook_profile' => \Spatie\WebhookClient\WebhookProfile\ProcessEverythingWebhookProfile::class, /* * This class determines the response on a valid webhook call. */ 'webhook_response' => \Spatie\WebhookClient\WebhookResponse\DefaultRespondsTo::class, /* * The classname of the model to be used to store webhook calls. The class should * be equal or extend Spatie\WebhookClient\Models\WebhookCall. */ 'webhook_model' => \Spatie\WebhookClient\Models\WebhookCall::class, /* * In this array, you can pass the headers that should be stored on * the webhook call model when a webhook comes in. * * To store all headers, set this value to `*`. */ 'store_headers' => [], /* * The class name of the job that will process the webhook request. * * This should be set to a class that extends \Spatie\WebhookClient\Jobs\ProcessWebhookJob. */ 'process_webhook_job' => App\Handler\ProcessWebhook::class, ], ], /* * The integer amount of days after which models should be deleted. * * 7 deletes all records after 1 week. Set to null if no models should be deleted. */ 'delete_after_days' => 30, ]; ``` ### Preparing the database By default, all webhook calls get saved into the database. So, we need to publish the migration that will hold the records. So run: ```bash Terminal php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations" ``` This will create a new migration file in the “database/migration” folder. Then run `php artisan migrate` to run the migration. ### Setting up the queue system Before we set up our job handler — let’s set up our queue system Go to your “.env” file and set the QUEUE\_CONNECTION=database — you can decide to use other connections like redis. Let’s create our jobs table by running php artisan queue:table and then run the migration using php artisan migrate. ### Create the Handlers The next thing we do is to create a folder named Handler inside the app folder. Then inside this app/Handler, create two files which are * OurPaySignature.php * ProcessWebhook.php Inside app/Handler/OurPaySignature.php, what we want to do is to validate that the request came from OurPay. Add the code to that file. ```php // app/Handler/OurPaySignature.php signingSecret); $wh = new \StandardWebhooks\Webhook($signingSecret); return boolval( $wh->verify($request->getContent(), array( "webhook-id" => $request->header("webhook-id"), "webhook-signature" => $request->header("webhook-signature"), "webhook-timestamp" => $request->header("webhook-timestamp"), ))); } } ``` Great. So the other file app/Handler/ProcessWebhook.php extends the ProcessWebhookJob class which holds the WebhookCall variables containing each job’s detail. ```php // app/Handler/ProcessWebhook.php webhookCall, true); $data = $decoded['payload']; switch ($data['type']) { case "checkout.created": // Handle the checkout created event break; case "checkout.updated": // Handle the checkout updated event break; case "subscription.created": // Handle the subscription created event break; case "subscription.updated": // Handle the subscription updated event break; case "subscription.active": // Handle the subscription active event break; case "subscription.revoked": // Handle the subscription revoked event break; case "subscription.canceled": // Handle the subscription canceled event break; default: // Handle unknown event Log::info($data['type']); break; } //Acknowledge you received the response http_response_code(200); } } ``` Our application is ready to receive webhook requests. Don’t forget to run `php artisan queue:listen` to process the jobs. ### Tips If you're keeping track of active and inactive subscriptions in your database, make sure to handle the `subscription.active` and `subscription.revoked` events accordingly. The cancellation of a subscription is handled by the `subscription.canceled` event. The user has probably canceled their subscription before the end of the billing period. Do not revoke any kind of access immediately, but rather wait until the end of the billing period or when you receive the `subscription.revoked` event. Notifying the client about the event ------------------------------------------- If you're building a real-time application, you might want to notify the client about the event. On the confirmation-page, you can listen for the `checkout.updated` event and update the UI accordingly when it reaches the succeeded status. OurPay Laravel Example App -------------------------------- We've created a simple example Laravel application that you can use as a reference [View Code on GitHub](https://github.com/sunnycodet/ourpay-laravel) # Migrate Seat-Based Pricing to the Member Model Source: https://docs.ourpay.dev/guides/migrate-seat-based-to-member-model If you sell seat-based products on OurPay, your organization will be migrated to the **member model**. This guide explains what changes, what breaks, and exactly how to update your code. ## What the member model introduces The member model introduces a separation between **who pays** and **who uses** through three entities: - A **Customer** is the billing entity (who pays). They own subscriptions, orders, and payment methods. After migration, customers with seat-based products are upgraded to `type: "team"`. - A **Member** is a person under a customer (who uses). Each member has their own email and role (`owner`, `billing_manager`, or `member`), and receives benefit grants independently. - A **CustomerSeat** is the link between a subscription and a member. It tracks assignment status and carries metadata. Today, each seat holder is represented as their own `Customer`. After migration, seat holders become `Member` records under the purchasing customer, and `CustomerSeat` links the subscription to each member. For a full overview of how these entities work together, see the [Seat-Based Pricing guide](/guides/seat-based-pricing). ## How the migration works: two phases The migration happens in **two phases** so you can update your integration before the breaking change hits. ### Phase 1: Preparation (non-breaking) In this phase, OurPay creates `Member` records for all your existing seat holders and populates `member_id` on seats and benefit grants. **Nothing breaks** because `customer_id` on seats still points to the seat holder, and no customers are deleted. This gives you time to start reading the new `member` and `member_id` fields and update your code before the breaking change. ### Phase 2: Member model enabled (breaking) In this phase, the member model is fully activated. `customer_id` on seats and grants **flips** to point to the billing customer (the buyer), old seat-holder customer records are deleted, and all new operations use the member model. **This is the breaking change.** If your code reads `customer_id` to identify seat holders, it will now get the buyer instead. ## A real-world example Imagine you sell a **Team Pro** plan. Jane (the manager) buys 3 seats and assigns them to Alice and Bob. ### Before migration (current state) ``` Jane purchases Team Pro (3 seats) └── Subscription: sub_456 (customer_id: cust_jane) Seats: seat_1 → customer_id: cust_alice ← Alice is her own Customer seat_2 → customer_id: cust_bob ← Bob is his own Customer seat_3 → unassigned Benefit grants: grant for Alice → customer_id: cust_alice grant for Bob → customer_id: cust_bob ``` Your code probably does something like this to grant access: ```typescript // Your current webhook handler if (event.type === 'benefit_grant.created') { const userId = event.data.customer_id; // "cust_alice", the seat holder await grantAccessToApp(userId); } ``` This works because `customer_id` on the grant points to Alice directly. ### After Phase 1: Preparation (non-breaking) Members are created, `member_id` is populated on seats and grants, but `customer_id` still points to the seat holder. Your existing code **continues to work**. ``` Jane's Customer record (cust_jane) ├── Member: mem_jane (role: owner) ← new ├── Member: mem_alice (role: member) ← new └── Member: mem_bob (role: member) ← new Seats: seat_1 → customer_id: cust_alice, member_id: mem_alice ← customer_id unchanged seat_2 → customer_id: cust_bob, member_id: mem_bob ← customer_id unchanged seat_3 → unassigned Benefit grants: grant for Alice → customer_id: cust_alice, member_id: mem_alice ← customer_id unchanged grant for Bob → customer_id: cust_bob, member_id: mem_bob ← customer_id unchanged ``` **What you can do now:** Start reading `seat.member` and `grant.member` in your code. Both the old `customer_id` and the new `member` fields are available, so you can update your integration at your own pace. ```typescript // During Phase 1, both fields work if (event.type === 'benefit_grant.created') { const grant = event.data; // Old way still works: grant.customer_id; // "cust_alice", still the seat holder // New way also works: grant.member.id; // "mem_alice" grant.member.email; // "alice@company.com" } ``` ### After Phase 2: Member model enabled (breaking) `customer_id` flips to the billing customer. Old seat-holder customers are deleted. ``` Jane's Customer record (cust_jane, type: "team") ├── Member: mem_jane (role: owner) ├── Member: mem_alice (role: member) └── Member: mem_bob (role: member) cust_alice → deleted (404) cust_bob → deleted (404) Seats: seat_1 → customer_id: cust_jane, member_id: mem_alice ← customer_id changed! seat_2 → customer_id: cust_jane, member_id: mem_bob ← customer_id changed! seat_3 → unassigned Benefit grants: grant for Alice → customer_id: cust_jane, member_id: mem_alice ← customer_id changed! grant for Bob → customer_id: cust_jane, member_id: mem_bob ← customer_id changed! ``` Now **every** `customer_id` is `cust_jane`, the buyer. Your old webhook handler would grant access to Jane instead of Alice and Bob. ```typescript // ❌ Broken after Phase 2 because customer_id is now the buyer if (event.type === 'benefit_grant.created') { const userId = event.data.customer_id; // "cust_jane", NOT Alice! await grantAccessToApp(userId); } // ✅ Works in both phases using member to identify the actual user if (event.type === 'benefit_grant.created') { const grant = event.data; const user = grant.member; await grantAccessToApp(user.id, user.email); // mem_alice, "alice@company.com" } ``` ## Breaking changes at a glance These changes happen in **Phase 2** only: | What changed | Phase 1 | Phase 2 | Fix | |---|---|---|---| | `seat.customer_id` | Still the seat holder | The buyer | Use `seat.member` or `seat.email` | | `grant.customer_id` | Still the grant recipient | The buyer | Use `grant.member` | | Old seat-holder customer IDs | Still valid | `404` | Map to `member_id` via seat list | ## How to update your code Update your code during Phase 1 so everything works when Phase 2 activates. The `member` field is available in both phases. ### 1. Seats: identify holders by member, not customer_id Calling `GET /v1/customer-seats/{seat_id}` or listing seats for a subscription returns: **Phase 1** (non-breaking, `customer_id` still points to Alice): ```json { "id": "seat_1", "subscription_id": "sub_456", "customer_id": "cust_alice", "member": { "id": "mem_alice", "email": "alice@company.com", "customer_id": "cust_jane", "role": "member" }, "email": "alice@company.com", "customer_email": "alice@company.com" } ``` **Phase 2** (breaking, `customer_id` flipped to Jane): ```json { "id": "seat_1", "subscription_id": "sub_456", "customer_id": "cust_jane", "member": { "id": "mem_alice", "email": "alice@company.com", "customer_id": "cust_jane", "role": "member" }, "email": "alice@company.com", "customer_email": "alice@company.com" } ``` - Use `seat.member` or `seat.email` to identify who holds the seat. This works in both phases. - `seat.customer_email` still resolves to the holder's email, so it is safe to keep using. ### 2. Benefit grants: use grant.member for the recipient Calling `GET /v1/benefit-grants/{grant_id}` or receiving a `benefit_grant.created` webhook returns: **Phase 1:** ```json { "id": "grant_1", "customer_id": "cust_alice", "member": { "id": "mem_alice", "email": "alice@company.com", "customer_id": "cust_jane", "role": "member" }, "benefit_id": "ben_789" } ``` **Phase 2:** ```json { "id": "grant_1", "customer_id": "cust_jane", "member": { "id": "mem_alice", "email": "alice@company.com", "customer_id": "cust_jane", "role": "member" }, "benefit_id": "ben_789" } ``` - Use `grant.member` to know who received the benefit. This is consistent across both phases. - License keys and downloads tied to this grant also transfer in Phase 2. ### 3. Replace stored seat-holder customer IDs If you stored `cust_alice` or `cust_bob` in your database to track who has access, those IDs will return `404` after Phase 2. Use the seat list endpoint to map old references to new member IDs during Phase 1: ```typescript // List all seats for a subscription to get the new member IDs const seats = await ourpay.customerSeats.list({ subscription_id: "sub_456" }); for (const seat of seats.result.items) { if (seat.member) { // Map: seat.member.email → seat.member.id await updateYourDatabase(seat.member.email, seat.member.id); } } ``` ### 4. Update webhook handlers **Seat webhooks:** ```typescript // Before if (event.type === 'customer_seat.claimed') { const holderId = event.data.customer_id; // ❌ Will be the buyer after Phase 2 await grantAccess(holderId); } // After (works in both phases) if (event.type === 'customer_seat.claimed') { const holder = event.data.member; // ✅ Always the seat occupant await grantAccess(holder.id, holder.email); } ``` **Benefit grant webhooks:** ```typescript if (event.type === 'benefit_grant.created') { const grant = event.data; if (grant.member) { // Seat-based grant: member is the end user await grantAccess(grant.member.id, grant.member.email); } else { // Direct purchase: customer is the end user await grantAccess(grant.customer_id); } } ``` **New member lifecycle events** are now available if you want to track when team members are added or removed: - `member.created` is emitted when a member joins the team (via seat assignment or customer creation) - `member.updated` is emitted when a member's details change - `member.deleted` is emitted when a member is removed ### 5. Seat assignment Existing assignment calls using `email`, `customer_id`, or `external_customer_id` continue to work. OurPay resolves them to a member internally. However, the **response changes** between phases: **Phase 1:** `seat.customer_id` in the response still points to the seat holder. ```typescript const seat = await ourpay.customerSeats.assign({ subscription_id: "sub_456", email: "charlie@company.com" }); seat.customer_id; // "cust_charlie" (the seat holder) seat.member.email; // "charlie@company.com" seat.member.id; // "mem_charlie" ``` **Phase 2:** `seat.customer_id` in the response points to the buyer, not the seat holder. ```typescript const seat = await ourpay.customerSeats.assign({ subscription_id: "sub_456", email: "charlie@company.com" }); seat.customer_id; // "cust_jane" (the buyer, NOT charlie) seat.member.email; // "charlie@company.com" seat.member.id; // "mem_charlie" ``` If your code reads `customer_id` from the assignment response, switch to `seat.member` instead. You can also assign by member ID directly: ```typescript const seat = await ourpay.customerSeats.assign({ subscription_id: "sub_456", member_id: "mem_charlie" }); ``` Do not mix legacy identifiers (`customer_id`, `external_customer_id`) with member identifiers (`member_id`, `external_member_id`) in the same request. ### 6. Pass member_id when creating customer sessions After migration, pass a `member_id` when creating customer sessions to scope the portal view to a specific member: ```typescript // Before: customer session with no member context const session = await ourpay.customerSessions.create({ customer_id: "cust_jane" }); // After: pass member_id to scope the session const session = await ourpay.customerSessions.create({ customer_id: "cust_jane", member_id: "mem_alice" }); // Redirect to session.customer_portal_url ``` You can also use `external_member_id` as an alternative to `member_id`: ```typescript const session = await ourpay.customerSessions.create({ customer_id: "cust_jane", external_member_id: "your_user_id_alice" }); ``` - **Owners and billing managers** see full team management, including assigning seats, managing members, and adjusting seat count. - **Regular members** see only their own benefits and account details. For team customers, `member_id` is required. For individual customers, it is optional. When omitted, the owner member is used automatically. ## Migration checklist **During Phase 1 (do this now):** - [ ] Update seat-handling code to use `seat.member` / `seat.email` instead of `seat.customer_id` - [ ] Update benefit grant code to use `grant.member` instead of `grant.customer_id` - [ ] Replace any stored seat-holder customer IDs with member IDs - [ ] Update webhook handlers to read `member` from seat and grant payloads - [ ] Pass `member_id` when calling `customerSessions.create()` for portal access **Optional enhancements:** - [ ] Use `member_id` or `external_member_id` in seat assignment calls - [ ] Subscribe to `member.created`, `member.updated`, `member.deleted` webhooks # Integrate OurPay with Next.js Source: https://docs.ourpay.dev/guides/nextjs Feel free to use our quick-start script to get started inside a new Next.js project: ```bash Terminal # Inside a new Next.js project npx ourpay-init ``` Consider following this guide while using the OurPay Sandbox Environment. This will allow you to test your integration without affecting your production data. [A complete code-example of this guide can be found on GitHub](https://github.com/sunnycodet/ourpay-next). ## Install the OurPay JavaScript SDK To get started, you need to install the OurPay JavaScript SDK and the OurPay Nextjs helper package. You can do this by running the following command: ```bash Terminal pnpm install @ourpay-dev/sdk @ourpay-sh/nextjs ``` ## Setting up environment variables ### OurPay Access Token To authenticate with OurPay, you need to create an access token, and supply it to Next.js using a `OURPAY_ACCESS_TOKEN` environment variable. You can create an organization access token from your organization settings. ## Configuring a OurPay API Client To interact with the OurPay API, you need to create a new instance of the `OurPay` class. This class uses the provided access token to authenticate with the OurPay API. ```typescript // src/ourpay.ts import { OurPay } from "@ourpay-dev/sdk"; export const api = new OurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, server: "sandbox", // Use this option if you're using the sandbox environment - else use 'production' or omit the parameter }); ``` Remember to replace `sandbox` with `production` when you're ready to switch to the production environment. ## Generating OurPay Checkout Sessions Next up, we need to create a checkout endpoint to handle the creation of checkout sessions. Go ahead and create a new GET route in Next.js. ```typescript // src/app/checkout/route.ts import { Checkout } from "@ourpay-sh/nextjs"; export const GET = Checkout({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, successUrl: "https://your-app.com/confirmation?checkout_id={CHECKOUT_ID}", server: "sandbox", // Use this option if you're using the sandbox environment - else use 'production' or omit the parameter }); ``` ## Handling OurPay Webhooks OurPay can send you events about various things happening in your organization. This is very useful for keeping your database in sync with OurPay checkouts, orders, subscriptions, etc. Configuring a webhook is simple. Head over to your organization's settings page and click on the "Add Endpoint" button to create a new webhook. ### Tunneling webhook events to your local development environment If you're developing locally, you can use a tool like [ngrok](https://ngrok.com/) to tunnel webhook events to your local development environment. This will allow you to test your webhook handlers without deploying them to a live server. Run the following command to start an ngrok tunnel: ```bash Terminal ngrok http 3000 ``` ### Add Webhook Endpoint 1. Point the Webhook to `your-app.com/api/webhook/ourpay`. This must be an absolute URL which OurPay can reach. If you use ngrok, the URL will look something like this: `https://.ngrok-free.app/api/webhook/ourpay`. 2. Select which events you want to be notified about. You can read more about the available events in the [Events section](/api-reference#webhooks). 3. Generate a secret key to sign the requests. This will allow you to verify that the requests are truly coming from OurPay. 4. Add the secret key to your environment variables. ```bash Terminal # .env OURPAY_ACCESS_TOKEN="ourpay_pat..." OURPAY_WEBHOOK_SECRET="..." ``` ### Setting up the Webhook handler ```typescript // src/app/api/webhook/ourpay/route.ts import { Webhooks } from "@ourpay-sh/nextjs"; export const POST = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET, onPayload: async (payload) => // Handle payload... }); ``` The webhook event is now verified and you can proceed to handle the payload data. ### Handling Webhook Events Depending on which events you've subscribed to, you'll receive different payloads. This is where you can update your database, send notifications, etc. ```typescript // src/app/api/webhook/ourpay/route.ts import { Webhooks } from "@ourpay-sh/nextjs"; export const POST = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET, onPayload: async (payload) => ..., onOrderCreated: async (order) => ..., onCustomerStateChanged: async (customerState) => ..., ... }); ``` ### Notifying the client about the event If you're building a real-time application, you might want to notify the client about the event. On the confirmation-page, you can listen for the `checkout.updated` event and update the UI accordingly when it reaches the succeeded status. # Implementing Seat-Based Pricing Source: https://docs.ourpay.dev/guides/seat-based-pricing Seat-based pricing lets you sell products where one person buys seats and assigns them to their team. Each seat holder gets their own benefits — license keys, Discord roles, file downloads, or anything else you offer. ## Understanding the model Before writing any code, there are three entities you need to understand. They change how you think about every API call, webhook, and portal flow. ### Customer, Member, and CustomerSeat With standard OurPay products, one person buys and one person uses — they're the same person. With seat-based products, buying and using are separate concerns, modeled through three distinct entities: - A **Customer** is the billing entity — who pays. They own subscriptions, orders, and payment methods. On first seat-based purchase, the customer is permanently upgraded to `type: "team"`, which enables members and team management. - A **Member** is a person under a customer — who uses. Each member has their own email, role (`owner`, `billing_manager`, or `member`), and receives benefit grants independently. The person who purchases the product is created as an `owner` member. Both `owner` and `billing_manager` roles can manage seats, update or cancel the subscription, and manage payment methods. The `owner` role may receive additional management capabilities in the future. - A **CustomerSeat** is the link between a product and a member. It tracks assignment status (`pending`, `claimed`, `revoked`), holds the invitation token, and carries optional metadata. ``` Customer (Jane — purchaser, type: "team") ├── Member: Jane (role: owner) → manages team, can self-assign a seat for benefits ├── Member: Alice (role: member) → gets benefits via seat └── Member: Bob (role: member) → gets benefits via seat Subscription (Team Pro — 3 seats) ├── CustomerSeat → Alice (claimed) → benefits granted ├── CustomerSeat → Bob (claimed) → benefits granted └── CustomerSeat → unassigned → available ``` ### Why this separation matters This three-entity model gives you flexibility that a simple "buyer = user" model can't: - **Members persist across products.** The same member can hold seats from different subscriptions or orders under the same customer. - **Roles control portal access.** Owners and billing managers see full team management. Regular members see only their own benefits. - **Benefits track to people, not purchases.** Always use `grant.member` to identify who has access — `grant.customer_id` is always the billing entity, not the end user. ### What this means for your integration - **Benefit grants** reference a `member`, not just a `customer`. Always use `grant.member` to identify who received the benefit. - **Webhooks** include a `member` object on seat and grant events. Use it instead of `customer_id` to identify the end user. - **Benefits are not granted at purchase time.** Seats must be assigned and claimed before benefits are granted. (Exception: when using the default OurPay confirmation page, the buyer's seat is auto-claimed.) ``` Purchase → Assign seats → Members claim → Benefits granted ``` ## Prerequisites - OurPay organization with seat-based pricing enabled - OurPay SDK installed (`npm install @ourpay-dev/sdk` or `pip install ourpay-sdk`) — make sure it's the latest version - Basic understanding of OurPay products and subscriptions ## Implementation This guide covers both **subscription** and **one-time purchase** seat-based products. The flow is the same — the only difference is in scaling and billing. ### Step 1: Create a seat-based product Open the dashboard and [create a new product](https://ourpay.dev/to/dashboard/products/new). Under **Pricing**: - **Product type**: Subscription (recurring) or One-time (perpetual licenses) - **Pricing type**: Seat-based - **Tiering model**: Choose how seats are priced (see below) **Fixed price per seat** (default) — every seat costs the same flat rate. Enter a single price per seat. **Graduated** — seats are priced per tier range independently. Each tier's seats are billed at that tier's rate and the totals are summed. Example with 1–10 seats at \$10 and 11+ at \$8: buying 14 seats = 10 × \$10 + 4 × \$8 = \$132. **Volume discounts** — the per-seat price is determined by the total seats purchased, and that rate applies to all seats. Example with the same tiers: buying 14 seats = 14 × \$8 = \$112 (all seats at the 11+ rate). | Tier | Max Seats | Price per Seat | |------|-----------|----------------| | 1 | 4 | $10/month | | 2 | 9 | $9/month | | 3 | Unlimited | $8/month | Configure benefits that seat holders will receive (license keys, file downloads, Discord roles, etc.). Remember: benefits are granted on seat claim, not at purchase. ### Step 2: Checkout ```typescript const checkout = await ourpay.checkouts.create({ products: ["prod_123"], seats: 5, success_url: "https://your-app.com/success", customer_email: "billing@company.com" }); // Redirect to checkout.url ``` The checkout automatically calculates pricing based on your tiers. ### Step 3: Seat management After purchase, the billing manager can assign and manage seats directly from the **Customer Portal** — no custom UI required. The portal provides: - **Assign seats** to team members by email - **Revoke seats** to remove access and free up seats for reassignment - **Resend invitations** for pending seats - **Adjust seat count** on subscriptions (add or reduce) OurPay automatically sends invitation emails to assigned team members with a secure claim link. Once a member claims their seat, benefits are granted immediately. Invitation emails are sent automatically when seats are assigned. The claim flow is fully managed by OurPay — members click the link, claim their seat, and get immediate access to benefits. #### API-driven seat assignment If you want to manage seat assignment programmatically (e.g., auto-assigning seats when users sign up), use the [Customer Seats API](/api-reference/customer-seats/assign-seat): ```typescript const seat = await ourpay.customerSeats.assign({ subscription_id: subscriptionId, email: "engineer@company.com", immediate_claim: true // Skip invitation email, grant benefits immediately }); ``` Use `immediate_claim: true` when you manage your own user authentication and want to bypass the email invitation flow. Benefits are granted immediately without the member needing to click a claim link. ### Step 4: Handle benefit grants Listen for benefit webhooks to sync access in your system. Remember: use `grant.member` to identify the recipient, not `grant.customer_id` (which is the buyer). Always [verify webhook signatures](/integrate/webhooks/endpoints#verify-signature) before processing events. The example below omits verification for brevity. ```typescript app.post('/webhooks/ourpay', async (req, res) => { // Verify webhook signature first — see webhook docs const event = req.body; if (event.type === 'benefit_grant.created') { const grant = event.data; const recipient = grant.member; await grantAccess(recipient.id, recipient.email, grant.benefit); } if (event.type === 'benefit_grant.revoked') { const grant = event.data; await revokeAccess(grant.member.id, grant.benefit); } res.sendStatus(200); }); ``` ### Step 5: Scale seats **Subscriptions** — modify the seat count: ```typescript await ourpay.subscriptions.update({ id: subscriptionId, seats: newTotal // Cannot be less than currently assigned seats (pending + claimed) }); ``` You cannot reduce seats below the number of currently assigned seats (both pending and claimed). Revoke seats first before reducing the count. **One-time purchases** — buy a new order: ```typescript const checkout = await ourpay.checkouts.create({ products: [productId], seats: additionalSeats, success_url: "https://your-app.com/success" }); // Each order has its own independent seat pool ``` ## Member sessions and portal To give a member access to the customer portal, create a customer session with a `member_id` to scope the view: ```typescript const session = await ourpay.customerSessions.create({ customer_id: "cust_123", member_id: "mem_789" }); // Redirect to session.customer_portal_url ``` - **Billing managers** (owner/billing_manager role) see full team management — assigning seats, managing members, and viewing seat utilization. - **Members** see only their own benefits and account details. ## Webhook events | Event | When | Key fields | |---|---|---| | `order.paid` | Customer completes purchase | `customer_id`, `product` | | `subscription.updated` | Seat count changes | `customer_id`, `seats` | | `customer_seat.assigned` | Seat assigned, invitation sent | `member`, `email`, `status: "pending"` | | `customer_seat.claimed` | Member claims their seat | `member`, `status: "claimed"` | | `benefit_grant.created` | Benefit granted to member | `member`, `customer_id` (buyer) | | `customer_seat.revoked` | Seat revoked | `member`, `status: "revoked"` | | `benefit_grant.revoked` | Benefit removed from member | `member`, `customer_id` (buyer) | On subscription cancellation, each seat is revoked individually. A subscription with 5 seats and 3 benefits produces 1 + 5 + 15 = 21 webhook events. Make sure your handlers are idempotent. ## Best practices - **Use `grant.member` everywhere** — not `grant.customer_id` — to identify who has access - **Use seat metadata** to store department, role, or cost center for your own tracking - **Communicate clearly** to billing managers about seat assignment: if using the default OurPay confirmation page, their seat is auto-claimed; if using a custom success URL, they'll need to assign themselves a seat through the portal ## Troubleshooting | Problem | Solution | |---|---| | Cannot reduce seats | Revoke assigned seats first. You can't go below the combined pending + claimed count. | | Claim link expired | Tokens expire after 24 hours. Resend via the portal or API: `ourpay.customerSeats.resend({ seat_id })` | ## Subscriptions vs one-time purchases | | Subscriptions | One-Time Purchases | |---|---|---| | **Payment** | Recurring (monthly/yearly) | Single payment | | **Seat duration** | While subscribed | Perpetual | | **Adding seats** | Modify subscription | Purchase new order | | **Benefits** | While subscription active | Forever after claim | ## Limitations - Seats must be assigned individually (use API for bulk) - Claim links expire after 24 hours - Maximum 1,000 seats per subscription - Metadata limited to 10 keys and 1KB per seat ## Next steps - [Seat-Based Pricing Feature Documentation](/features/seat-based-pricing) - [Customer Seats API Reference](/api-reference/customer-seats/assign-seat) - [Webhook setup](/integrate/webhooks/endpoints) - [Customer Portal customization](/features/customer-portal) # Authentication Source: https://docs.ourpay.dev/integrate/authentication All bearer tokens should be kept private and never shared or exposed in client-side code. To authenticate requests, OurPay API has two mechanisms. 1. [Organization Access Tokens (OAT)](/integrate/oat) - Recommended 2. [OAuth 2.0 Provider](/integrate/oauth2/introduction) (Partner Integrations) ## Organization Access Tokens (OAT) They are tied to **one** of your organization. You can create them from your organization settings. ## Security To protect your data and ensure the security of OurPay, we've several mechanisms in place to automatically revoke tokens that may have been leaked publicly on the web. In particular, we're part of the [GitHub Secret Scanning Program](https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning). If GitHub systems detect a OurPay token in a code repository or public discussion, our systems are notified and the tokens are immediately revoked. If you received an email about one of your token being leaked, it means that we were notified of such situation. The email contains the details about the nature of the token and the source of the leak. In the future, it's crucial that you remain extra cautious about not leaking your tokens publicly online. You can read more about the good practices to manage secrets in the [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html). # Customer State Source: https://docs.ourpay.dev/integrate/customer-state Customer State is a concept allowing you to query for the current state of a customer, including their active subscriptions and granted [benefits](/features/benefits/introduction), in a single [API call](/api-reference/customers/get-customer-state-by-external-id) or single [webhook event](/api-reference/customer_state_changed). Combined with the [External ID](/features/customer-management#external-id) feature, you can get up-and-running in minutes. ## The customer state object The customer state object contains: - All the data about the customer. - The list of their **active** subscriptions. - The list of their **granted** benefits. - The list of their **active** meters, with their current balance. Thus, with that single object, you have all the required information to check if you should provision access to your service or not. One endpoint to rule them all, using your own customer ID. The same one, but with internal OurPay customer ID. ## The `customer.state_changed` webhook To be notified of the customer state changes, you can listen to the `customer.state_changed` webhook event. It's triggered when: - Customer is created, updated or deleted. - A subscription is created or updated. - A benefit is granted or revoked. By subscribing to this webhook event, you keep your system up-to-date and update your customer's access accordingly. One webhook to rule them all. # OurPay over Model Context Protocol (MCP) Source: https://docs.ourpay.dev/integrate/mcp Supercharge your AI agents with OurPay as a Model Context Protocol (MCP) server. ## What is MCP? MCP is a protocol for integrating tools with AI agents. It can greatly enhance the capabilities of your AI agents by providing them with real-time data and context. OurPay offers a remote MCP server that you can connect to from most AI clients. ## How does it work? You need a MCP-capable agent environment to use OurPay over MCP. A few of them are Claude Desktop and Cursor. ## Connecting to OurPay MCP OurPay provides two MCP servers: - **Production**: `https://mcp.ourpay.workers.dev/mcp/ourpay-mcp` - Connect to your live OurPay organization - **Sandbox**: `https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox` - Connect to the OurPay sandbox environment for testing When you can specify a MCP URL, use one of the URLs above depending on your environment. If you have to specify a command, use: ```json { "mcpServers": { "OurPay": { "command": "npx", "args": ["mcp-remote", "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp"] } } } ``` For sandbox: ```json { "mcpServers": { "OurPay Sandbox": { "command": "npx", "args": ["mcp-remote", "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox"] } } } ``` ### Cursor In `.cursor/mcp.json`, add: ```json { "mcpServers": { "OurPay": { "url": "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp" } } } ``` For sandbox: ```json { "mcpServers": { "OurPay Sandbox": { "url": "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox" } } } ``` ### Windsurf In `mcp_config.json`, add: ```json { "mcpServers": { "OurPay": { "command": "npx", "args": ["mcp-remote", "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp"] } } } ``` For sandbox: ```json { "mcpServers": { "OurPay Sandbox": { "command": "npx", "args": ["mcp-remote", "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox"] } } } ``` ### Codex Add the following to your `~/.codex/config.toml`: ```toml [features] rmcp_client = true [mcp_servers.ourpay] type = "http" url = "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp" ``` Then run: ```sh codex mcp login ourpay ``` For sandbox: ```toml [features] rmcp_client = true [mcp_servers.ourpay_sandbox] type = "http" url = "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox" ``` Then run: ```sh codex mcp login ourpay_sandbox ``` ### Claude Code Run the following command: ``` claude mcp add --transport http "OurPay" "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp" ``` For sandbox: ``` claude mcp add --transport http "OurPay-Sandbox" "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox" ``` ### OpenCode In `opencode.jsonc`, add: ```jsonc { "$schema": "https://opencode.ai/config.json", "mcp": { "OurPay": { "type": "remote", "url": "https://mcp.ourpay.workers.dev/mcp/ourpay-mcp" } } } ``` For sandbox: ```jsonc { "$schema": "https://opencode.ai/config.json", "mcp": { "OurPay Sandbox": { "type": "remote", "url": "https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox" } } } ``` ### ChatGPT MCP is only available for paid users in beta on ChatGPT web, by enabling Developer Mode. Once Developer Mode is enabled, go to _Settings_ → _Connectors_ and add the MCP server using `https://mcp.ourpay.workers.dev/mcp/ourpay-mcp`. For sandbox, use `https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox` instead. ### Claude Desktop Go to _Settings_ → _Connectors_ and click _Add custom connector_. Name it "OurPay" and add `https://mcp.ourpay.workers.dev/mcp/ourpay-mcp` as the server URL. For sandbox, use `https://mcp.ourpay.workers.dev/mcp/ourpay-sandbox` as the server URL instead. Save, and click _Connect_ to connect to your OurPay organization. # Organization Access Tokens Source: https://docs.ourpay.dev/integrate/oat All bearer tokens should be kept private and never shared or exposed in client-side code. Organization Access Tokens (OAT) are tied to **one** of your organization. You can create them from your organization settings as follows: Open [**Settings**](https://ourpay.dev/to/dashboard/settings) for your organization in the OurPay dashboard. Scroll down to **Developers** and click **New Token** - **Give your token a name**: Choose a descriptive name so you remember what this token is used for. - **Set an expiration date**: Decide how long the token should be valid. - **Select the required scopes**: Pick the permissions your token needs. # OAuth 2.0 Connect Source: https://docs.ourpay.dev/integrate/oauth2/connect ## Authorize To start the authorization flow you need to redirect the user to the authorization URL. It looks like this: ``` https://ourpay.dev/oauth2/authorize? response_type=code &client_id=CLIENT_ID &redirect_uri=https%3A%2F%2Fexample.com%2Fcallback &scope=openid%20email ``` The parameters are the one described in the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). The most important ones are: Indicates that you want to use the authorization code flow. Most common and the only one supported by OurPay. The Client ID you got when creating the OAuth 2.0 client. The URL where the user will be redirected after granting access to their data. Make sure you declared it when creating the OAuth2 client. A space-separated list of scopes you want to ask for. Make sure they are part of the scopes you declared when creating the OAuth2 client. If you redirect the user to this URL, they'll see a page asking them to grant access to their data, corresponding to the scopes you asked for. OurPay issues **user-scoped** access tokens, tied to the user granting access. On the consent screen, the user can optionally **limit the token to specific organizations**: by default it can access all of their organizations (including ones they join later), but if they select specific ones, it's restricted to those. Access always intersects with the user's current membership, so it adjusts automatically if they leave an organization. You can check which organizations a token is limited to by [introspecting it](/api-reference/oauth2/introspect-token) — the response includes an `organizations` array (an empty array means all of them). If the user grants access, they'll be redirected to your `redirect_uri` with a `code` parameter in the query string. This code is a one-time code that you can exchange for an access token. #### Exchange code token Once you have the authorization code, you can exchange it for an access token. To do so, you'll need to make a `POST` request to the token endpoint. This call needs to be authenticated with the Client ID and Client Secret you got when creating the OAuth2 client. Here is an example with cURL: ```bash Terminal curl -X POST https://api.ourpay.dev/v1/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=authorization_code&code=AUTHORIZATION_CODE&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&redirect_uri=https://example.com/callback' ``` You should get the following response: ```json { "token_type": "Bearer", "access_token": "ourpay_at_XXX", "expires_in": 864000, "refresh_token": "ourpay_rt_XXX", "scope": "openid email", "id_token": "ID_TOKEN" } ``` The `access_token` will allow you to make authenticated API requests on behalf of the user. The `refresh_token` is a long-lived token that you can use to get new access tokens when the current one expires. The `id_token` is a signed JWT token containing information about the user, as per the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html#IDToken). #### Public Clients Public clients are clients where the Client Secret can't be kept safe, as it would be accessible by the final user. This is the case for SPA, mobile applications, or any client running on the user's device. In this case, **and only if the client is configured as a Public Client**, the request to the token endpoint won't require the `client_secret` parameter. However, the [PKCE](https://oauth.net/2/pkce/) method will be required to maximize security. ### Make authenticated requests Once you have an access token, either from a Personal Access Token or from the OpenID Connect flow, you can make authenticated requests to the API. Here is a simple example with cURL: ```bash Terminal curl -X GET https://api.ourpay.dev/v1/oauth2/userinfo \ -H 'Authorization: Bearer ourpay_at_XXX' ``` # Introduction Source: https://docs.ourpay.dev/integrate/oauth2/introduction ### OpenID Connect (OAuth2) Only use our **OpenID Connect** in case you want to act on the behalf of other users via our API, e.g building an app/service for OurPay customers. Otherwise, always use an **Organization Access Token (OAT)** to integrate OurPay for your own service. OurPay implements the [OpenID Connect specification](https://openid.net/developers/how-connect-works/) to enable third-party authentication. It's a layer on top of the OAuth2 framework aiming at making integration more standard and predictable. In particular, it comes with a **discovery endpoint** allowing compatible clients to automatically work with the OpenID Connect server. Here is OurPay's one: [OpenID Configuration](https://api.ourpay.dev/.well-known/openid-configuration) # Create an OAuth 2.0 Client Source: https://docs.ourpay.dev/integrate/oauth2/setup Before being able to make authentication requests, you'll need an **OAuth2 Client**. It's the entity that'll identify you, as a third-party developer, between OurPay and the final user. You can manage them from your [User Settings](https://ourpay.dev/settings#oauth) Here are the required fields: * *Application Name*: the name of the application that'll be shown to the final users. * *Client Type*: the type of client you are creating. [Read more](#public-clients) * *Redirect URIs*: for security reasons, you need to declare your application URL where the users will be redirected after granting access to their data. When configuring your OAuth client, you must use an `https://` URL for security reasons. We block `http://` URLs, except when the hostname is `localhost`. This exception allows you to use `http://localhost` for convenient testing in development mode. * *Scopes*: the list of scopes your app will be able to ask for. To improve privacy and security, select only the scopes you really need for your application. * *Homepage URL*: the URL of your application. It'll be shown to the final users on the authorization page. Optionally, you can also add a **terms of service** and **privacy policy** URL. They'll be shown to the final users on the authorization page. Once your client is created, you'll get a **Client ID** and a **Client Secret**. You'll need those values to make authentication requests. Those values are super sensitive and should be kept secret. They allow making authentication requests on OurPay! # Sandbox Environment Source: https://docs.ourpay.dev/integrate/sandbox A separate environment, isolated from your production data To test OurPay or work on your integration without worrying about actual money processing or breaking your live organization, you can use our [sandbox environment](https://sandbox.ourpay.workers.dev/start). It's a dedicated server, completely isolated from the production instance where you can do all the experiments you want. **Why a dedicated environment instead of a test mode?** Since we're dealing with money and need to keep track of all movements to assure our Merchant of Record service, we found it safer to isolate live data from test data so it never interferes. Besides, it allows you to create an unlimited number of account and organization to test lot of different scenarios. Consider it as your own development server! ## Get started You can access the sandbox environment directly on [sandbox.ourpay.workers.dev](https://sandbox.ourpay.workers.dev/start) or by clicking on `Go to sandbox` from the organization switcher. You'll then need to create a dedicated user account and organization, the same way described in our [Quick Start guide](/introduction). ### Testing payments The sandbox environment allows you to experience the complete customer funnel, including checkout. You can perform test payments using Stripe's [test card numbers](https://docs.stripe.com/testing#cards). The easiest one to test a successful payment is to use the following card number with a future expiration date and random CVC: ``` 4242 4242 4242 4242 ``` ## API and SDK To make requests to our [API](/api-reference), all you need to do is to switch the base URL from `https://api.ourpay.dev` to `https://sandbox-api.ourpay.workers.dev`. You'll also need to create an access token in the **sandbox environment**, the access token created in the production environment can't be used in the sandbox. Our official SDKs support the sandbox environment through a dedicated parameter. ```ts TypeScript import { createOurPay } from "@ourpay-dev/sdk/2026-10"; const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, environment: "sandbox", }); ``` ```py Python import os from ourpay.v2026_10 import OurPay ourpay = OurPay( os.environ["OURPAY_ACCESS_TOKEN"], environment="sandbox", ) ``` ## Limitations The limitations listed below only apply to sandbox and doesn't reflect the behavior in production. * Customer-facing emails (order confirmations, subscription renewal reminders, etc.) are only delivered to recipients who are members of your organization. Manage these in [**Settings → Members**](https://ourpay.dev/to/dashboard/settings/members). Sub-addressing aliases like `you+test@example.com` are accepted. # Astro Source: https://docs.ourpay.dev/integrate/sdk/adapters/astro ## Examples - [With Astro](https://github.com/sunnycodet/examples/tree/main/with-astro) - [With Astro and Cloudflare Workers](https://github.com/sunnycodet/examples/tree/main/with-astro-cloudflare-workers) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/astro ``` ```bash Terminal yarn add zod @ourpay-sh/astro ``` ```bash Terminal pnpm add zod @ourpay-sh/astro ``` ```bash Terminal bun add zod @ourpay-sh/astro ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" import { Checkout } from "@ourpay-sh/astro"; import { OURPAY_ACCESS_TOKEN, OURPAY_SUCCESS_URL } from "astro:env/server"; export const GET = Checkout({ accessToken: OURPAY_ACCESS_TOKEN, successUrl: OURPAY_SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" import { CustomerPortal } from "@ourpay-sh/astro"; import { OURPAY_ACCESS_TOKEN } from "astro:env/server"; export const GET = CustomerPortal({ accessToken: OURPAY_ACCESS_TOKEN, getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" import { Webhooks } from '@ourpay-sh/astro'; import { OURPAY_WEBHOOK_SECRET } from "astro:env/server" export const POST = Webhooks({ webhookSecret: OURPAY_WEBHOOK_SECRET, onPayload: async (payload) => /** Handle payload */, }) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # BetterAuth Source: https://docs.ourpay.dev/integrate/sdk/adapters/better-auth ## @ourpay-sh/better-auth A [Better Auth](https://github.com/better-auth/better-auth) plugin for integrating [OurPay](https://ourpay.dev) payments and subscriptions into your authentication flow. ### Features - [Automatic Customer creation on signup](#automatic-customer-creation-on-signup) - [Sync customer deletion](#sync-customer-deletion) - [Reference System to associate purchases with organizations](#3-2-orders) - [Checkout Integration](#checkout-plugin) - [Event Ingestion & Customer Meters for flexible Usage Based Billing](#usage-plugin) - [Handle OurPay Webhooks securely with signature verification](#webhooks-plugin) - [Customer Portal](#portal-plugin) ## Examples - [With Next.js, Better Auth and Cloudflare Workers](https://github.com/sunnycodet/examples/tree/main/with-nextjs-better-auth-cloudflare-workers) ## Installation Install the required Better Auth and OurPay packages using the following command: ```bash Terminal npm install better-auth @ourpay-sh/better-auth @ourpay-dev/sdk ``` ```bash Terminal yarn add better-auth @ourpay-sh/better-auth @ourpay-dev/sdk ``` ```bash Terminal pnpm add better-auth @ourpay-sh/better-auth @ourpay-dev/sdk ``` ```bash Terminal bun add better-auth @ourpay-sh/better-auth @ourpay-dev/sdk ``` ## Integrate OurPay with BetterAuth Go to your OurPay Organization Settings, create an Organization Access Token, and add it to the environment variables of your application. ```bash .env OURPAY_ACCESS_TOKEN=... ``` The OurPay plugin comes with it's own set of plugins to add functionality to your stack: - **checkout** - Enable seamless checkout integration - **portal** - Make it possible for your customers to manage their orders, subscriptions & benefits - **usage** - List customer meters & ingest events for Usage Based Billing - **webhooks** - Listen for relevant OurPay webhooks ```typescript icon="square-js" auth.ts import { betterAuth } from "better-auth"; import { ourpay, checkout, portal, usage, webhooks } from "@ourpay-sh/better-auth"; // [!code ++] import { OurPay } from "@ourpay-dev/sdk"; // [!code ++] const ourpayClient = new OurPay({ // [!code ++] accessToken: process.env.OURPAY_ACCESS_TOKEN, // [!code ++] // Use 'sandbox' if you're using the OurPay Sandbox environment // Remember that access tokens, products, etc. are completely separated between environments. // Access tokens obtained in Production are for instance not usable in the Sandbox environment. server: 'sandbox' // [!code ++] }); // [!code ++] const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ // [!code ++] client: ourpayClient, // [!code ++] createCustomerOnSignUp: true, // [!code ++] use: [ // [!code ++] checkout({ // [!code ++] products: [ // [!code ++] { // [!code ++] productId: "123-456-789", // ID of Product from OurPay Dashboard // [!code ++] slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro // [!code ++] } // [!code ++] ], // [!code ++] successUrl: "/success?checkout_id={CHECKOUT_ID}", // [!code ++] authenticatedUsersOnly: true // [!code ++] }), // [!code ++] portal(), // [!code ++] usage(), // [!code ++] webhooks({ // [!code ++] secret: process.env.OURPAY_WEBHOOK_SECRET, // [!code ++] onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes // [!code ++] onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.) // [!code ++] ... // Over 25 granular webhook handlers // [!code ++] onPayload: (payload) => // Catch-all for all events // [!code ++] }) // [!code ++] ], // [!code ++] }) // [!code ++] ] }); ``` #### OurPay Plugin Configuration Options ```typescript // ... const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ client: ourpayClient, // [!code ++] createCustomerOnSignUp: true, // [!code ++] getCustomerCreateParams: ({ user }, request) => ({ // [!code ++] metadata: { // [!code ++] myCustomProperty: 123, // [!code ++] }, // [!code ++] }), // [!code ++] use: [ // [!code ++] // This is where you add OurPay plugins // [!code ++] ], // [!code ++] }), ], }); ``` - `client` (required): OurPay SDK client instance - `createCustomerOnSignUp` (optional): Automatically create a OurPay customer when a user signs up - `getCustomerCreateParams` (optional): Custom function to provide additional customer creation metadata - `use` (optional): Array of OurPay plugins to enable specific functionality (checkout, portal, usage, and webhooks) You will be using the BetterAuth Client to interact with the OurPay functionalities. ```typescript icon="square-js" auth-client.ts import { createAuthClient } from "better-auth/react"; import { ourpayClient } from "@ourpay-sh/better-auth/client"; // [!code ++] import { organizationClient } from "better-auth/client/plugins"; // [!code ++] // All OurPay plugins, etc. should be attached to BetterAuth server export const authClient = createAuthClient({ // [!code ++] plugins: [ourpayClient()], // [!code ++] }); // [!code ++] ``` ## Automatic Customer creation on signup Enable the `createCustomerOnSignUp` [OurPay plugin configuration option](#ourpay-plugin-configuration-options) to automatically create a new OurPay Customer when a new User is added in the BetterAuth database. All new customers are created with an associated `externalId`, i.e. the ID of your User in the Database. This skips any OurPay to User mapping in your database. ## Sync Customer deletion To add user deletion logic with external OurPay customer deletion in BetterAuth, extend the user config of your betterAuth setup with the deleteUser option and an afterDelete hook. Here’s how to integrate this alongside your OurPay plugin and automatic customer creation: ```typescript icon="square-js" Customer Deletion Example const auth = betterAuth({ user: { // [!code ++] deleteUser: { // [!code ++] enabled: true, // [!code ++] afterDelete: async (user, request) => { // [!code ++] await ourpay.customers.deleteExternal({ // [!code ++] externalId: user.id, // [!code ++] }); // [!code ++] }, // [!code ++] }, // [!code ++] }, // [!code ++] }); ``` ## Checkout Plugin [Source code](https://github.com/sunnycodet/ourpay-adapters/blob/main/packages/ourpay-betterauth/src/plugins/checkout.ts) To support [checkouts](/features/checkout/links) in your app, you would pass the `checkout` plugin in the `use` property. The checkout plugin accepts the following configuration options: - **`products`** (optional): An array of product mappings or a function that returns them asynchronously. Each mapping contains a `productId` and a `slug` that allows you to reference products by a friendly slug instead of their full ID. - **`successUrl`** (optional): The relative path or absolute URL where customers will be redirected after a successful checkout completion. You can use the `{CHECKOUT_ID}` placeholder in the URL to include the checkout session ID in the redirect. - **`returnUrl`** (optional): An optional URL which renders a back-button in the Checkout. - **`authenticatedUsersOnly`** (optional): A boolean flag that controls whether checkout sessions require user authentication. When set to `true`, only authenticated users can initiate checkouts and the customer information will be automatically associated with the authenticated user. When `false`, anonymous checkouts are allowed. - **`theme`** (optional): A string that can be used to enforce the theme of the checkout page. Can be either `light` or `dark`. Update the `use` property of the OurPay plugin for BetterAuth client to have the `checkout` plugin. ```typescript icon="square-js" Checkout Plugin Example import { ourpay, checkout // [!code ++] } from "@ourpay-sh/better-auth"; const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ ... use: [ checkout({ // [!code ++] // Optional field - will make it possible to pass a slug to checkout instead of Product ID products: [ { productId: "123-456-789", slug: "pro" } ], // [!code ++] // Relative path or absolute URL to redirect to when checkout is successfully completed successUrl: "/success?checkout_id={CHECKOUT_ID}", // [!code ++] // Whether you want to allow unauthenticated checkout sessions or not authenticatedUsersOnly: true, // [!code ++] // An optional URL which renders a back-button in the Checkout returnUrl: "https://myapp.com" // [!code ++] }) // [!code ++] ], }) ] }); ``` When the `checkout` plugin is passed, you are then able to initialize Checkout Sessions using the `checkout` method on the BetterAuth client. This will redirect the user to the product's checkout link. The `checkout` method accepts the following properties: - **`products`** (optional): An array of OurPay Product IDs. - **`slug`** (optional): A string that can be used as a reference to the `products` defined in the Checkout config - **`referenceId`** (optional): An identifier that will be saved in the metadata of the checkout, order & subscription object ```typescript icon="square-js" BetterAuth Checkout with OurPay Example await authClient.checkout({ // OurPay Product IDs products: ["e651f46d-ac20-4f26-b769-ad088b123df2"], // [!code ++] // OR // if "products" in passed in the checkout plugin's config, you may pass the slug // slug: "pro", // [!code ++] }); ``` This plugin supports the Organization plugin. If you pass the organization ID to the Checkout referenceId, you will be able to keep track of purchases made from organization members. ```typescript icon="square-js" BetterAuth Checkout with OurPay Organization Example const organizationId = (await authClient.organization.list())?.data?.[0]?.id, await authClient.checkout({ // Any OurPay Product ID can be passed here products: ["e651f46d-ac20-4f26-b769-ad088b123df2"], // Or, if you setup "products" in the Checkout Config, you can pass the slug slug: 'pro', // Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object referenceId: organizationId }); ``` ## Usage Plugin [Source code](https://github.com/sunnycodet/ourpay-adapters/blob/main/packages/ourpay-betterauth/src/plugins/usage.ts) A plugin for Usage Based Billing that allows you to [ingest events](#event-ingestion) from your application and list the [authenticated user's Usage Meter](#customer-meters). To enable [usage based billing](/integrate/sdk/adapters/better-auth) in your app, you would pass the `usage` plugin in the `use` property. ```typescript icon="square-js" Usage Plugin Example import { ourpay, checkout, portal, usage // [!code ++] } from "@ourpay-sh/better-auth"; const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ ... use: [ checkout(...), portal(), usage() // [!code ++] ], }) ] }); ``` ### 1. Event Ingestion OurPay's Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it. **Ingest events from your server, not from the browser.** Events drive billing, so the client must never be trusted to decide what is consumed. Always ingest from the same server-side handler that performs the metered action (e.g. the route that generated the AI video, processed the upload, etc.) so that the recorded usage cannot be forged or bypassed. Because `createCustomerOnSignUp: true` sets the OurPay customer's `externalId` to the BetterAuth user ID, you can pass `externalCustomerId: session.user.id` when ingesting — no extra lookup is needed. The example below uses the OurPay SDK directly from a server route handler. The same `ourpayClient` instance you configured in `auth.ts` is reused: ```typescript icon="square-js" app/api/ai/video/route.ts (Next.js App Router) import { auth } from "@/lib/auth"; import { ourpayClient } from "@/lib/ourpay"; import { headers } from "next/headers"; export async function POST(request: Request) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) { return new Response("Unauthorized", { status: 401 }); } // Run the metered work on the server const { video, tokensConsumed } = await makeNewVideo(request); // Ingest the resulting usage against the authenticated user await ourpayClient.events.ingest({ events: [ { name: "ai-video", externalCustomerId: session.user.id, metadata: { tokensConsumed, }, }, ], }); return Response.json({ video }); } ``` The `events.ingest` method accepts: - `name` (string): The name of the event to ingest. For example, `ai_usage`, `video_streamed` or `file_uploaded`. - `externalCustomerId` (string): The BetterAuth user ID. Pass `session.user.id` from the server-side session. - `metadata` (object): A record of key-value pairs that describe the event. Values can be strings, numbers, or booleans. Use this to store information that can be filtered on or used to compute usage — duration, token count, file size, etc. #### Client-side ingestion endpoint The `usage` plugin also registers a BetterAuth endpoint that is callable from the client as `authClient.usage.ingestion(...)`. It forwards to OurPay from the BetterAuth server and attaches the authenticated user automatically: ```typescript icon="square-js" auth-client.ts const { data: ingested } = await authClient.usage.ingestion({ event: "file-uploads", metadata: { uploadedFiles: 12, }, }); ``` This endpoint trusts whatever the client sends it, so a user can call it from their browser and claim any usage they want. Only use it for events that genuinely originate on the client and that you are comfortable not being authoritative (e.g. analytics-like signals). For billable usage, stick to server-side ingestion as shown above. ### 2. Customer Meters A method to list the authenticated user's Usage Meters (aka Customer Meters). A Customer Meter contains all the information about their consumption on your defined meters. The `meters` method of the `usage` plugin accepts the following parameters: - `page` (number): The page number for pagination (starts from 1). - `limit` (number): The maximum number of meters to return per page. ```typescript icon="square-js" Customer Meters with Usage Plugin Example const { data: customerMeters } = await authClient.usage.meters.list({ query: { page: 1, limit: 10, }, }); ``` The `meters` method returns the following fields in the response object: - **Customer Information**: Details about the authenticated customer - **Meter Information**: Configuration and settings of the usage meter - **Customer Meter Information**: - **Consumed Units**: Total units consumed by the customer - **Credited Units**: Total units credited to the customer - **Balance**: The balance of the meter, i.e. the difference between credited and consumed units. ## Webhooks Plugin [Source code](https://github.com/sunnycodet/ourpay-adapters/blob/main/packages/ourpay-betterauth/src/plugins/webhooks.ts) The `webhooks` plugin can be used to capture incoming events from your OurPay organization. To set up the OurPay `webhooks` plugin with the BetterAuth client, follow the steps below: Configure a Webhook endpoint in your OurPay Organization Settings page by following [this guide](/integrate/webhooks/endpoints). Webhook endpoint is configured at `/api/auth/ourpay/webhooks`. Add the obtained webhook secret to your application environment as an environment variable (to be used as `process.env.OURPAY_WEBHOOK_SECRET`): ```bash .env OURPAY_WEBHOOK_SECRET="..." ``` Pass the `webhooks` plugin in the `use` property. ```typescript icon="square-js" Webhooks Plugin Example import { ourpay, webhooks // [!code ++] } from "@ourpay-sh/better-auth"; const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ ... use: [ webhooks({ // [!code ++] secret: process.env.OURPAY_WEBHOOK_SECRET, // [!code ++] onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes // [!code ++] onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.) // [!code ++] ... // Over 25 granular webhook handlers // [!code ++] onPayload: (payload) => // Catch-all for all events // [!code ++] }) // [!code ++] ], }) ] }); ``` The `webhooks` plugin allows you to invoke handlers for all OurPay webhook events: - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes ## Portal Plugin [Source code](https://github.com/sunnycodet/ourpay-adapters/blob/main/packages/ourpay-betterauth/src/plugins/portal.ts) A plugin which enables customer management of their purchases, orders and subscriptions. ```typescript icon="square-js" Portal Plugin Example import { ourpay, checkout, portal // [!code ++] } from "@ourpay-sh/better-auth"; const auth = betterAuth({ // ... Better Auth config plugins: [ ourpay({ ... use: [ checkout(...), portal({ returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal }) // [!code ++] ], }) ] }); ``` The `portal` plugin gives the BetterAuth Client a set of customer management methods, scoped under `authClient.customer` object. ### 1. Customer Portal Management The following method will redirect the user to the OurPay Customer Portal, where they can see their orders, purchases, subscriptions, benefits, etc. ```typescript icon="square-js" Open Customer Portal Example await authClient.customer.portal(); ``` ### 2. Customer State The portal plugin also adds a convenient method to retrieve the Customer State. ```typescript icon="square-js" Retrieve Customer State Example const { data: customerState } = await authClient.customer.state(); ``` The customer state object contains: - All the data about the customer. - The list of their active subscriptions. This does not include subscriptions done by a parent organization. See the subscription list-method below for more information. - The list of their granted benefits. - The list of their active meters, with their current balance. Using the customer state object, you can determine whether to provision access for the user to your service. Learn more about the OurPay Customer State [in the OurPay Docs](https://docs.ourpay.dev/integrate/customer-state). ### 3. Benefits, Orders & Subscriptions The portal plugin adds the following 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer. #### 3.1 Benefits This method only lists granted benefits for the authenticated user/customer. ```typescript icon="square-js" List User Benefits Example const { data: benefits } = await authClient.customer.benefits.list({ query: { page: 1, limit: 10, }, }); ``` #### 3.2 Orders This method lists orders like purchases and subscription renewals for the authenticated user/customer. ```typescript icon="square-js" List User Orders Example const { data: orders } = await authClient.customer.orders.list({ query: { page: 1, limit: 10, productBillingType: "one_time", // or 'recurring' }, }); ``` Using the Organization ID as the `referenceId` you can retrieve all the subscriptions associated with that organization (instead of the user). To figure out if a user should have access, pass the user's organization ID to see if there is an active subscription for that organization. ```typescript icon="square-js" List Organization Subscriptions Example const organizationId = (await authClient.organization.list())?.data?.[0]?.id, const { data: subscriptions } = await authClient.customer.orders.list({ query: { page: 1, limit: 10, active: true, referenceId: organizationId }, }); const userShouldHaveAccess = subscriptions.some( sub => // Your logic to check subscription product or whatever. ) ``` #### 3.3 Subscriptions This method lists the subscriptions associated with authenticated user/customer. ```typescript icon="square-js" List User Subscriptions Example const { data: subscriptions } = await authClient.customer.subscriptions.list({ query: { page: 1, limit: 10, active: true, }, }); ``` This will not return subscriptions made by a parent organization to the authenticated user. # Deno Source: https://docs.ourpay.dev/integrate/sdk/adapters/deno ## Examples - [With Deno](https://github.com/sunnycodet/examples/tree/main/with-deno) ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" import { Checkout } from "jsr:@ourpay-sh/deno"; Deno.serve( Checkout({ accessToken: "xxx", returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }) ); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" import { CustomerPortal } from "jsr:@ourpay-sh/deno"; Deno.serve( CustomerPortal({ accessToken: "xxx", getCustomerId: (req) => "", returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", }) ); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" import { Webhooks } from "jsr:@ourpay-sh/deno"; Deno.serve( Webhooks({ webhookSecret: Deno.env.get('OURPAY_WEBHOOK_SECRET'), onPayload: async (payload) => /** Handle payload */, }) ); ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Elysia Source: https://docs.ourpay.dev/integrate/sdk/adapters/elysia ## Examples - [With Elysia](https://github.com/sunnycodet/examples/tree/main/with-elysia) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/elysia ``` ```bash Terminal yarn add zod @ourpay-sh/elysia ``` ```bash Terminal pnpm add zod @ourpay-sh/elysia ``` ```bash Terminal bun add zod @ourpay-sh/elysia ``` ### Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" import { Elysia } from "elysia"; import { Checkout } from "@ourpay-sh/elysia"; const app = new Elysia(); app.get( "/checkout", Checkout({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }) ); ``` #### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ### Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" import { Elysia } from "elysia"; import { CustomerPortal } from "@ourpay-sh/elysia"; const app = new Elysia(); app.get( "/portal", CustomerPortal({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }) ); ``` ### Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" import { Elysia } from 'elysia' import { Webhooks } from "@ourpay-sh/elysia"; const app = new Elysia() app.post('/ourpay/webhooks', Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => /** Handle payload */, })) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Express Source: https://docs.ourpay.dev/integrate/sdk/adapters/express ## Examples - [With Express v4](https://github.com/sunnycodet/examples/tree/main/with-express-v4) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/express ``` ```bash Terminal yarn add zod @ourpay-sh/express ``` ```bash Terminal pnpm add zod @ourpay-sh/express ``` ```bash Terminal bun add zod @ourpay-sh/express ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript import express from "express"; import { Checkout } from "@ourpay-sh/express"; const app = express(); app.get( "/checkout", Checkout({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }) ); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript import express from "express"; import { CustomerPortal } from "@ourpay-sh/express"; const app = express(); app.get( "/portal", CustomerPortal({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }) ); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript import express from 'express' import { Webhooks } from "@ourpay-sh/express"; const app = express() app .use(express.json()) .post('/ourpay/webhooks', Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => /** Handle payload */, })) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Fastify Source: https://docs.ourpay.dev/integrate/sdk/adapters/fastify ## Examples - [With Fastify](https://github.com/sunnycodet/examples/tree/main/with-fastify) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/fastify ``` ```bash Terminal yarn add zod @ourpay-sh/fastify ``` ```bash Terminal pnpm add zod @ourpay-sh/fastify ``` ```bash Terminal bun add zod @ourpay-sh/fastify ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript import fastify from "fastify"; import { Checkout } from "@ourpay-sh/fastify"; fastify().get( "/checkout", Checkout({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }) ); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript import fastify from "fastify"; import { CustomerPortal } from "@ourpay-sh/fastify"; fastify().get( "/portal", CustomerPortal({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }) ); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript import fastify from 'fastify' import { Webhooks } from "@ourpay-sh/fastify"; fastify.post('/ourpay/webhooks', Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => /** Handle payload */, })) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Hono Source: https://docs.ourpay.dev/integrate/sdk/adapters/hono ## Examples - [With Hono and Cloudflare Workers](https://github.com/sunnycodet/examples/tree/main/with-hono-cloudflare-workers) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/hono ``` ```bash Terminal yarn add zod @ourpay-sh/hono ``` ```bash Terminal pnpm add zod @ourpay-sh/hono ``` ```bash Terminal bun add zod @ourpay-sh/hono ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript import { Hono } from "hono"; import { Checkout } from "@ourpay-sh/hono"; const app = new Hono(); app.get( "/checkout", Checkout({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }) ); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript import { Hono } from "hono"; import { CustomerPortal } from "@ourpay-sh/hono"; const app = new Hono(); app.get( "/portal", CustomerPortal({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }) ); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript import { Hono } from 'hono' import { Webhooks } from "@ourpay-sh/hono"; const app = new Hono() app.post('/ourpay/webhooks', Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => /** Handle payload */, })) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Laravel Source: https://docs.ourpay.dev/integrate/sdk/adapters/laravel Seamlessly integrate OURPAY subscriptions and payments into your Laravel application. This package provides an elegant way to handle subscriptions, manage recurring payments, and interact with OURPAY's API. With built-in support for webhooks, subscription management, and a fluent API, you can focus on building your application while we handle the complexities of subscription billing. This provider is not maintained or officially supported by OurPay. Use at your own discretion. If you have questions about the provider, please contact the project maintainer. ## Installation **Step 1:** You can install the package via composer: ```bash Terminal composer require danestves/laravel-ourpay ``` **Step 2:** Run `:install`: ```bash Terminal php artisan ourpay:install ``` This will publish the config, migrations and views, and ask to run the migrations. Or publish and run the migrations individually: ```bash Terminal php artisan vendor:publish --tag="ourpay-migrations" ``` ```bash Terminal php artisan vendor:publish --tag="ourpay-config" ``` ```bash Terminal php artisan vendor:publish --tag="ourpay-views" ``` ```bash Terminal php artisan migrate ``` This is the contents of the published config file: ```php Settings | under the "Developers" section. | */ 'access_token' => env('OURPAY_ACCESS_TOKEN'), /* |-------------------------------------------------------------------------- | OurPay Webhook Secret |-------------------------------------------------------------------------- | | The OurPay webhook secret is used to verify that the webhook requests | are coming from OurPay. You can find your webhook secret in the OurPay | dashboard > Settings > Webhooks on each registered webhook. | | We (the developers) recommend using a single webhook for all your | integrations. This way you can use the same secret for all your | integrations and you don't have to manage multiple webhooks. | */ 'webhook_secret' => env('OURPAY_WEBHOOK_SECRET'), /* |-------------------------------------------------------------------------- | OurPay Url Path |-------------------------------------------------------------------------- | | This is the base URI where routes from OurPay will be served | from. The URL built into OurPay is used by default; however, | you can modify this path as you see fit for your application. | */ 'path' => env('OURPAY_PATH', 'ourpay'), /* |-------------------------------------------------------------------------- | Default Redirect URL |-------------------------------------------------------------------------- | | This is the default redirect URL that will be used when a customer | is redirected back to your application after completing a purchase | from a checkout session in your OurPay account. | */ 'redirect_url' => null, /* |-------------------------------------------------------------------------- | Currency Locale |-------------------------------------------------------------------------- | | This is the default locale in which your money values are formatted in | for display. To utilize other locales besides the default "en" locale | verify you have to have the "intl" PHP extension installed on the system. | */ 'currency_locale' => env('OURPAY_CURRENCY_LOCALE', 'en'), ]; ``` ## Usage ### Access Token Configure your access token. Create a new token in the OurPay Dashboard > Settings > Developers and paste it in the `.env` file. - https://sandbox.ourpay.workers.dev/dashboard/ORG_SLUG/settings (Sandbox) - https://ourpay.dev/dashboard/ORG_SLUG/settings (Production) ```bash Terminal OURPAY_ACCESS_TOKEN="" ``` ### Webhook Secret Configure your webhook secret. Create a new webhook in the OurPay Dashboard > Settings > Webhooks. - https://sandbox.ourpay.workers.dev/dashboard/ORG_SLUG/settings/webhooks (Sandbox) - https://ourpay.dev/dashboard/ORG_SLUG/settings/webhooks (Production) Configure the webhook for the following events that this package supports: - `order.created` - `order.updated` - `subscription.created` - `subscription.updated` - `subscription.active` - `subscription.canceled` - `subscription.revoked` - `benefit_grant.created` - `benefit_grant.updated` - `benefit_grant.revoked` ```bash Terminal OURPAY_WEBHOOK_SECRET="" ``` ### Billable Trait Let’s make sure everything’s ready for your customers to checkout smoothly. 🛒 First, we’ll need to set up a model to handle billing—don’t worry, it’s super simple! In most cases, this will be your app’s User model. Just add the Billable trait to your model like this (you’ll import it from the package first, of course): ```php use Danestves\LaravelOurPay\Billable; class User extends Authenticatable { use Billable; } ``` Now the user model will have access to the methods provided by the package. You can make any model billable by adding the trait to it, not just the User model. ### OurPay Script OurPay includes a JavaScript script that you can use to initialize the [OurPay Embedded Checkout](https://docs.ourpay.dev/features/checkout/embed). If you going to use this functionality, you can use the `@ourpayEmbedScript` directive to include the script in your views inside the `` tag. ```blade ... @ourpayEmbedScript ``` ### Webhooks This package includes a webhook handler that will handle the webhooks from OurPay. #### Webhooks & CSRF Protection Incoming webhooks should not be affected by [CSRF protection](https://laravel.com/docs/csrf). To prevent this, add your webhook path to the except list of your `App\Http\Middleware\VerifyCsrfToken` middleware: ```php protected $except = [ 'ourpay/*', ]; ``` Or if you're using Laravel v11 and up, you should exclude `ourpay/*` in your application's `bootstrap/app.php` file: ```php ->withMiddleware(function (Middleware $middleware) { $middleware->validateCsrfTokens(except: [ 'ourpay/*', ]); }) ``` ### Commands This package includes a list of commands that you can use to retrieve information about your OurPay account. | Command | Description | | ---------------------------- | ------------------------------------------ | | `php artisan ourpay:products` | List all available products with their ids | ### Checkouts #### Single Payments To create a checkout to show only a single payment, pass a single items to the array of products when creating the checkout. ```php use Illuminate\Http\Request; Route::get('/subscribe', function (Request $request) { return $request->user()->checkout(['product_id_123']); }); ``` If you want to show multiple products that the user can choose from, you can pass an array of product ids to the `checkout` method. ```php use Illuminate\Http\Request; Route::get('/subscribe', function (Request $request) { return $request->user()->checkout(['product_id_123', 'product_id_456']); }); ``` This could be useful if you want to offer monthly, yearly, and lifetime plans for example. > [!NOTE] > If you are requesting the checkout a lot of times we recommend you to cache the URL returned by the `checkout` method. #### Custom Price You can override the price of a product using the `charge` method. ```php use Illuminate\Http\Request; Route::get('/subscribe', function (Request $request) { return $request->user()->charge(1000, ['product_id_123']); }); ``` #### Embedded Checkout Instead of redirecting the user you can create the checkout link, pass it to the page and use our blade component: ```php use Illuminate\Http\Request; Route::get('/billing', function (Request $request) { $checkout = $request->user()->checkout(['product_id_123']); return view('billing', ['checkout' => $checkout]); }); ``` Now we can use the button like this: ```blade ``` The component accepts the normal props that a link element accepts. You can change the theme of the embedded checkout by using the following prop: ```blade ``` It defaults to light theme, so you only need to pass the prop if you want to change it. ### Prefill Customer Information You can override the user data using the following methods in your models provided by the `Billable` trait. ```php public function ourpayName(): ?string; // default: $model->name public function ourpayEmail(): ?string; // default: $model->email ``` ### Redirects After Purchase You can redirect the user to a custom page after the purchase using the `withSuccessUrl` method: ```php $request->user()->checkout('variant-id') ->withSuccessUrl(url('/success')); ``` You can also add the `checkout_id={CHECKOUT_ID}` query parameter to the URL to retrieve the checkout session id: ```php $request->user()->checkout('variant-id') ->withSuccessUrl(url('/success?checkout_id={CHECKOUT_ID}')); ``` ### Custom metadata and customer metadata You can add custom metadata to the checkout session using the `withMetadata` method: ```php $request->user()->checkout('variant-id') ->withMetadata(['key' => 'value']); ``` You can also add customer metadata to the checkout session using the `withCustomerMetadata` method: ```php $request->user()->checkout('variant-id') ->withCustomerMetadata(['key' => 'value']); ``` These will then be available in the relevant webhooks for you. #### Reserved Keywords When working with custom data, this library has a few reserved terms. - `billable_id` - `billable_type` - `subscription_type` Using any of these will result in an exception being thrown. ### Customers #### Customer Portal Customers can update their personal information (e.g., name, email address) by accessing their [self-service customer portal](https://docs.ourpay.dev/features/customer-portal). To redirect customers to this portal, call the `redirectToCustomerPortal()` method on your billable model (e.g., the User model). ```php use Illuminate\Http\Request; Route::get('/customer-portal', function (Request $request) { return $request->user()->redirectToCustomerPortal(); }); ``` Optionally, you can obtain the signed customer portal URL directly: ```php $url = $user->customerPortalUrl(); ``` ### Orders #### Retrieving Orders You can retrieve orders by using the `orders` relationship on the billable model: ```blade @foreach ($user->orders as $order) @endforeach
{{ $order->ordered_at->toFormattedDateString() }} {{ $order->ourpay_id }} {{ $order->amount }} {{ $order->tax_amount }} {{ $order->refunded_amount }} {{ $order->refunded_tax_amount }} {{ $order->currency }}
``` #### Check order status You can check the status of an order by using the `status` attribute: ```php $order->status; ``` Or you can use some of the helper methods offers by the `Order` model: ```php $order->paid(); ``` Aside from that, you can run two other checks: refunded, and partially refunded. If the order is refunded, you can utilize the refunded_at timestamp: ```blade @if ($order->refunded()) Order {{ $order->ourpay_id }} was refunded on {{ $order->refunded_at->toFormattedDateString() }} @endif ``` You may also see if an order was for a certain product: ```php if ($order->hasProduct('product_id_123')) { // ... } ``` Or for an specific price: ```php if ($order->hasPrice('price_id_123')) { // ... } ``` Furthermore, you can check if a consumer has purchased a specific product: ```php if ($user->hasPurchasedProduct('product_id_123')) { // ... } ``` Or for an specific price: ```php if ($user->hasPurchasedPrice('price_id_123')) { // ... } ``` ### Subscriptions #### Creating Subscriptions Starting a subscription is simple. For this, we require our product's variant id. Copy the product id and start a new subscription checkout using your billable model: ```php use Illuminate\Http\Request; Route::get('/subscribe', function (Request $request) { return $request->user()->subscribe('product_id_123'); }); ``` When a customer completes their checkout, the incoming `SubscriptionCreated` event webhook connects it to your billable model in the database. You may then get the subscription from your billable model: ```php $subscription = $user->subscription(); ``` #### Checking Subscription Status Once a consumer has subscribed to your services, you can use a variety of methods to check on the status of their subscription. The most basic example is to check if a customer has a valid subscription. ```php if ($user->subscribed()) { // ... } ``` You can utilize this in a variety of locations in your app, such as middleware, rules, and so on, to provide services. To determine whether an individual subscription is valid, you can use the `valid` method: ```php if ($user->subscription()->valid()) { // ... } ``` This method, like the subscribed method, returns true if your membership is active, on trial, past due, or cancelled during its grace period. You may also check if a subscription is for a certain product: ```php if ($user->subscription()->hasProduct('product_id_123')) { // ... } ``` Or for a certain price: ```php if ($user->subscription()->hasPrice('price_id_123')) { // ... } ``` If you wish to check if a subscription is on a specific price while being valid, you can use: ```php if ($user->subscribedToPrice('price_id_123')) { // ... } ``` Alternatively, if you use different [subscription types](#multiple-subscriptions), you can pass a type as an additional parameter: ```php if ($user->subscribed('swimming')) { // ... } if ($user->subscribedToPrice('price_id_123', 'swimming')) { // ... } ``` #### Cancelled Status To see if a user has cancelled their subscription, you can use the cancelled method: ```php if ($user->subscription()->cancelled()) { // ... } ``` When they are in their grace period, you can utilize the `onGracePeriod` check. ```php if ($user->subscription()->onGracePeriod()) { // ... } ``` #### Past Due Status If a recurring payment fails, the subscription will become past due. This indicates that the subscription is still valid, but your customer's payments will be retried automatically over a 21-day period. ```php if ($user->subscription()->pastDue()) { // ... } ``` #### Subscription Scopes There are several subscription scopes available for querying subscriptions in specific states: ```php // Get all active subscriptions... $subscriptions = Subscription::query()->active()->get(); // Get all of the cancelled subscriptions for a specific user... $subscriptions = $user->subscriptions()->cancelled()->get(); ``` Here's all available scopes: ```php Subscription::query()->incomplete(); Subscription::query()->incompleteExpired(); Subscription::query()->onTrial(); Subscription::query()->active(); Subscription::query()->pastDue(); Subscription::query()->unpaid(); Subscription::query()->cancelled(); ``` #### Changing Plans When a consumer is on a monthly plan, they may desire to upgrade to a better plan, alter their payments to an annual plan, or drop to a lower-cost plan. In these cases, you can allow them to swap plans by giving a different product id to the `swap` method: ```php use App\Models\User; $user = User::find(1); $user->subscription()->swap('product_id_123'); ``` This will change the customer's subscription plan, however billing will not occur until the next payment cycle. If you want to immediately invoice the customer, you can use the `swapAndInvoice` method instead. ```php $user = User::find(1); $user->subscription()->swapAndInvoice('product_id_123'); ``` #### Multiple Subscriptions In certain situations, you may wish to allow your consumer to subscribe to numerous subscription kinds. For example, a gym may provide a swimming and weight lifting subscription. You can let your customers subscribe to one or both. To handle the various subscriptions, you can offer a type of subscription as the second argument when creating a new one: ```php $user = User::find(1); $checkout = $user->subscribe('product_id_123', 'swimming'); ``` You can now always refer to this specific subscription type by passing the type argument when getting it: ```php $user = User::find(1); // Retrieve the swimming subscription type... $subscription = $user->subscription('swimming'); // Swap plans for the gym subscription type... $user->subscription('gym')->swap('product_id_123'); // Cancel the swimming subscription... $user->subscription('swimming')->cancel(); ``` #### Cancelling Subscriptions To cancel a subscription, call the `cancel` method. ```php $user = User::find(1); $user->subscription()->cancel(); ``` This will cause your subscription to be cancelled. If you cancel your subscription in the middle of the cycle, it will enter a grace period, and the ends_at column will be updated. The customer will continue to have access to the services offered for the duration of the period. You may check the grace period by calling the `onGracePeriod` method: ```php if ($user->subscription()->onGracePeriod()) { // ... } ``` OurPay does not offer immediate cancellation. To resume a subscription while it is still in its grace period, use the resume method. ```php $user->subscription()->resume(); ``` When a cancelled subscription approaches the end of its grace period, it becomes expired and cannot be resumed. ### Handling Webhooks OurPay can send webhooks to your app, allowing you to react. By default, this package handles the majority of the work for you. If you have properly configured webhooks, it will listen for incoming events and update your database accordingly. We recommend activating all event kinds so you may easily upgrade in the future. #### Webhook Events - `Danestves\LaravelOurPay\Events\BenefitGrantCreated` - `Danestves\LaravelOurPay\Events\BenefitGrantUpdated` - `Danestves\LaravelOurPay\Events\BenefitGrantRevoked` - `Danestves\LaravelOurPay\Events\OrderCreated` - `Danestves\LaravelOurPay\Events\OrderRefunded` - `Danestves\LaravelOurPay\Events\SubscriptionActive` - `Danestves\LaravelOurPay\Events\SubscriptionCanceled` - `Danestves\LaravelOurPay\Events\SubscriptionCreated` - `Danestves\LaravelOurPay\Events\SubscriptionRevoked` - `Danestves\LaravelOurPay\Events\SubscriptionUpdated` Each of these events has a billable `$model` object and an event `$payload`. The subscription events also include the `$subscription` object. These can be accessed via the public properties. If you wish to respond to these events, you must establish listeners for them. For example, you may wish to react when a subscription is updated. ```php payload['type'] === 'subscription.updated') { // Handle the incoming event... } } } ``` The [OurPay documentation](https://docs.ourpay.dev/integrate/webhooks/events) includes an example payload. Laravel v11 and up will automatically discover the listener. If you're using Laravel v10 or lower, you should configure it in your app's `EventServiceProvider`: ```php [ OurPayEventListener::class, ], ]; } ``` ## Testing ```bash Terminal composer test ``` ## Changelog Please see [CHANGELOG](https://github.com/danestves/laravel-ourpay/blob/main/CHANGELOG.md) for more information on what has changed recently. ## License The MIT License (MIT). Please see [License File](https://github.com/danestves/laravel-ourpay/blob/main/LICENSE.md) for more information. # Next.js Source: https://docs.ourpay.dev/integrate/sdk/adapters/nextjs ## Examples - [With Next.js](https://github.com/sunnycodet/examples/tree/main/with-nextjs) - [With Next.js and Upstash QStash](https://github.com/sunnycodet/examples/tree/main/with-nextjs-qstash-schedule-downgrades) - [With Next.js, Better Auth and Cloudflare Workers](https://github.com/sunnycodet/examples/tree/main/with-nextjs-better-auth-cloudflare-workers) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/nextjs ``` ```bash Terminal yarn add zod @ourpay-sh/nextjs ``` ```bash Terminal pnpm add zod @ourpay-sh/nextjs ``` ```bash Terminal bun add zod @ourpay-sh/nextjs ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" checkout/route.ts import { Checkout } from "@ourpay-sh/nextjs"; export const GET = Checkout({ accessToken: process.env.OURPAY_ACCESS_TOKEN, successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" portal/route.ts import { CustomerPortal } from "@ourpay-sh/nextjs"; export const GET = CustomerPortal({ accessToken: process.env.OURPAY_ACCESS_TOKEN, getCustomerId: (req: NextRequest) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" api/webhook/ourpay/route.ts import { Webhooks } from "@ourpay-sh/nextjs"; export const POST = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => { // Handle the payload // No need to return an acknowledge response }, }); ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Nuxt Source: https://docs.ourpay.dev/integrate/sdk/adapters/nuxt ## Examples - [With Nuxt](https://github.com/sunnycodet/examples/tree/main/with-nuxt) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/nuxt ``` ```bash Terminal yarn add zod @ourpay-sh/nuxt ``` ```bash Terminal pnpm add zod @ourpay-sh/nuxt ``` ```bash Terminal bun add zod @ourpay-sh/nuxt ``` ### Register the module Add the module to your `nuxt.config.ts`: ```typescript export default defineNuxtConfig({ modules: ["@ourpay-sh/nuxt"], }); ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" server/routes/api/checkout.post.ts export default defineEventHandler((event) => { const { private: { ourpayAccessToken, ourpayCheckoutSuccessUrl, ourpayServer }, } = useRuntimeConfig(); const checkoutHandler = Checkout({ accessToken: ourpayAccessToken, successUrl: ourpayCheckoutSuccessUrl, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: ourpayServer as "sandbox" | "production", theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); return checkoutHandler(event); }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" server/routes/api/portal.get.ts export default defineEventHandler((event) => { const { private: { ourpayAccessToken, ourpayCheckoutSuccessUrl, ourpayServer }, } = useRuntimeConfig(); const customerPortalHandler = CustomerPortal({ accessToken: ourpayAccessToken, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: ourpayServer as "sandbox" | "production", getCustomerId: (event) => { // Use your own logic to get the customer ID - from a database, session, etc. return Promise.resolve("9d89909b-216d-475e-8005-053dba7cff07"); }, }); return customerPortalHandler(event); }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" server/routes/webhook/ourpay.post.ts export default defineEventHandler((event) => { const { private: { ourpayWebhookSecret }, } = useRuntimeConfig(); const webhooksHandler = Webhooks({ webhookSecret: ourpayWebhookSecret, onPayload: async (payload) => { // Handle the payload // No need to return an acknowledge response }, }); return webhooksHandler(event); }); ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Remix Source: https://docs.ourpay.dev/integrate/sdk/adapters/remix ## Examples - [With Remix](https://github.com/sunnycodet/examples/tree/main/with-remix) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/remix ``` ```bash Terminal yarn add zod @ourpay-sh/remix ``` ```bash Terminal pnpm add zod @ourpay-sh/remix ``` ```bash Terminal bun add zod @ourpay-sh/remix ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" app/routes/checkout.tsx import { Checkout } from "@ourpay-sh/remix"; export const loader = Checkout({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" app/routes/customer-portal.tsx import { CustomerPortal } from "@ourpay-sh/remix"; export const loader = CustomerPortal({ accessToken: "xxx", // Or set an environment variable to OURPAY_ACCESS_TOKEN getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" app/routes/webhook.tsx import { Webhooks } from "@ourpay-sh/remix"; export const action = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => /** Handle payload */, }) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Supabase Source: https://docs.ourpay.dev/integrate/sdk/adapters/supabase ## Examples - [With Supabase and React Router v7](https://github.com/sunnycodet/examples/tree/main/with-react-router-supabase) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/supabase ``` ```bash Terminal yarn add zod @ourpay-sh/supabase ``` ```bash Terminal pnpm add zod @ourpay-sh/supabase ``` ```bash Terminal bun add zod @ourpay-sh/supabase ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript import { Checkout } from "@ourpay-sh/supabase"; export const GET = Checkout({ accessToken: OURPAY_ACCESS_TOKEN, successUrl: OURPAY_SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript import { CustomerPortal } from "@ourpay-sh/supabase"; export const GET = CustomerPortal({ accessToken: OURPAY_ACCESS_TOKEN, getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript import { Webhooks } from '@ourpay-sh/supabase'; export const POST = Webhooks({ webhookSecret: OURPAY_WEBHOOK_SECRET, onPayload: async (payload) => /** Handle payload */, }) ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Sveltekit Source: https://docs.ourpay.dev/integrate/sdk/adapters/sveltekit ## Examples - [With SvelteKit](https://github.com/sunnycodet/examples/tree/main/with-sveltekit) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/sveltekit ``` ```bash Terminal yarn add zod @ourpay-sh/sveltekit ``` ```bash Terminal pnpm add zod @ourpay-sh/sveltekit ``` ```bash Terminal bun add zod @ourpay-sh/sveltekit ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" src/routes/checkout/+server.ts import { Checkout } from "@ourpay-sh/sveltekit"; export const GET = Checkout({ accessToken: process.env.OURPAY_ACCESS_TOKEN, successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" src/routes/portal/+server.ts import { CustomerPortal } from "@ourpay-sh/sveltekit"; export const GET = CustomerPortal({ server: process.env.OURPAY_MODE, // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise accessToken: process.env.OURPAY_ACCESS_TOKEN, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal getCustomerId: (event) => "", // Function to resolve a OurPay Customer ID }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" src/routes/api/webhooks/ourpay/+server.ts import { Webhooks } from "@ourpay-sh/sveltekit"; export const POST = Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET, onPayload: async (payload) => { // Handle the payload console.log(payload) }, }); ``` ### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # TanStack Start Source: https://docs.ourpay.dev/integrate/sdk/adapters/tanstack-start ## Examples - [With TanStack Start](https://github.com/sunnycodet/examples/tree/main/with-tanstack-start) ## Installation Install the required OurPay packages using the following command: ```bash Terminal npm install zod @ourpay-sh/tanstack-start ``` ```bash Terminal yarn add zod @ourpay-sh/tanstack-start ``` ```bash Terminal pnpm add zod @ourpay-sh/tanstack-start ``` ```bash Terminal bun add zod @ourpay-sh/tanstack-start ``` ## Checkout Create a Checkout handler which takes care of redirections. ```typescript icon="square-js" routes/api/checkout.ts import { Checkout } from "@ourpay-sh/tanstack-start"; import { createFileRoute } from "@tanstack/react-start"; export const Route = createFileRoute("/api/checkout")({ server: { handlers: { GET: Checkout({ accessToken: process.env.OURPAY_ACCESS_TOKEN, successUrl: process.env.SUCCESS_URL, returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise theme: "dark", // Enforces the theme - System-preferred theme will be set if left omitted }), }, }, }); ``` ### Query Params Pass query params to this route. - products `?products=123` - customerId (optional) `?products=123&customerId=xxx` - customerExternalId (optional) `?products=123&customerExternalId=xxx` - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com` - customerName (optional) `?products=123&customerName=Jane` - metadata (optional) `URL-Encoded JSON string` ## Customer Portal Create a customer portal where your customer can view orders and subscriptions. ```typescript icon="square-js" routes/api/portal.ts import { CustomerPortal } from "@ourpay-sh/tanstack-start"; import { createFileRoute } from "@tanstack/react-start"; import { getSupabaseServerClient } from "~/servers/supabase-server"; export const Route = createFileRoute("/api/portal")({ server: { handlers: { GET: CustomerPortal({ accessToken: OURPAY_ACCESS_TOKEN, getCustomerId: async (request: Request) => "", // Function to resolve a OurPay Customer ID returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Checkout server: "sandbox", // Use sandbox if you're testing OurPay - omit the parameter or pass 'production' otherwise }), }, }, }); ``` ## Webhooks A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. ```typescript icon="square-js" routes/api/webhook/ourpay.ts import { Webhooks } from "@ourpay-sh/tanstack-start"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/api/webhook/ourpay")({ server: { handlers: { POST: Webhooks({ webhookSecret: process.env.OURPAY_WEBHOOK_SECRET!, onPayload: async (payload) => { // Handle the payload // No need to return an acknowledge response }, }), }, }, }); ``` #### Payload Handlers The Webhook handler also supports granular handlers for easy integration. - `onPayload` - Catch-all handler for any incoming Webhook event - `onCheckoutCreated` - Triggered when a checkout is created - `onCheckoutUpdated` - Triggered when a checkout is updated - `onOrderCreated` - Triggered when an order is created - `onOrderPaid` - Triggered when an order is paid - `onOrderRefunded` - Triggered when an order is refunded - `onRefundCreated` - Triggered when a refund is created - `onRefundUpdated` - Triggered when a refund is updated - `onSubscriptionCreated` - Triggered when a subscription is created - `onSubscriptionUpdated` - Triggered when a subscription is updated - `onSubscriptionActive` - Triggered when a subscription becomes active - `onSubscriptionCanceled` - Triggered when a subscription is canceled - `onSubscriptionRevoked` - Triggered when a subscription is revoked - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed - `onProductCreated` - Triggered when a product is created - `onProductUpdated` - Triggered when a product is updated - `onOrganizationUpdated` - Triggered when an organization is updated - `onBenefitCreated` - Triggered when a benefit is created - `onBenefitUpdated` - Triggered when a benefit is updated - `onBenefitGrantCreated` - Triggered when a benefit grant is created - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked - `onCustomerCreated` - Triggered when a customer is created - `onCustomerUpdated` - Triggered when a customer is updated - `onCustomerDeleted` - Triggered when a customer is deleted - `onCustomerStateChanged` - Triggered when a customer state changes # Python SDK Source: https://docs.ourpay.dev/integrate/sdk/python The official Python SDK provides fully typed synchronous and asynchronous clients for the OurPay API. The new SDK is currently in public preview. Install the pre-release explicitly to try it before the stable release. ## Installation The SDK requires Python 3.11 or later. ```bash Terminal uv add ourpay-sdk --prerelease allow ``` ```bash Terminal pip install --pre ourpay-sdk ``` ## Quickstart Create an [organization access token](/integrate/oat), store it in `OURPAY_ACCESS_TOKEN`, and make your first request: ```python main.py import os from ourpay.v2026_10 import OurPay ourpay = OurPay(os.environ["OURPAY_ACCESS_TOKEN"]) customer_state = ourpay.customers.get_state_external("customer_external_id") print(customer_state) ``` ### Async client Use `OurPayAsync` in asynchronous applications: ```python async_main.py import asyncio import os from ourpay.v2026_10 import OurPayAsync async def main() -> None: ourpay = OurPayAsync(os.environ["OURPAY_ACCESS_TOKEN"]) customer_state = await ourpay.customers.get_state_external( "customer_external_id" ) print(customer_state) asyncio.run(main()) ``` The import path pins your client to the `2026-04` API version. ## Context managers Both clients support context managers to close their HTTP connections automatically when the block exits. For synchronous applications, use `OurPay` with `with`: ```python import os from ourpay.v2026_10 import OurPay with OurPay(os.environ["OURPAY_ACCESS_TOKEN"]) as ourpay: customer_state = ourpay.customers.get_state_external("customer_external_id") print(customer_state) ``` For asynchronous applications, use `OurPayAsync` with `async with`: ```python import asyncio import os from ourpay.v2026_10 import OurPayAsync async def main() -> None: async with OurPayAsync(os.environ["OURPAY_ACCESS_TOKEN"]) as ourpay: customer_state = await ourpay.customers.get_state_external( "customer_external_id" ) print(customer_state) asyncio.run(main()) ``` ## Sandbox environment The client uses production by default. Pass `environment="sandbox"` to use the isolated [sandbox environment](/integrate/sandbox): ```python ourpay = OurPay( os.environ["OURPAY_ACCESS_TOKEN"], environment="sandbox", ) ``` [View the source code on GitHub](https://github.com/sunnycodet/ourpay/tree/main/sdk/python). # TypeScript SDK Source: https://docs.ourpay.dev/integrate/sdk/typescript The official TypeScript SDK provides a fully typed client for the OurPay API. The new SDK is currently in public preview. Install it from the `next` tag to try it before the stable release. ## Installation ```bash Terminal npm install @ourpay-dev/sdk@next ``` ```bash Terminal yarn add @ourpay-dev/sdk@next ``` ```bash Terminal pnpm add @ourpay-dev/sdk@next ``` ## Quickstart Create an [organization access token](/integrate/oat) for server-side use, keep it out of browser bundles, store it in `OURPAY_ACCESS_TOKEN`, and make your first request: ```typescript icon="square-js" index.js import { createOurPay } from "@ourpay-dev/sdk/2026-10"; const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, }); const customerState = await ourpay.customers.getStateExternal("customer_external_id"); console.log(customerState); ``` The import path pins your client to the `2026-04` API version. ## Sandbox environment The client uses production by default. Pass `environment: "sandbox"` to use the isolated [sandbox environment](/integrate/sandbox): ```typescript const ourpay = createOurPay({ accessToken: process.env.OURPAY_ACCESS_TOKEN!, environment: "sandbox", }); ``` [View the source code on GitHub](https://github.com/sunnycodet/ourpay/tree/main/sdk/typescript). ## Framework adapters Implement Checkout & Webhook handlers in few lines of code. - [Astro](/integrate/sdk/adapters/astro) - [Better Auth](/integrate/sdk/adapters/better-auth) - [Deno](/integrate/sdk/adapters/deno) - [Elysia](/integrate/sdk/adapters/elysia) - [Express](/integrate/sdk/adapters/express) - [Hono](/integrate/sdk/adapters/hono) - [Fastify](/integrate/sdk/adapters/fastify) - [Next.js](/integrate/sdk/adapters/nextjs) - [Nuxt](/integrate/sdk/adapters/nuxt) - [Remix](/integrate/sdk/adapters/remix) - [Sveltekit](/integrate/sdk/adapters/sveltekit) - [TanStack Start](/integrate/sdk/adapters/tanstack-start) # Handle & monitor webhook deliveries Source: https://docs.ourpay.dev/integrate/webhooks/delivery Once a webhook endpoint is setup you will have access to the delivery overview page. Here you can: - See historic deliveries - Review payload sent - Trigger redelivery in case of failure Now, let's integrate our endpoint route to validate, parse & handle incoming webhooks. ## Validate & parse webhooks You now need to setup a route handler for the endpoint registered on OurPay to receive, validate and parse webhooks before handling them according to your needs. ### Using our SDKs Our TypeScript & Python SDKs come with a built-in helper function to easily validate and parse the webhook event - see full examples below. ```typescript icon="square-js" JS (Express) import express, { Request, Response } from 'express' import { validateEvent, WebhookVerificationError } from '@ourpay-dev/sdk/webhooks' const app = express() app.post( '/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => { try { const event = validateEvent( req.body, req.headers, process.env['OURPAY_WEBHOOK_SECRET'] ?? '', ) // Process the event res.status(202).send('') } catch (error) { if (error instanceof WebhookVerificationError) { res.status(403).send('') } throw error } }, ) ``` ```python Python (Flask) import os from flask import Flask, request from ourpay_sdk.webhooks import validate_event, WebhookVerificationError app = Flask(__name__) @app.route('/webhook', methods=['POST']) def webhook(): try: event = validate_event( body=request.data, headers=request.headers, secret=os.getenv('OURPAY_WEBHOOK_SECRET', ''), ) # Process the event return "", 202 except WebhookVerificationError as e: return "", 403 ``` Both examples above expect an environment variable named `OURPAY_WEBHOOK_SECRET` to be set to the secret you configured during the endpoint setup. ### Custom validation We follow the [Standard Webhooks](https://www.standardwebhooks.com/) standard which offers [many libraries across languages](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) to easily validate signatures. Or you can follow their [specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) in case you want to roll your own. **Note: Secret needs to be base64 encoded** One common gotcha with the specification is that the webhook secret is expected to be base64 encoded. You don't have to do this with our SDK as it takes care of the implementation details with better developer ergonomics. ## IP Allowlist If you are using a firewall or a reverse proxy that requires IP allowlisting, here are the IPs range you need to allow: **New IP ranges** Starting **October 27th, 2025**, new IP ranges will be added: ``` 74.220.50.0/24 74.220.58.0/24 ``` ```txt Production 3.134.238.10 3.129.111.220 52.15.118.168 74.220.50.0/24 74.220.58.0/24 ``` ```txt Sandbox 3.134.238.10 3.129.111.220 52.15.118.168 74.220.50.0/24 74.220.58.0/24 ``` ## Failure Handling ### Delivery Retries If we hit an error while trying to reach your endpoint, whether it is a temporary network error or a bug, we'll retry to send the event up to **10 times** with an exponential backoff. ### Delivery Timeouts We currently timeout our requests to your endpoint after **10 seconds**, triggering a retry attempt after a delay as explained above. However, we strongly recommend you optimize your endpoint route to respond within **2 seconds** to ensure reliable delivery. We may lower the timeout threshold in the future, so we advise implementing your webhook handler to queue a background worker task to handle the payload asynchronously. ### Endpoint Disabling Webhook endpoints are automatically disabled after **10 consecutive failed deliveries** (non-2xx responses). When this happens: - The endpoint is marked as disabled and will no longer receive new events. - All organization members will receive an email notification. To re-enable a disabled endpoint, open your organization's [webhook settings](https://ourpay.dev/to/dashboard/settings/webhooks) and manually enable it. Before re-enabling, ensure your endpoint is properly configured and reachable to avoid repeated disabling. ## Troubleshooting ### Not receiving webhooks Seeing deliveries on OurPay, but not receiving them on your end? Below are some common techniques to resolve the issue depending on the reported error status. **General** _Start ngrok or similar_ Make sure you have started `ngrok` or whatever tunneling service you're using during local development. _Add excessive logging_ E.g `console.log('webhook.handler_called')`, `console.log('webhook.validate_signature')`, `console.log('webhook.signature_validated')` etc. So you can easily confirm if the handler is called and how far it gets before any issues arise. `HTTP 404` - Try `curl -vvv -X POST ` in your terminal to confirm the route exists and see any issues along the way - Try adding trailing `/` to the URL on OurPay. Often `/foo` is resolved to `/foo/` by frameworks. `HTTP 3xx` Redirect responses (301, 302, 307, etc.) are treated as failures. OurPay does not follow redirects for webhook deliveries. Update your webhook URL to the final destination URL to avoid redirects. A common cause is hosting providers like Vercel that redirect between `www` and non-`www` domains. Make sure your configured URL matches your actual domain. `HTTP 403` - Using middleware for authorization? Make sure to exclude the webhook route from it since it needs to be publicly accessible - Using Cloudflare? - Check the firewall logs to verify if they are blocking our requests and setup a custom WAF rule to accept incoming requests from OurPay. - Webhook delivery failures with 403 errors can occur when Cloudflare's Bot Fight Mode is enabled. Bot Fight Mode automatically blocks requests it identifies as bots, including legitimate webhook requests from OurPay. Adding OurPay's IP addresses to your IP Allow List or creating custom WAF rules will not resolve this issue. To fix webhook delivery problems, disable Bot Fight Mode in your Cloudflare dashboard under Security > Bots. Alternatively, you can check your Cloudflare firewall logs to confirm if requests are being blocked and create appropriate firewall rules if needed. ### Invalid signature exceptions Rolling your own webhook validation logic? Make sure to base64 encode the secret you configured on OurPay in your code before generating the signature to validate against. # Setup Webhooks Source: https://docs.ourpay.dev/integrate/webhooks/endpoints Our webhook implementation follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification and our SDKs offer: - Built-in webhook signature validation for security - Fully typed webhook payloads In addition, our webhooks offer built-in support for **Slack** & **Discord** formatting. Making it a breeze to setup in-chat notifications for your team. ## Get Started **Use our sandbox environment during development** So you can easily test purchases, subscriptions, cancellations and refunds to automatically trigger webhook events without spending a dime. Head over to your organization settings and click on the `Add Endpoint` button to create a new webhook. Enter the URL to which the webhook events should be sent. For standard, custom integrations, leave this parameter on **Raw**. This will send a payload in JSON format. If you wish to send notifications to a Discord or Slack channel, you can select the corresponding format here. OurPay will then adapt the payload so properly formatted messages are sent to your channel. If you paste a Discord or Slack Webhook URL, the format will be automatically selected. We cryptographically sign the requests using this secret. So you can easily verify them using our SDKs to ensure they are legitimate webhook payloads from OurPay. You can set your own or generate a random one. Finally, select all the events you want to be notified about and you're done 🎉 **Developing locally?** Install OurPay CLI to use the listening command. This will allow you to test your webhook handlers without deploying them to a live server. Install the OurPay CLI ```bash Terminal curl -fsSL https://ourpay.dev/install.sh | bash ``` Once you have installed the OurPay CLI, you can easily start a tunnel: ```bash Terminal ourpay listen http://localhost:3000/ ``` This will relay webhooks automatically to the speicified URL. ```bash ✔ Select Organization … My Organization Connected My Organization Secret 6t3c8ce2247c493a3ade20uea4484d64 Forwarding http://localhost:3000 Waiting for events... ``` [Now, it's time to integrate our endpoint to receive events →](/integrate/webhooks/delivery) # Webhook Events Source: https://docs.ourpay.dev/integrate/webhooks/events ## Billing Events ### Checkout Fired when a checkout link has expired without being completed. ### Customers Fired when a new customer has been created. Fired when a customer has been updated. Fired when a customer has been deleted. Fired when a customer's state has changed. Includes active subscriptions and granted benefits. ### Subscriptions In order to properly implement logic for handling subscriptions, you should look into the following events. Fired when a new subscription has been created. Fired when a subscription enters a new billing period, before the renewal order exists and whether or not the payment succeeds. Fired when a subscription payment has failed. The customer can recover by updating their payment method. Use this event if you want to handle cancellations, un-cancellations, etc. The updated event is a catch-all event for `subscription.active`, `subscription.canceled`, `subscription.uncanceled`, `subscription.cycled`, `subscription.past_due`, `subscription.revoked`, `subscription.paused` and `subscription.resumed`. Carries a `billing_reason` field, which can be `purchase`, `subscription_create`, `subscription_cycle` and `subscription_update`. To act on a renewal, listen to `subscription.cycled` instead: it fires whether or not the renewal payment succeeds. Fired when a scheduled pause takes effect at the end of the period. Billing stops and benefits are revoked until the subscription resumes. Fired when a paused subscription resumes. A new billing period starts and the customer is charged immediately. #### Cancellation Sequences When a subscription is canceled, the events triggered depend on whether the cancellation is immediate or scheduled for the end of the billing period. **End-of-Period Cancellation (default)** When a subscription is **canceled** (by customer action from the portal or by the merchant from the dashboard/API), the following events are sent immediately: 1. `subscription.updated` 2. `subscription.canceled` Both events contain the same subscription data. The subscription will still have `active` status, but the `cancel_at_period_end` flag will be set to `true`. When the end of the current billing period arrives, the subscription is definitively revoked: billing cycles stop and benefits are revoked. The following events are then sent: 3. `subscription.updated` 4. `subscription.revoked` Both events contain the same subscription data. The subscription will have the `canceled` status. **Immediate Revocation** When a merchant cancels a subscription with **immediate revocation**, those events are sent at once: 1. `subscription.updated` 2. `subscription.canceled` 3. `subscription.revoked` All three events contain the same subscription data. The subscription will have the `canceled` status immediately. #### Renewal Sequences When a subscription is renewed for a new cycle, the webhook events are triggered in a specific sequence to help you track the renewal process and handle billing logic appropriately. **Initial Renewal Events** When a subscription reaches its renewal date, the following events are sent immediately (if enabled on the webhook): 1. `subscription.cycled` 2. `subscription.updated` 3. `order.created` `subscription.cycled` fires only on a new billing period, so you can act on a renewal without inspecting `billing_reason` on the order. It also fires when a trial converts to a paid subscription, since that starts a period too: read `status` to tell the two apart. The subscription data will reflect the new billing period through the `current_period_start` and `current_period_end` properties, showing the updated cycle dates. The order data represents the new invoice for the upcoming cycle, with a total representing what the customer will pay for this new period. If usage-based billing is involved, their consumption for the past period will be included in the total. The status of this order is `pending` at this stage. **Payment Processing Events** Shortly after the initial renewal events, the platform will trigger a payment for the new order. Once the payment is successfully processed, the following events are sent: 4. `order.updated` 5. `order.paid` Both events will contain the same order data, with the order status changed to `paid`. #### Pause Sequences Pausing is scheduled for the end of the current billing period, much like an end-of-period cancellation. When a subscription is **paused** (by customer action from the portal or by the merchant from the dashboard/API), only one event is sent immediately, because the subscription stays `active` until the period ends: 1. `subscription.updated` The `pause_at_period_end` flag is set to `true`, and `resumes_at` holds the scheduled resume date if you set one. When the end of the current billing period arrives, the pause takes effect: billing stops and benefits are revoked. The following events are then sent: 2. `subscription.updated` 3. `subscription.paused` Both events contain the same subscription data, now with the `paused` status. **Resume** When a paused subscription resumes, either immediately when you resume it or automatically on its `resumes_at` date, a new billing period starts and the customer is charged right away. The following events are sent: 1. `subscription.updated` 2. `subscription.resumed` 3. `order.created` The subscription data reflects the new period through `current_period_start` and `current_period_end`, and the order represents the immediate charge for the new cycle. ### Orders ### Refunds ### Benefit Grants ## Organization Events ### Benefits ### Products ### Discounts ### Organization # Integrating Webhooks Locally Source: https://docs.ourpay.dev/integrate/webhooks/locally ### Install OurPay CLI macOS, Linux, WSL: ```bash curl -fsSL https://ourpay.dev/install.sh | bash ``` ### Login to your account This will allow you to authenticate with OurPay. ```bash ourpay login ``` ### Listen for Webhooks ```bash ourpay listen http://localhost:3000/ ``` You will be prompted to select which Organization you want to listen for. ```bash ✔ Select Organization … My Organization Connected My Organization Secret 6t3c8ce2247c493a3ade20uea4484d64 Forwarding http://localhost:3000 Waiting for events... ``` ### Set the secret Make sure that you copy the secret & set it in your environment variables. If you don't set the correct secret, you'll see 403 errors if you use our Webhook utilities in your app. ```bash # .env OURPAY_WEBHOOK_SECRET=6t3c8ce2247c493a3ade20uea4484d64 ``` ### All set! You're now fully setup. Webhooks will be tunneled via the CLI listen connection, and relayed to the specified target URL. # OurPay: Turn Your Software into a Business Source: https://docs.ourpay.dev/introduction ## What is OurPay? Turn your software into a business with OurPay. Sell digital products, subscriptions, and more without the hassle of traditional payment systems. Unlike Stripe that only handles transactions, we provide complete billing infrastructure with tax compliance, product management, and automated access & delivery. We handle all international tax compliance, so you can sell globally without worrying about VAT, GST, or sales tax regulations. ## Problems We Solve **The Problem**: Selling digital products globally means dealing with VAT, GST, and sales tax in dozens of jurisdictions, each with different rates, rules, and filing requirements. Most developers either ignore this (risky) or avoid international sales entirely. **OurPay's Solution**: As your Merchant of Record, we handle all international tax compliance. We calculate, collect, and remit taxes worldwide. You focus on building; we handle the paperwork. **The Problem**: Building subscription billing, product catalogs, customer portals, and payment flows from scratch takes months of development time and ongoing maintenance. **OurPay's Solution**: Complete billing infrastructure out-of-the-box with APIs that let you integrate in minutes. No need to build customer portals, handle subscription lifecycle, or manage failed payments. **The Problem**: Manually sending license keys, granting repository access, or managing Discord invites for every purchase doesn't scale and creates delays for customers. **OurPay's Solution**: Automated benefit delivery for common developer needs - license keys, file downloads, GitHub repo access, Discord roles, and more. Customers get instant access. **The Problem**: Traditional MoR solutions charge 5-8% per transaction plus monthly fees, eating into your profits before you even start. **OurPay's Solution**: Transparent fee structure starting at 5% + 50¢ per transaction on the free Starter plan, with optional paid plans that lower your rate. We earn when you earn. ## Core Features ### Flexible Product Management Sell digital products, courses, templates, or software licenses with instant delivery Recurring billing with automatic renewals and dunning management Fixed price, pay-what-you-want, or free products with optional minimums ### Powerful Checkout Experience No-code solution for quick product sales. Create and share instantly. Integrate seamlessly into your website with customizable branding. Programmatically create dynamic checkout sessions for custom flows. ### Automated Benefits (Entitlements) **Set it and forget it**: Configure once, and customers get instant access to their benefits automatically. No manual work required. Generate and deliver software licenses automatically with custom formats Secure delivery of digital assets up to 10GB with download tracking Auto-invite customers to private repositories and manage permissions Automatic role assignment and server invites for community access ### Global Merchant of Record - **Worldwide Tax Compliance** - We handle VAT, GST, and sales tax in all jurisdictions - **EU VAT Handling** - Proper B2B reverse charge and B2C tax collection - **Automatic Tax Calculation** - Real-time tax rates for every transaction ## Quick Start Guide [Sign up for OurPay](https://ourpay.dev/signup) using GitHub, Google, or email. Create an organization to manage your products and customers. Set up a digital product in minutes: - Choose between one-time purchase or subscription - Set your pricing (fixed, pay-what-you-want, or free) - Configure automated benefits for instant delivery Learn more about [Products →](/features/products) Pick the approach that fits your needs: Perfect for getting started quickly: - Create [Checkout Links](/features/checkout/links) from your dashboard - Share via email, social media, or embed in websites - Start accepting payments immediately Integrate into your existing website: - Add our [Embedded Checkout](/features/checkout/embed) component - Maintain your site's look and feel - Customers never leave your domain Maximum flexibility for custom workflows: - Use our [SDKs](/integrate/sdk/typescript) for any language - Build custom checkout flows and experiences - Integrate with your existing tech stack Stay synchronized with customer events: - Configure webhook endpoints in your dashboard - React to purchases, subscription changes, and customer events - Keep your database in sync automatically Read the [Webhooks guide →](/integrate/webhooks/endpoints) ## Integration Options ### Framework Adapters (Recommended) React-based full-stack framework with App Router support Payments and billing empowered by authentication & authorization PHP web application framework with Eloquent ORM integration A modern runtime for TypeScript Vue.js framework React framework Fast Node.js Cloudflare Workers Modern runtime Full-stack React Bun framework Static site generator Flexible Node.js framework Full-stack framework ### Native SDKs For web and Node.js applications For Django, Flask, FastAPI frameworks ## Why Choose OurPay? Focus on your product, not billing infrastructure. Get to market weeks faster. Sell worldwide without worrying about tax compliance or regional restrictions. License keys and downloads handled automatically. No manual work required. Transparent, pay-as-you-earn pricing with no hidden fees. Complete billing solution without months of custom development work. Pay only when you earn, no monthly minimums or setup fees. Multiple team members can manage products, customers, and analytics. Branded experience that builds customer trust and increases conversions. Advanced analytics, custom fields, bulk operations, and priority support. Full programmatic control over products, customers, orders, and subscriptions. Reliable real-time synchronization with your systems and database. ## Transparent Pricing **Per successful transaction** Optional paid plans to get even lower transaction fees **Additional fees may apply**: Some transactions may incur additional fees (e.g. international cards). Payout fees are charged by payment providers. See our [detailed fees page](/merchant-of-record/fees) for complete information. ## Open Source & Community OurPay is built in the open with full transparency and a growing community of contributors. Apache 2.0 license with 36+ contributors and growing Feature requests, roadmap, and issues - all developed in public No hidden fees or surprise charges. What you see is what you pay. While self-hosting is technically possible, we recommend using our hosted service to get the full Merchant of Record benefits including global tax compliance. ## Ready to Start? **Free signup, no credit card required** Get started in under 2 minutes **Framework-specific tutorials** Step-by-step integration guides **Complete API documentation** Interactive examples and SDKs **Get help from our team** Active community and support # Acceptable use Source: https://docs.ourpay.dev/merchant-of-record/acceptable-use OurPay's acceptable-use rules are maintained in the [Acceptable Use Policy](https://ourpay.dev/legal/acceptable-use-policy). # account-reviews Source: https://docs.ourpay.dev/merchant-of-record/account-reviews As a Merchant of Record (MoR), OurPay resells your digital goods and services on your behalf. To do this responsibly, we verify that every business using OurPay complies with our [Acceptable Use Policy](https://ourpay.dev/legal/acceptable-use-policy), and we continuously monitor transactions to prevent fraud, abuse, and high-risk activity. We process every review as quickly as we can and resolve every single one. ### First payout review **Build first, submit second.** It's tempting to get approved before doing the integration work, but a complete setup is what makes a review fast and clean. Configure your [products and benefits](/features/products), wire up your integration (checkout links, API keys, webhooks), and have a live website pointing to it. The more we can see end-to-end, the more confidently — and quickly — we can approve your account. Before your first payout, you'll go through our main account review. You can submit everything yourself from [**Finance → Account**](https://ourpay.dev/to/dashboard/finance/account) in your OurPay dashboard, in three steps: 1. **Submit for approval.** Tell us about your business, your products, and how you intend to use OurPay. 2. **Identity verification (KYC).** The organization owner verifies their identity with a passport, ID card, or driver's license along with a selfie — secure, easy, and quick through Stripe Identity. 3. **Connect a payout account.** Set up a [payout account](/features/finance/accounts) via Stripe Connect Express so we can transfer your earnings to you. Initial reviews can take **up to 14 days** to complete, depending on volume, weekends, and holidays. This review keeps us compliant with our [Acceptable Use Policy](https://ourpay.dev/legal/acceptable-use-policy) and meets our own KYC/AML requirements as a billing platform. ### Continuous reviews We continuously monitor all transactions across our platform to proactively prevent fraud, and we run asynchronous reviews of accounts at certain sales thresholds. Most of these reviews don't require any additional information from you. You can always request a [payout](/features/finance/payouts). A request made while your organization is under review shows as "Held for review" until approval, then is paid out automatically. Your customers are unaffected: new purchases keep going through, and existing subscriptions renew as usual. In a continuous review, we look at two things: that your use case is still within our [Acceptable Use Policy](https://ourpay.dev/legal/acceptable-use-policy) and consistent with what was approved during your first review, and that your account is in good financial health — refunds, chargebacks, and risk scores across recent transactions. **Chargebacks and card networks.** Credit card networks (e.g. Visa, Mastercard) consider chargebacks above 0.7% of sales excessive. Exceeding that threshold can lead to monitoring programs with extra costs, penalties, and ultimately termination from the network. We might reach out proactively to collaborate on lowering your chargeback ratio before it approaches these thresholds. ## Operational Guidelines ### Customer Support You're responsible for supporting your own customers. The volume of support requests we receive about your account, and how you handle them when we loop you in, both factor into our reviews. When we include you in a customer support thread, we expect a response within **48 hours**. If we don't hear back, we may refund the affected customers and issue a warning. Repeated unresponsiveness leads to offboarding. ### Test Transactions Don't run test purchases with real card details. Payment processors flag this as "card testing" and can block the card or your account, and it triggers our own account reviews. Use our sandbox environment instead. If you need to verify something in production, set up a free product or a 100% discount code so no real money changes hands. ### Chargeback Management We hold accounts to a **0.4% chargeback rate** — well below the 0.7% threshold the card networks themselves treat as excessive. Customers can file chargebacks up to 120 days after a transaction, so we monitor these continuously. If your rate climbs toward our threshold, we'll reach out first to collaborate on bringing it down. If that doesn't work, we may take the following actions, in order of severity: 1. Refund individual transactions as needed. 2. Hold payouts pending review, until the 120-day chargeback window for those transactions has closed. 3. Pause future payments. 4. Block the account and refund customers. We don't take any of these actions lightly and always try to resolve issues with you first. We integrate with credit card networks to receive early chargeback signals before they're officially filed. Below a certain transaction value, we automatically refund and cancel any related subscription to reduce chargebacks proactively. If we issue a proactive refund, we'll notify you by email. Keep in mind that while a refund reverses the payment, it may not revoke benefits the customer has already received or accessed, such as downloaded files or forked repositories. ### Policy Violations When an account violates our [Acceptable Use Policy](https://ourpay.dev/legal/acceptable-use-policy) — separately from chargeback issues — we offboard the merchant. Payment processing is blocked and payouts are held pending review. If we suspect fraud or intentional abuse, we block the account immediately. Otherwise, we reach out and give you 48 hours to respond; failure to respond may result in refunds to affected customers. We may also run test transactions ourselves to verify account status. We aim to work with you on the best path forward, but for compliance and risk reasons we have to cancel subscriptions and refund payments made in violation of the policy. ## FAQ Your account may go through multiple reviews as your business grows. We perform [continuous reviews](#continuous-reviews) at certain sales thresholds to maintain platform integrity and prevent fraud. This is a standard practice across payment platforms and is part of our ongoing risk management process. We request social media information as part of our identity verification and fraud prevention processes. This helps us: * Verify that you're a real business or creator with an online presence * Understand your products and services better * Ensure compliance with our [acceptable use policies](https://ourpay.dev/legal/acceptable-use-policy) Providing accurate social media information helps speed up the review process and demonstrates the legitimacy of your business. No, your social media settings are not publicly visible. This information is used internally for verification and compliance purposes only. We treat all merchant information with strict confidentiality and use it solely for risk assessment and account review processes. To help us verify that everything is working correctly in line with our acceptable use policy, our team would ask you to share a 100% discount code by email. This is our preferred method, as it allows the team to go through the full journey themselves and confirm the automated fulfillment from an unpaid user to a paid user. Alternatively, you can provide a video recording that clearly shows the complete flow from an unpaid user to a paid user, including how the product is automatically accessible after purchase. To transfer admin ownership of an organization: 1. Invite the new admin to the team via `Settings` > `Members` in the OurPay dashboard 2. Ask that new admin to complete identity verification under `Finance` > `Account` after logging in via that email in the OurPay dashboard 3. Make sure no payout is pending 4. Send an email from the current admin email to our support confirming the transfer to the new admin If you need assistance with changing organization ownership or have special circumstances, please contact [support@ourpay.com](mailto:support@ourpay.com). # Fees Source: https://docs.ourpay.dev/merchant-of-record/fees ## Plans OurPay offers a free Starter plan plus three optional paid plans — Pro, Growth, and Scale — that lower your variable rate and prioritize your support inquiries. You can switch between plans anytime, and your rate adjusts immediately. | Plan | Monthly fee | Per transaction | Support | Included | | --- | --- | --- | --- | --- | | **Starter** | Free | 5% + 50¢ | Standard Support | | | **Pro** | $20 /mo | 3.8% + 40¢ | Prioritized Support | | | **Growth** | $100 /mo | 3.6% + 35¢ | Prioritized Support | | | **Scale** | $400 /mo | 3.4% + 30¢ | Slack + Prioritized Support | [Single Sign-On](/features/sso) | The paid plans replace the per-transaction Merchant of Record premium with a fixed monthly fee and a lower variable rate. ### When does a paid plan pay off? Each paid plan crosses over to save you money at a predictable monthly sales threshold. | Plan | Breakeven vs. Starter | | --- | --- | | **Pro** | ~$1,379 /mo in sales | | **Growth** | ~$5,634 /mo in sales | | **Scale** | ~$19,048 /mo in sales | Below your plan's threshold, a lower tier is the better deal. Above it, the paid plan saves money — and you get faster support on top. ## Preview features Paid plans also unlock early access to features that are still in preview. While in preview, each of these is available only on paid plans: - [Shared Slack Channel benefit](/features/benefits/slack-shared-channel) — automatically give each customer a shared Slack channel via Slack Connect. - [`reset` proration behavior](/features/subscriptions/proration#charge-full-amount-and-reset-cycle-reset) — on a plan change, charge the full new-plan price and restart the billing cycle, with no proration. - [Off-session charges](/features/orders#arbitrary-charges) — charge a customer's saved payment method for an arbitrary amount, outside of a checkout or renewal. ## Early Member Organizations created before **May 27, 2026** stay on the Early Member rate indefinitely. This was the rate we offered while OurPay was catching up on feature parity with other Merchant of Record providers, and we've committed to honoring it for everyone who signed up under it. | Monthly fee | Per transaction | Subscription fee | | --- | --- | --- | | Free | 4% + 40¢ | +0.5% | **One trade-off worth understanding:** Early Member is yours forever as long as you stay on it. The moment you upgrade to a paid plan, Early Member is retired for that organization. You can still switch freely between the paid plans afterwards, but downgrading to Starter lands you on the new 5% + 50¢ rate, not your original Early Member rate. Organizations created on or after May 27, 2026 start on Starter (5% + 50¢). This applies to new organizations even if they're created by customers who signed up earlier. ## Additional Fees These apply on top of your plan's per-transaction fee. * **+1.5%** for international cards (non-US) * **+0.5%** for subscription payments — **Early Member only**. Starter, Pro, Growth, and Scale have no separate subscription fee. * *We also reserve the right to pass on any other fees Stripe might impose in the future.* ### Example Below is a $30 purchase from Sweden (25% VAT) paid with an international card. | Item | Amount | | --- | --- | | Product Price | $30 | | VAT (25%) | $7.5 | | **Total Transaction Value** | **$37.5** | Here's how the fees on that $37.5 transaction compare across plans. | Plan | Transaction Fee | International (+1.5%) | Total Fees | | --- | --- | --- | --- | | **Starter** (5% + 50¢) | \$2.38 | \$0.56 | **\$2.94** | | **Pro** (3.8% + 40¢) | \$1.83 | \$0.56 | **\$2.39** | | **Growth** (3.6% + 35¢) | \$1.70 | \$0.56 | **\$2.26** | | **Scale** (3.4% + 30¢) | \$1.58 | \$0.56 | **\$2.14** | ## Refunds You can issue both full or partial refunds on OurPay to your customers. However, the initial transaction fees are not refunded to you since credit card networks and PSPs charge them regardless of a future refund. Please note: OurPay reserves the right to issue refunds at our own discretion up to 60 days after the purchase as part of our efforts to continuously and proactively reduce disputes & chargebacks which cost you $15/dispute. We only leverage this right for this purpose and in the interest of reducing chargebacks and fees for you. [Learn more about refunds →](/features/refunds) ## Dispute/Chargeback Fees Sometimes, customers can open a **dispute/chargeback** via their bank for a purchase. **Disputes cost $15 per dispute** regardless of outcome and is deducted from your balance directly. This fee is charged by the underlying credit card networks & PSPs regardless of outcome and therefore something we cannot refund. However, we continuously work to proactively reduce the rate of chargebacks across OurPay to be at or lower than industry standards. Credit card networks impose monitoring programs, penalties and higher chargeback costs for sellers with high chargeback rates (~0.7%+). Since OurPay is the Merchant of Record, we therefore always monitor and proactively prevent our rate coming close to these thresholds. Therefore, we might need to intervene and even suspend your account unless swift and proactive measures are taken to reduce chargebacks to an acceptable industry standard. ## Payout Fees While payouts may incur fees charged by our payout provider (Stripe), OurPay does not add any extra fees or markup. These are strictly Stripe's fees, and OurPay does not profit from them. In addition, OurPay offers manual withdrawals for developers. Keeping you in control of when to issue payouts. *Unless you have a OurPay balance that you haven't withdrawn for several months, at which point we'll eventually need to trigger a payout on your behalf.* **Fees (Stripe)** * $2 per month of active payout(s) * 0.25% + $0.25 per payout * Cross border fees (currency conversion): 0.25% (EU) - 1% in other countries. ## Volume pricing Large or fast-growing business? The published Scale plan is our cheapest public rate. If you need something custom on top of that, [reach out to us](/support). # Merchant of Record Source: https://docs.ourpay.dev/merchant-of-record/introduction ### What is a Merchant of Record? We take on the liability of international sales taxes globally for you. So you can focus on growing your business vs. accounting bills. Leave billing infrastructure and international sales tax headaches to us. ### Payment Service Providers vs. Merchants of Record **Payment Service Providers (PSPs)** Stripe and other Payment Service Providers (PSPs) offer an accessible and convenient abstraction to faciliate transactions on top of underlying credit card networks & banks. - ✅ Powerful, flexibile & low-level APIs to facilitate transactions - ✅ Can be used to power all business- and pricing models under the sun. - ❌ You are responsible for all liabilities associated with transactions, e.g international taxes - ❌ Low-level APIs require more development even for common use cases **Merchants of Record (MoRs)** Merchants of Record offer yet another layer of convenient abstraction to facilitate digital orders on top of the underlying PSPs and transactions. E.g OurPay is built on Stripe (+ more PSPs in the future). - ✅ Higher-level Dashboard, APIs & SDKs to better facilitate digital products, services & orders beyond the underlying transactions - ✅ The platform (OurPay) handles international taxes by being a reseller of your digital goods & services. Of course, without being in the way of your relationship with your customers. - ❌ Less flexibility & control in terms of advanced business- and pricing models. - ❌ Higher fees per payment **What should you choose?** **Ship with what you feel comfortable with vs. others tell you to** Just like in programming, abstractions are super helpful to ship faster with fewer low-level concerns, but in exchange for reduced flexibility and higher costs. So what's the right level of abstraction for you? As always, it depends (tm). **Go with Stripe (PSP) if...** - You've already integrated it? Just ship already - we salute builders however they ship - You're comfortable with the Stripe API and prefer absolute control with low-level APIs. - You're looking for the lowest fees possible. - You're fine with handling international taxes yourself (you absolutely can). **Go with OurPay (MoR) if...** - You want product-, customer-, order- and subscription management via an intuitive and easy dashboard - You want to offer file downloads, license keys, Discord- and/or private GitHub repository invites with ease - with more built-in automations to come. - You prefer a more high-level API optimized for making monetization easier. We're only getting started here and have some big things coming - You want us to handle international taxes for you ### OurPay MoR **We take on the liability of international sales taxes globally for you. So you can focus on building your passion. Leaving billing infrastructure and sales tax headaches to us.** So how does OurPay offer a Merchant of Record (MoR) service and handle international sale taxes? All other Merchants of Record simply state they handle it internationally - don't worry about it. We do too. But we believe in transparency and don't want to scare customers into thinking it's impossible to manage it themselves. So below we'll share how exactly we go about doing this. #### International Sales Taxes Most countries, states and jurisdictions globally impose sales taxes on digital goods and services (VAT, GST, US Sales Tax etc). Regardless of whether the merchant (seller) is a resident there or not - they're doing business there. For example, a \$10/month subscription should cost \$12.5/month for a Swedish (25% VAT) consumer, but \$10/month for a Swedish business with VAT registration (reverse charge). Merchants are responsible for 1) capturing & 2) remitting sales taxes to the local tax authorities. What does that mean in our example? 1. **Capturing**. Charging the Swedish consumer \$12.5/month and saving \$2.5/month for the Swedish tax authorities. Stripe Tax is an excellent service to automate this and the one OurPay uses today. 2. **Remitting**. Filing & paying the captured sales taxes with the tax authorities on time. Stripe Tax does not do this, i.e the merchant is liable to register, file and pay taxes to local tax authorities. Many jurisdictions, however, don't require this until you reach a certain threshold in terms of sales volume. But others require registration even before the first sale - or after a very low threshold. In addition to having different rates and rules on which goods are taxable and whether they're deductable or not for business customers. For example, United Kingdom and EU countries require upfront registration for international companies, but Texas (United States) does not until you've sold for more than $500,000 🇺🇸🦅 In short: It's complex and hard. Even large and well-known businesses don't do it perfectly. Arguably, it's almost impossible and at least highly impracticle and expensive to comply perfectly upfront. Many companies even delay compliance as a calculated risk, i.e focus on validating & growing their business with the risk of paying back taxes + penalities later. **PSP (Stripe)** - ✅ Your volume alone is what counts towards international thresholds vs. the MoR platform, i.e customers might not need to pay sales taxes with you, but would via a MoR. - ✅ You can deduct inbound VAT against purchases your business does with VAT - ❌ You're liable for capturing & remitting international sales taxes - ❌ Stripe Tax is great to monitor & automate capturing, but registration and remittance is up to you. **MoR (OurPay)** - ✅ We are liable for all of the above as your reseller, i.e we have to worry about it vs. you. - ✅ Offer EU VAT for B2B sales (expected and desired within EU for businesses) without having to register, capture and remit it yourself. - ❌ Sales taxes would be added for more customers vs. with you selling directly - ❌ You cannot leverage inbound VAT towards VAT expense deductions yourself Merchants of Record (MoR) handles sales taxes, e.g US Sales Tax, EU VAT, Canadian GST etc. **However, you're always responsible for your own income/revenue tax** in your country of residency. #### OurPay Coverage **We support global payments and take on the liability for international sales taxes on your behalf.** OurPay is registered in jurisdictions around the world and works with global accounting firms to monitor sales volumes, register in new markets as thresholds are reached, and handle filings and remittance on an ongoing basis. You don't need to track where you owe taxes, or when — that's our job. **Want to do this yourself?** Selling a lot and want to handle this yourself, i.e worth the ongoing costs? Feel free to reach out and we'd be happy to introduce you to our contacts at the accounting firms we use. We consider MoR a key value-add to OurPay, but not the sole reason for OurPay to exist. Our ambition is to be the easiest way to monetize for developers. However, we're never going to be the right solution for all use cases. But we'll always salute and help anyone who ships software - regardless of billing platform. ## Frequently Asked Questions OSS VAT numbers use the `EU` prefix instead of country-specific prefixes (like `IE` for Ireland). Some accounting software predates the OSS program or lacks support for this format. You can manually enter the number if your software allows overriding validation, or contact your software vendor to request OSS number support. # supported-countries Source: https://docs.ourpay.dev/merchant-of-record/supported-countries ### Payments & Merchant of Record We support payments globally except from countries with US sanctions (Cuba, Russia, Iran, North Korea, and Syria). As your Merchant of Record (MoR) we take on the [liability for international sales taxes](/merchant-of-record/introduction). ### Payouts OurPay uses Stripe Connect Express to issue payouts to residents or businesses in any of the countries below. See [Payout Accounts](/features/finance/accounts) for how to connect one. **FAQ: Stripe isn't available in my country. Can I still use OurPay?** As the Merchant of Record, OurPay takes care of charging customers, so Stripe Payments doesn't need to be available in your country. All you need is to be in a country supported by Stripe Connect Express to receive payouts. Stripe Connect Express country coverage is much broader, and includes everywhere listed below. * 🇦🇱 Albania * 🇩🇿 Algeria * 🇦🇴 Angola * 🇦🇬 Antigua and Barbuda * 🇦🇷 Argentina * 🇦🇲 Armenia * 🇦🇺 Australia * 🇦🇹 Austria * 🇦🇿 Azerbaijan * 🇧🇸 Bahamas * 🇧🇭 Bahrain * 🇧🇩 Bangladesh * 🇧🇪 Belgium * 🇧🇯 Benin * 🇧🇹 Bhutan * 🇧🇴 Bolivia * 🇧🇦 Bosnia and Herzegovina * 🇧🇼 Botswana * 🇧🇳 Brunei * 🇧🇬 Bulgaria * 🇰🇭 Cambodia * 🇨🇦 Canada * 🇨🇱 Chile * 🇨🇴 Colombia * 🇨🇷 Costa Rica * 🇭🇷 Croatia * 🇨🇾 Cyprus * 🇨🇿 Czech Republic * 🇩🇰 Denmark * 🇩🇴 Dominican Republic * 🇪🇨 Ecuador * 🇪🇬 Egypt * 🇸🇻 El Salvador * 🇪🇪 Estonia * 🇪🇹 Ethiopia * 🇫🇮 Finland * 🇫🇷 France * 🇬🇦 Gabon * 🇬🇲 Gambia * 🇩🇪 Germany * 🇬🇭 Ghana * 🇬🇷 Greece * 🇬🇹 Guatemala * 🇬🇾 Guyana * 🇭🇰 Hong Kong * 🇭🇺 Hungary * 🇮🇸 Iceland * 🇮🇳 India * 🇮🇩 Indonesia * 🇮🇪 Ireland * 🇮🇱 Israel * 🇮🇹 Italy * 🇨🇮 Ivory Coast * 🇯🇲 Jamaica * 🇯🇵 Japan * 🇯🇴 Jordan * 🇰🇿 Kazakhstan * 🇰🇪 Kenya * 🇰🇼 Kuwait * 🇱🇦 Laos * 🇱🇻 Latvia * 🇱🇮 Liechtenstein * 🇱🇹 Lithuania * 🇱🇺 Luxembourg * 🇲🇴 Macao * 🇲🇬 Madagascar * 🇲🇾 Malaysia * 🇲🇹 Malta * 🇲🇺 Mauritius * 🇲🇽 Mexico * 🇲🇩 Moldova * 🇲🇨 Monaco * 🇲🇳 Mongolia * 🇲🇦 Morocco * 🇲🇿 Mozambique * 🇳🇦 Namibia * 🇳🇱 Netherlands * 🇳🇿 New Zealand * 🇳🇪 Niger * 🇳🇬 Nigeria * 🇲🇰 North Macedonia * 🇳🇴 Norway * 🇴🇲 Oman * 🇵🇰 Pakistan * 🇵🇦 Panama * 🇵🇾 Paraguay * 🇵🇪 Peru * 🇵🇭 Philippines * 🇵🇱 Poland * 🇵🇹 Portugal * 🇶🇦 Qatar * 🇷🇴 Romania * 🇷🇼 Rwanda * 🇱🇨 Saint Lucia * 🇸🇲 San Marino * 🇸🇦 Saudi Arabia * 🇸🇳 Senegal * 🇷🇸 Serbia * 🇸🇬 Singapore * 🇸🇰 Slovakia * 🇸🇮 Slovenia * 🇿🇦 South Africa * 🇰🇷 South Korea * 🇪🇸 Spain * 🇱🇰 Sri Lanka * 🇸🇪 Sweden * 🇨🇭 Switzerland * 🇹🇼 Taiwan * 🇹🇿 Tanzania * 🇹🇭 Thailand * 🇹🇹 Trinidad and Tobago * 🇹🇳 Tunisia * 🇹🇷 Turkey * 🇦🇪 United Arab Emirates * 🇬🇧 United Kingdom * 🇺🇸 United States * 🇺🇾 Uruguay * 🇺🇿 Uzbekistan * 🇻🇳 Vietnam ## Frequently Asked Questions Stripe Connect Express is a different product than the regular Stripe payments. Yes, any individual or company operating in our [supported countries](/merchant-of-record/supported-countries) can receive payouts from OurPay even if Stripe standalone is invite-only there. This is possible as OurPay is the Merchant of Record, all payments from customers are made to OurPay (US). [Stripe Connect Express](https://docs.stripe.com/connect/express-accounts) is then used to issue payouts, and is supported in more countries via cross-border transfer than Stripe Payments standalone. You might still see a warning in Stripe Connect Express that payments are invite-only, but don't worry. No direct sales are made directly to the Stripe Connect Express account. They're all made to OurPay (US) as a platform and the merchant of record. We only use the transfer and payout feature of Stripe Connect Express which is available in all of our [supported countries](/merchant-of-record/supported-countries). Yes, given that Stripe Connect Express supports individual as a business type in your region. To know which business type is supported in your country, follow steps as below: - Open required [verification information](https://docs.stripe.com/connect/required-verification-information#US+RS+express+recipient+individual+transfers) by Stripe to set up a business or personal account in your country. - Ensure `Platform Country` is set to `United States (US)`. - Ensure `Dashboard Type` is set to `express`. - Ensure `Service Agreement` is set to `recipient`. - Ensure `Capability` is set to `transfers`. - Select the correct `Account Country` relevant to you. - Click on the toggle for `Business Type` which will allow you know if individual, business, company or LLC/LLP is supported by Stripe Connect Express in that region. # Migrate Away from OurPay Source: https://docs.ourpay.dev/migrate-away You own your business, and that includes the freedom to leave. If you decide OurPay isn't the right fit, we'll help you move your customers and their saved payment methods to another payment provider so your subscribers experience as little disruption as possible. Because OurPay is a [Merchant of Record](/merchant-of-record/introduction), your customers' payment methods are securely vaulted with our payment processor (Stripe). Moving them to your own account is a coordinated, support-assisted process rather than a one-click export, so we can do it securely and in line with card network rules. **This process is handled by our team** Migrations are not self-serve today. To get started, email [support@ourpay.com](mailto:support@ourpay.com) with the account you want to migrate to, and we'll guide you through every step. ## What can be moved | Data | Moves to your new provider | Notes | | --- | --- | --- | | Customers | Yes | Name, email, and billing details. | | Saved payment methods | Yes | Easiest Stripe-to-Stripe. We can also move them to any PCI-compliant provider via Stripe's secure PAN export. | | Products, prices, discounts, benefits | No, recreate them | These live in your OurPay configuration and need to be set up again on your new provider. | | Active subscriptions | No, recreate them | Recreate them on your new provider following your existing billing cycle, then cancel on OurPay. See [Recreating your subscriptions](#recreating-your-subscriptions). | **Stripe to Stripe is the simplest path** If your new provider is Stripe, we transfer your customers and their saved payment methods directly between Stripe accounts. Moving to a different provider is also supported through Stripe's secure PAN export, which sends card data to any PCI-compliant processor. Both paths are handled by our team, so no card data ever passes through you and your customers don't need to re-enter their details. ## The holding period When you start a migration, we set your OurPay organization to an **offboarding** state. While offboarding: - Your existing subscriptions keep renewing until you cancel them, so customers are never cut off mid-transition. - New checkouts are disabled, since you're moving your business elsewhere. - **Payouts are paused for 120 days.** **Why payouts are held for 120 days** We hold your remaining balance for 120 days to cover any refunds and chargebacks that may still come in on payments processed while you were on OurPay. Disputes can be filed weeks or months after a purchase, and as Merchant of Record we're liable for them. Holding the balance protects both you and us from a negative balance after you've already withdrawn. After 120 days, your remaining balance is released and you can withdraw it. ## How the migration works Email [support@ourpay.com](mailto:support@ourpay.com) with the Stripe account (or other provider) you want to migrate to. We will confirm the details and set your organization to offboarding. The destination account must already exist and be under your control before we can begin the migration. We coordinate a secure transfer of your customers and their saved payment methods to your new account. Stripe-to-Stripe is a direct account-to-account copy. For other providers, we use Stripe's PAN export to send card data to your PCI-compliant processor. No card data passes through you, keeping the migration PCI-compliant. Set up your products, prices, discounts, and benefits on your new provider. These configurations live in OurPay and don't transfer automatically. Depending on your new provider, this can often be automated using their APIs or bulk import tools and we don't offer personalized exports of data. Recreate active subscriptions on your new provider, aligned to each customer's existing OurPay billing cycle, then cancel them on OurPay. See the warning below to avoid double billing. After the 120-day holding period, withdraw your released balance. See [Withdrawing your remaining balance](#withdrawing-your-remaining-balance). ## Recreating your subscriptions To keep billing seamless, recreate each active subscription on your new provider so its **first charge lands on the same date as the next OurPay renewal**. This way the customer is billed once per period, by exactly one provider. **Cancel your OurPay subscriptions once you've migrated them** Once a subscription is live on your new provider, cancel the matching OurPay subscription so OurPay stops generating renewals for it. If you leave both subscriptions active, your customer may be charged twice for the same billing period. Your existing subscriptions will continue to renew on OurPay until you cancel them. We recommend migrating customers to your new provider and canceling the corresponding OurPay subscriptions as soon as possible. The 120-day holding period begins after your final transaction on OurPay. Leaving subscriptions active will continue generating transactions and delay the release of your remaining balance. ## Withdrawing your remaining balance After the 120-day holding period ends, your remaining balance is released and you can withdraw it to your connected bank account, subject to the standard minimum payout threshold. If your balance is below the minimum for your country, reach out to support and we'll help you withdraw the remainder. ## Frequently asked questions The timeline depends on the payment provider you're migrating to. If you're migrating to your own Stripe account, migrations can typically be completed within a few business days. For other billing providers, migrations usually take several weeks due to coordination between multiple parties. No. OurPay does not charge a fee to migrate your customers or saved payment methods to another provider. They shouldn't experience an interruption. Subscriptions keep renewing on OurPay until you cancel them, and we transfer saved payment methods so they don't need to re-enter card details. The main visible change is who appears on their statement once you start billing from your new provider. Yes. Stripe-to-Stripe is the easiest path, but we can also move your saved payment methods to another provider through Stripe's secure PAN export, as long as your new processor is PCI-compliant and able to receive the data. Raw card data is subject to strict PCI and card network rules and is never handed to merchants directly. Payment methods are moved account-to-account through the processor's secure migration process, which is why this step is handled by our team. We continue to process refunds and handle any chargebacks on payments made while you were on OurPay, drawing from your held balance. This is the reason for the 120-day hold. Your historical orders, invoices, and reporting remain available in OurPay during the offboarding period. We retain transaction records for compliance purposes for at least five years after processing, even after your migration is complete. Email [support@ourpay.com](mailto:support@ourpay.com) with your destination account and we'll take it from there. # Migrate to OurPay Source: https://docs.ourpay.dev/migrate ## Lemon Squeezy Ready to make the jump from Lemon Squeezy to OurPay? Use the `ourpay-migrate` CLI tool to quickly and easily migrate your existing Lemon Squeezy products to OurPay. ### Getting Started ```bash Terminal npx ourpay-migrate@latest ``` ### Supported Migrations * Products & Variants * License Keys * Associated Files * Discount Codes * Customers This tool is not able to move **active** subscriptions from your Lemon Squeezy store. ### Open Source The code for the CLI is open source and available on GitHub [View Code on GitHub](https://github.com/sunnycodet/ourpay-migrate) # Support Source: https://docs.ourpay.dev/support ## Documentation Our documentation covers common questions, setup guides, and product details. We recommend using it as a first point of reference whenever you need guidance or support. If question isn't addressed in the documentation, feel free to reach out to us over email. ## Dashboard Feature requests & bugs can be reported through the "Feedback" button found in the sidebar, bottom left of your OurPay dashboard. ## Email You can reach us at [support@ourpay.com](mailto:support@ourpay.com). We typically respond within **24–48 hours on weekdays** for customers and active merchants, and prioritize questions related to ongoing billing, payouts or payments. Please note that during periods of higher request volume, response times may occasionally be longer. ## Merchant Account Reviews Account reviews are typically performed within 7 days, but can take up to 14 days to complete, depending on volume, weekends, and holidays. More information on our account reviewal process can be found in our [Account Reviews documentation](https://docs.ourpay.dev/merchant-of-record/account-reviews)