Creating payments

Forte's Payments API lets you charge your end-users with one-off Payments or recurring Subscriptions. Forte provides payment tracking, tax calculation, and simplified webhooks for you, and manages chargebacks automatically.

Your company is always the merchant of record for Payments on Forte. Fees are calculated on a per-customer basis depending on account size and industry, and range from 0% (no markup over Stripe) to 3% (high risk industries and small accounts).

First-time setup

To enable payments in a production (non-Sandbox) Project, complete onboarding from Compliance Registration.

Open Payments Setup →

Prerequisites

  • Your Forte Project owner has completed compliance registration.
  • Your project is in sandbox mode (charges run against Stripe Test mode) or live (real money). Sandbox routing is automatic — there is no separate flag on the Payment.
Live payments require approved compliance

For non-sandbox projects, your account's compliance registration must be APPROVED. Until then, Forte rejects createPayment and createPaymentPreview with PAYMENTS_REQUIRE_APPROVED_COMPLIANCE. Sandbox projects bypass this gate so you can build against test mode before finishing onboarding.

Open Compliance →

Payments and payment previews

Forte provides one endpoint for cart previews and a separate endpoint for checkout flows. Preview is read-only. Call it as the user changes items, quantities, addresses, or coupons. Create is the write step. Call it once when the user selects Checkout.

OperationPurposeSide effects
createPaymentPreviewCompute subtotal, tax, and total without creating a charge. Use this for cart and quote pages where the user iterates.None — nothing is persisted, no PaymentIntent is created.
createPaymentCreate a Forte Payment (and the underlying Stripe PaymentIntent), ready to confirm with Stripe Elements.Persists a PaymentObject, creates a Stripe PaymentIntent and Tax Calculation on your connected account, writes to the user's audit trail.

Each endpoint has two API surfaces. Use client-side routes when a signed-in user pays for themselves. Use server-side routes when your backend creates a payment for a user it authenticated another way. See API Surfaces for details.

Client-side (signed-in user session):

  • POST /api/v1/{projectId}/users/me/payments/preview
  • POST /api/v1/{projectId}/users/me/payments

Server-side (API key):

  • POST /api/v1/projects/{projectId}/users/{userId}/payments/preview
  • POST /api/v1/projects/{projectId}/users/{userId}/payments

Previewing a payment total with tax

typescript
import { ForteClient } from "@forteplatforms/sdk";
 
const forte = new ForteClient();
 
const preview = await forte.users.createPaymentPreview({
  projectId,
  createPaymentPreviewRequest: {
    currency: "usd",
    lineItems: [
      { description: "Pro plan", unitAmountCents: 2500, quantity: 1, taxCode: "txcd_10000000" },
    ],
    customerAddress: {
      line1: "354 Oyster Point Blvd",
      city: "South San Francisco",
      state: "CA",
      postalCode: "94080",
      country: "US",
    },
  },
});
 
const { subtotalCents, taxCents, amountCents } = preview;

The response includes subtotalCents, taxCents, amountCents, currency, and per-line taxAmountCents. If you omit customerAddress, no tax is computed and amountCents == subtotalCents.

taxCode is Stripe's product tax category — for example, txcd_99999999 (general goods) or txcd_10000000 (digital goods). Tax is always applied as EXCLUSIVE (added on top of the unit price).

Use txcd_92010001 for shipping charges. Forte finds every line with this code, sums them, and reports the total as shipping cost so tax calculates correctly — some regions tax shipping differently from goods.

Creating a payment (and Stripe PaymentIntent)

When the user clicks "Pay," call createPayment with the same payload, optionally adding a description and metadata. Payments are scoped to a specific user, so the userId in the URL must match the user you charge.

typescript
const result = await forte.users.createPayment({
  projectId,
  createPaymentRequest: {
    currency: "usd",
    description: "May 2026 invoice",
    lineItems: [
      { description: "Pro plan", unitAmountCents: 2500, quantity: 1, taxCode: "txcd_10000000" },
    ],
    customerAddress: { line1: "...", city: "...", state: "...", postalCode: "...", country: "US" },
  },
});
 
const { payment, stripeClientSecret, stripePublishableKey, stripeConnectedAccountId } = result;

Response:

json
{
  "payment": {
    "id": "pay_...",
    "state": "DRAFT",
    "subtotalCents": 2500,
    "taxCents": 219,
    "amountCents": 2719,
    "stripePaymentIntentId": "pi_...",
    "stripeTaxCalculationId": "taxcalc_..."
  },
  "stripeClientSecret": "pi_..._secret_...",
  "stripePublishableKey": "pk_...",
  "stripeConnectedAccountId": "acct_..."
}
Open Users →

Choosing which payment methods to accept

