For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
SupportDashboard
Getting StartedAPI ReferenceRoadmapBlog
Getting StartedAPI ReferenceRoadmapBlog
  • Getting Started
    • Overview
    • What is Schematic?
    • Concepts
  • Using Schematic
    • Who Uses Schematic
  • Quickstart
    • Quickstart
    • Account Setup
    • Entitling a Feature
    • Tracking Usage
    • Components
    • Identifying Users
    • Setup the SDK
  • Using Feature Flags
    • Overview
    • Flags
    • Features
    • Tracking Feature Usage
    • Company Overrides
    • Feature Types
  • Building Your Catalog
    • Overview
    • Plans
    • Managing Company Plans
    • Configuring the Catalog
    • Add Ons
    • Trials
  • AI Tooling
    • For Developers
  • Setting Up Billing
    • Overview
    • Usage Based Billing Models
    • Seat Based Billing Models
    • Credit burndown
  • Using UI Components
    • Overview
      • Overview
      • Create a Component
      • Add to Your App
      • Element Library
      • Advanced Usage
      • Building Your Own Portal
  • Developer Resources
    • Concepts
    • Key Management
    • Environments
    • Entity Relationship Diagram
  • Production Readiness
    • Availability
    • Observability & Support
    • Security
  • Integrations
    • Segment Integration
    • Clerk Integration
    • WorkOS Integration
    • Salesforce Integration
    • Hubspot Integration
  • Playbooks
    • Overview
    • Creating a metered feature
    • Backfills and usage corrections
    • Rolling out beta functionality with Flags
    • Handling customer exceptions and feature trials
    • Automatically provision customers using Stripe
    • Build a slack webhook
LogoLogo
SupportDashboard
On this page
  • Authentication
  • Endpoints
  • Fetching portal data
  • Displaying the current plan
  • Displaying feature entitlements
  • Displaying add-ons
  • Displaying credits
  • Displaying payment methods
  • Listing invoices
  • Customer balance
  • Updating payment methods
  • Cancelling a subscription
  • Resolving prices
  • Tiered pricing
  • Things to watch out for
Using UI ComponentsCustomer Portal

Building Your Own Customer Portal

Was this page helpful?
Previous

Standalone Components

Next
Built with

Schematic’s customer portal component is built to handle the full range of billing and entitlement scenarios that Schematic supports, from simple flat-rate plans to complex tiered pricing with usage-based billing, credits, and multi-currency support. The component is designed to be customized visually and dropped in to save your team time immediately, while automatically adapting as your pricing and packaging evolves.

That said, if you’d rather build your own portal to fully control the experience, this guide covers the APIs you’ll need and the functionality you’ll need to build to fully replace Schematic’s customer portal component.

This guide only covers building your own customer portal. You will still need to use Schematic’s Checkout Component to handle checkout. See our launching checkout programatically guide for more info

Authentication

Every endpoint requires a temporary access token, which your backend creates via the Schematic API (POST /temporary-access-tokens). The token is scoped to a specific company and is short-lived. Pass it to your frontend and include it on every request:

X-Schematic-Api-Key: token_abc12345678901234567890123456

Endpoints

EndpointMethodPurpose
/components/hydrateGETGet full billing state for a company
/components/invoicesGETList invoices (paginated)
/checkout/balanceGETCustomer credit/debit balance
/checkout/unsubscribeDELETECancel a subscription
/components/setup-intentPOSTCreate a Stripe SetupIntent for payment method updates
/checkout/paymentmethod/updatePOSTUpdate the default payment method
/checkout/paymentmethod/{id}DELETERemove a payment method

Fetching portal data

GET /components/hydrate

This returns the full billing state for the authenticated company in a single call. Key fields in the response:

active_plans
array

Plans the company is currently on. Each includes name, description, pricing, entitlements, and features. The entry with current: true is their primary plan.

active_add_ons
array

Active add-ons (same shape as plans).

feature_usage.features
array

Per-feature usage data: access status, current usage, allocation limits, pricing, credit info, and metric reset dates.

subscription
object

Current subscription: billing interval (month or year), currency, status, total price, trial end date, cancellation status, payment method, and active discounts.

company
object

Company info: name, all payment methods on file, default payment method, billing credit balances.

credit_grants
array

Active Schematic credit grants (usage-based units defined in your plan, not Stripe account balance) with quantities (total, used, remaining), source, renewal info, and expiration dates.