By default a Payment accepts credit and debit cards only. To offer more, pass supportedPaymentMethods on createPayment — the list of methods the customer may use when confirming:

ValueMethod
CREDIT_CARDCards. Apple Pay / Google Pay also surface automatically in the Payment Element on supported devices — they ride on the card rail.
ACH_DIRECT_DEBITACH direct debit from a United States bank account. Requires currency: "usd" — any other currency is rejected with PAYMENT_ACH_REQUIRES_USD.
typescript
const result = await forte.projects.createPayment({
  projectId,
  userId,
  createPaymentRequest: {
    currency: "usd",
    lineItems: [{ description: "Pro plan", unitAmountCents: 2500, quantity: 1 }],
    // Offer both a card and a US bank account on this payment:
    supportedPaymentMethods: ["CREDIT_CARD", "ACH_DIRECT_DEBIT"],
  },
});

Omit the field (or pass an empty list) to keep the card-only default. The Payment Element you mount with the returned stripeClientSecret shows exactly the methods you allowed. When you pin a specific saved method with paymentMethodId, supportedPaymentMethods is ignored — the method (and its type) is already chosen.

ACH has its own lifecycle

ACH direct debit settles asynchronously and has its own bank verification, payment states, and saved-method behavior. See ACH direct debit for details.

Confirming the payment in the browser with Stripe Elements

The three Stripe identifiers in the response (stripeClientSecret, stripePublishableKey, stripeConnectedAccountId) are everything Stripe.js needs to mount a Payment Element bound to your connected account. Pass them through to your frontend as-is.

ts
import { loadStripe } from "@stripe/stripe-js"
 
const stripe = await loadStripe(stripePublishableKey, { stripeAccount: stripeConnectedAccountId })
const elements = stripe.elements({ clientSecret: stripeClientSecret })
const paymentElement = elements.create("payment")
paymentElement.mount("#payment-element")
 
// On submit:
await stripe.confirmPayment({ elements, confirmParams: { return_url: "..." } })
Pass stripeAccount to loadStripe

The stripeAccount option on loadStripe (or per-call) is required so Stripe.js confirms the PaymentIntent on the right connected account. Direct charges live on the connected account, not on the platform — without this, confirmation fails with a "no such PaymentIntent" error.

Charging a saved method off-session (server-initiated)

The preceding flow is on-session — the customer is present and confirms in the browser. To charge a method already on file without the customer present (a server-initiated, one-off charge), pass offSession: true with the saved paymentMethodId. No client secret exists to confirm; Forte charges immediately.

typescript
const result = await forte.projects.createPayment({
  projectId,
  userId,
  createPaymentRequest: {
    currency: "usd",
    lineItems: [{ description: "Usage — May", unitAmountCents: 4200, quantity: 1 }],
    paymentMethodId: "pm_...", // a card or bank account the user previously saved
    offSession: true,
  },
});
  • Cards complete (or decline) synchronously. A decline returns PAYMENT_DECLINED.
  • ACH bank accounts can't fail synchronously — the Payment comes back PROCESSING and settles to COMPLETED or FAILED over the following business days. Listen for the PAYMENT_COMPLETED / PAYMENT_FAILED triggers to learn the outcome.

offSession: true requires paymentMethodId — without it the call returns PAYMENT_OFF_SESSION_METHOD_REQUIRED. To put a card or bank account on file for later, see saving a payment method.

Off-session charging is for one-off charges, not recurring billing

For recurring billing on a schedule, use Subscriptions instead of calling createPayment yourself on a timer.

Payment lifecycle and state mapping

The state on a Forte Payment reflects the underlying Stripe PaymentIntent's status:

Forte stateStripe status mapped from
DRAFTrequires_payment_method, requires_confirmation, requires_capture
PROCESSINGprocessing, requires_action (for example, an ACH bank account being verified — see ACH direct debit)
COMPLETEDsucceeded
CANCELLEDcanceled
FAILEDanything else

Forte updates this automatically through Stripe webhooks. When a Payment hits COMPLETED and had a stripeTaxCalculationId, Forte also finalizes a Stripe Tax Transaction so your tax bookkeeping stays correct.

Unconfirmed DRAFT payments expire after 24 hours

A Payment the customer never confirms is short-lived. If it stays in DRAFT for 24 hours, Forte automatically moves it to CANCELLED and cancels the underlying PaymentIntent, so it can no longer be confirmed or charged. If the customer comes back later, create a fresh payment rather than reusing the old stripeClientSecret.

Refunding a payment

refundPayment issues a full refund of a COMPLETED payment and transitions it to REFUNDED. Forte returns the entire charged amount, including any tax, to your end-customer.

Refunds are a backend-only operation

Refunds can only be initiated from your backend using an API key. No client-side (signed-in user) route exists — your end-users cannot refund their own payments from the browser. The only route is the server-side one:

POST /api/v1/projects/{projectId}/users/{userId}/payments/{paymentId}/refund

Only COMPLETED payments can be refunded. Refunding a payment in any other state returns PAYMENT_NOT_REFUNDABLE, and refunding one that is already refunded returns PAYMENT_ALREADY_REFUNDED. The call returns the updated Payment with state: "REFUNDED".

typescript
import { ForteClient } from "@forteplatforms/sdk";
 
const forte = new ForteClient({ apiKey: process.env.FORTE_API_KEY });
 
const payment = await forte.projects.refundPayment({
  projectId,
  userId,
  paymentId,
});
 
// payment.state === "REFUNDED"

You can also refund a payment from the Forte console: open the user's page, find the payment in the Payments section, and use the Refund action on any completed payment.

Payment triggers (webhooks to your services)

When a Payment changes state, Forte invokes a Payment Trigger (in practice, an HTTP POST call) to one of your Forte Services. Use this to kick off fulfillment work as a side effect of a payment — for example, issue licenses, email receipts, write to your own ledger, or notify downstream systems.

Forte delivers triggers over a private network to your Service.

Trigger events

EventFires when
PAYMENT_COMPLETEDPayment transitions to COMPLETED.
PAYMENT_FAILEDPayment transitions to FAILED. Most useful for off-session ACH charges, which can bounce (for example, insufficient funds) days after they returned PROCESSING — this is how your backend learns of the failure, since the customer isn't on-session to see it.
PAYMENT_REFUNDEDPayment transitions to REFUNDED — whether from your own refundPayment call (or the console Refund button), a refund issued directly from the Stripe dashboard, or a lost dispute.

Creating a payment trigger

Triggers are project-scoped. Create, update, and delete them from the Forte Project's settings page in the web console.

Open Services →

Receiving and processing trigger events

For each matching event, Forte sends a POST request to your target Service and URL path with the following headers and body:

Headers:

HeaderValue
Content-Typeapplication/json
X-Forte-Trusted1

Body:

json
{
  "userId": "usr_...",
  "paymentId": "pay_...",
  "paymentTime": "2026-05-10T18:42:11.123Z",
  "state": "COMPLETED"
}
  • userId — the Forte User the payment belongs to.
  • paymentId — the Forte Payment ID. GET it via the payments API if you need the full object.
  • paymentTime — ISO-8601 UTC timestamp of the state transition.
  • state — uppercase, one of "COMPLETED", "FAILED", or "REFUNDED".
Trust comes from the network, not a signature

Your code must check the X-Forte-Trusted header. Forte automatically strips it from any other request, so its presence guarantees the hook is valid and trusted.

Trigger retry policy

You can view any failed (non-HTTP 2xx) trigger invocations in the web console. Forte retries them as follows:

SettingValue
Total attempts5
Delay between attempts60 seconds
Per-request timeout30 seconds
Counts as successHTTP 2xx
Counts as failureAny non-2xx, timeout, or connection error

Forte abandons the trigger after 5 failed attempts. Contact Forte support if you need help replaying dropped events or accessing old data for abandoned triggers.

Best practices for trigger handlers

  • Return 2xx in under 30 seconds.
  • Make handling idempotent on (paymentId, state) so that retries are safe.

Testing trigger handlers locally

While you're building a handler, you can fire any of your project's triggers directly at your local dev server using the Forte CLI — same JSON body and X-Forte-Trusted: 1 header that production sends, no real payment state change required.

bash
forte payment-triggers test

The command walks you through picking a trigger, searching for a user, and choosing one of that user's recent payments. Once you're on the "Ready to fire" screen:

KeyAction
enterPOST the webhook to your local URL — press again to re-fire the same payload.
uSearch for a different user.
pPick a different payment for the same user.
tEdit the local URL (defaults to http://localhost:<your service's port><trigger path>).
bOpen the user's page in the Forte console — useful for creating a sample payment if the user doesn't have one yet.
qQuit.

Skip the picker by passing the trigger ID directly:

bash
forte payment-triggers test pmt_trigger_<id>
The user must have a real payment in a fireable state

The CLI fires using the actual userId and paymentId of a real payment so your handler can look it up through the payments API just like in production. If the selected user has no COMPLETED (or REFUNDED) payments yet, the CLI offers to open the Forte console, where you can create a sample payment in one click (sandbox projects only).

Sandbox mode vs. live payments

If your project has sandbox mode enabled, every Payment runs against your sandbox Stripe Connect account in Stripe Test mode — the publishable key returned is a pk_test_.... Switch the project to live (or use a separate live project) to charge real cards.

Open Payments Setup →

Audit trail and logging

Forte writes payments and state events to the audit trail for the user they belong to. Payment previews are not logged.

Search

Search documentation and console pages