credit_bundles
array

Credit bundles available for purchase.

scheduled_downgrade
object

If a downgrade is pending: from/to plan names, effective date, and price change.

upcoming_invoice
object

Preview of the next invoice (amount due, due date).

default_plan
object

The free/default plan for companies without a subscription.

post_trial_plan
object

Which plan the company will land on after their trial ends.

trial_payment_method_required
boolean

Whether a payment method is needed to start a trial.

prevent_self_service_downgrade
boolean

If true, block self-service downgrade. Show prevent_self_service_downgrade_url to direct users to contact sales.

You can ignore the component, display_settings, checkout_settings, and deprecated show_* fields. Those control the behavior of Schematic’s hosted components and aren’t relevant when building your own UI.

Displaying the current plan

Find the entry in active_plans where current is true:

Plan name: active_plans[].name
Description: active_plans[].description
Billing period: subscription.interval ("month" or "year")
Currency: subscription.currency

To display the price, resolve it by period and currency (see Resolving prices below).

If subscription.trial_end is set and in the future, the company is on a trial. Show the trial end date and reference post_trial_plan for what happens next.

If scheduled_downgrade is present, show a notice: “Your plan will change from {from_plan_name} to {to_plan_name} on {effective_after}.”

Displaying feature entitlements

The feature_usage.features array contains every feature the company has access to. Each entry includes:

  • feature.name (and feature.plural_name for plural context)
  • access (boolean, whether the feature is currently available)
  • entitlement_type ("boolean" for on/off flags, "numeric" for metered features)

Boolean features just need a checkmark when access is true.

Numeric/metered features vary based on price_behavior:

price_behaviorHow to display
(none)Standard allocation. Show usage / allocation as a progress bar. Null allocation means unlimited.
pay_in_advancePre-purchased quantity. Show usage / allocation.
pay_as_you_goNo allocation. Show current usage and per-unit cost.
overageIncluded usage up to soft_limit, then per-unit overage charges. Show usage / soft_limit. Overage = max(0, usage - soft_limit).
tierPrice varies by usage bracket. Show current usage and which tier it falls in. See Tiered pricing for cost calculation.
credit_burndownUsage measured in credits. Show credit_used / (credit_remaining + credit_used). Effective limit = credit_total / credit_consumption_rate.

Pre-computed helpers. The API returns several fields that simplify display: effective_limit (the resolved cap), effective_price (per-unit price for the current scenario), percent_used (0-100+), overuse (amount above soft limit), is_unlimited, and has_valid_allocation. These cover most display needs, though tiered pricing breakdowns require additional calculation on your end.

Reset periods. If period is set (current_day, current_week, current_month, current_year, or billing), usage resets on that cadence. metric_reset_at is the next reset timestamp.

Displaying add-ons

Active add-ons are in active_add_ons. Each has a name, description, and charge_type ("recurring" or "one_time"). Resolve the price the same way as plans (see Resolving prices).

Displaying credits

The credit_grants array represents Schematic-managed credits (usage-based units defined in your plan), not the customer’s Stripe account balance. For the Stripe account balance, see Customer balance.

Each entry includes:

  • credit_name, singular_name, plural_name for display
  • quantity (total granted), quantity_used, quantity_remaining
  • source_label (e.g. “Pro Plan”, “Purchased”)
  • grant_reason ("plan_grant", "purchase", "promotion", etc.)
  • renewal_enabled and renewal_period for auto-renewing grants
  • expires_at (null if no expiry)

Group credit grants by billing_credit_id to show per-credit-type balances.

Displaying payment methods

The company.default_payment_method object (and company.payment_methods array for all methods) includes:

  • card_brand ("visa", "mastercard", etc.), card_last4, card_exp_month, card_exp_year for cards
  • bank_name, account_last4, account_name for bank accounts
  • payment_method_type ("card", "us_bank_account", etc.)

Listing invoices

GET /components/invoices?limit=20&offset=0

Returns an array of invoices, paginated with limit and offset. Each invoice includes amount_due, amount_paid, amount_remaining, currency, status, due_date, created_at, url (link to the Stripe hosted invoice page), and subtotal.

You’ll need to filter the results. Exclude invoices where:

  • status is void, draft, or uncollectible
  • amount_due is 0
  • external_id starts with upcoming_
  • The invoice is unpaid and due_date is in the future

Sort by due_date (falling back to created_at) descending. Negative amount_due values represent credits.

Customer balance

GET /checkout/balance

Returns the customer’s Stripe account balance as a monetary amount in their billing currency (e.g. dollars for USD), representing balance adjustments from prorations, refunds, or other Stripe-level credits.

This is separate from credit grants used in credit based billing/entitlements, which are usage-based units defined in Schematic.

Updating payment methods

Your portal may need to let users update their payment method. This requires Stripe components to handle custom card data. Schematic never directly handles PCI data, relying instead on Stripe’s Payment Method ids:

  1. Call POST /components/setup-intent to get a Stripe SetupIntent. The response includes setup_intent_client_secret, publishable_key, and optionally account_id.

  2. Load Stripe.js with the publishable key. If account_id is present, you must pass it as stripeAccount when initializing Stripe. This is required when your Schematic account uses Stripe Connect.

1import { loadStripe } from "@stripe/stripe-js";
2
3const stripeOptions = {};
4if (setupIntent.account_id) {
5 stripeOptions.stripeAccount = setupIntent.account_id;
6}
7const stripe = await loadStripe(setupIntent.publishable_key, stripeOptions);
  1. Mount a Stripe PaymentElement and confirm the setup:
1const { setupIntent, error } = await stripe.confirmSetup({
2 elements,
3 confirmParams: {
4 payment_method_data: {
5 billing_details: { email: customerEmail }
6 },
7 return_url: window.location.href,
8 },
9 redirect: "if_required",
10});
  1. Save the payment method:
POST /checkout/paymentmethod/update
Body: { "payment_method_id": "<setupIntent.payment_method>" }

Cancelling a subscription

DELETE /checkout/unsubscribe

No request body. The cancellation timing (immediate vs. end of period) is determined by your account’s configuration in Schematic.

Resolving prices

Plans and add-ons can have prices in multiple currencies and for multiple billing periods (monthly, yearly). To resolve the correct price:

  1. Determine the billing period ("month" or "year") and currency
  2. Check the item’s currency_prices array for a matching currency (case-insensitive)
  3. From that currency entry, pick monthly_price or yearly_price based on the period
  4. If no currency match, fall back to the top-level monthly_price or yearly_price

For add-ons, also check charge_type: use one_time_price for one-time charges instead of the period-based prices.

Price formatting: The price field is in smallest currency units (e.g. cents). Use price_decimal (a string) when available for fractional precision. Use Intl.NumberFormat with the currency code for proper locale formatting:

1new Intl.NumberFormat("en-US", {
2 style: "currency",
3 currency: "usd",
4}).format(amountInCents / 100);

Tiered pricing

Some usage-based features use tiered pricing. The price_tier array on the price object defines the brackets. There are two modes:

Volume (tiers_mode is "volume"): find the tier the total quantity falls into, then multiply the full quantity by that tier’s per_unit_price, plus the tier’s flat_amount.

quantity = 150
tiers = [{up_to: 100, per_unit_price: 10}, {up_to: null, per_unit_price: 5}]
result: 150 * 5 = 750 (all units priced at the tier they land in)

Graduated (default): accumulate cost across each tier bracket.

quantity = 150
tiers = [{up_to: 100, per_unit_price: 10}, {up_to: null, per_unit_price: 5}]
result: (100 * 10) + (50 * 5) = 1250 (each bracket priced independently)

Each tier can also include a flat_amount that’s added once when that tier is entered. Use per_unit_price_decimal over per_unit_price when available.

Things to watch out for

Currency is locked to the subscription. Once a company has an active subscription, they can’t change currencies without cancelling and re-subscribing.

Invoice filtering is client-side. The invoices endpoint returns all invoices including drafts, voids, and zero-amount entries. You need to filter these yourself (see the invoices section above).

Price fields have two formats. price is an integer in the smallest currency unit (cents). price_decimal is a string with full decimal precision. Prefer price_decimal when available.

Credit grants can have multiple sources. A company might have plan-included credits, purchased credits, and promotional credits all at the same time. Group by billing_credit_id to show per-credit-type balances.

Stripe Connect requires stripeAccount. If the setup intent response includes an account_id, you must pass it as stripeAccount when loading Stripe.js. Without it, Stripe operations will fail.

Use active_plans for plan data, not company.plan. Both exist in the hydrate response, but active_plans is the richer version with full pricing and entitlement data.