API reference Stripe
Stripe API ("Pay over time")
Buy now, pay later (Affirm, Klarna, Afterpay) for US companies, through Stripe Connect. TalusPay remains the processor for cards, ACH and tap-to-pay (payments-endpoints.md); Stripe is used only for "Pay over time".
How it works:
- An admin sets the company up inside the app. The app calls Create Account Session and shows Stripe's onboarding screen natively with Stripe's iOS / Android Connect SDK. Stripe collects the business, identity and bank details.
- Stripe reviews the business. Our webhook tracks the outcome, emails the company when it is approved (or rejected / restricted), and the app reads the status from Get Account or the session bootstrap (
stripeAccount). - Once approved, the public document viewer shows a Pay over time button on payable invoices and estimate deposits. The customer is redirected to Stripe's hosted Checkout page. The payment is recorded by our webhook, which marks the document paid exactly like a TalusPay payment.
Every company gets its own Stripe account (Connect direct charges): the company is the merchant of record and Stripe bills it the processing fees directly (deducted before payout). Tradesmen Technologies takes no platform fee and carries no loss liability.
Companies never use a Stripe-hosted dashboard (dashboard: none). Every Stripe screen they see is one of Stripe's embedded Connect components:
- In the app (native Stripe Connect SDK for iOS / Android, fed by Create Account Session): onboarding, payments (with refunds and disputes) and payouts. Payments and payouts are Stripe preview features on mobile.
- On our web page
https://{web client domain}/stripe-account/(opened in the phone's browser via Create Web Link): account details and requirement alerts, which Stripe offers on the web only, plus setup, payouts, payments and tax documents. Stripe's components cannot run inside an in-app web view, so this page must open in the system browser.
Stripe requires the onboarding, account-management and notification-banner components for accounts like these; Stripe may also email companies directly about compliance (brand those emails in the Connect settings).
Base Path
- Production:
https://api.tradesmentechnologies.com/prod
Authentication
- Create Account Session and Create Web Link: Bearer token,
employeeTypeADMIN(403 otherwise). - Create Web Session: public (no token); gated by the one-hour code from Create Web Link.
- Get Account: Bearer token, any employee.
- Create Checkout Session: public (no token); gated by the document code like the other public document endpoints.
- Webhooks: called by Stripe only; authenticated by the
Stripe-Signatureheader.
Account status
status is derived by the server from the Stripe account; clients never set it.
| Value | Meaning |
|---|---|
PENDING |
Account created; the company still has onboarding information to provide (see requirementsDue). |
IN_REVIEW |
Onboarding submitted; Stripe is verifying the business. |
ACTIVE |
The account can take payments. "Pay over time" is offered once at least one method in payLaterMethods is active. |
RESTRICTED |
Was active; Stripe paused payments until the company provides more information (see requirementsDue). |
REJECTED |
Stripe will not enable payments for this business. Stripe's onboarding and account components show the reason. |
DISCONNECTED |
The Stripe account was closed. Starting onboarding again creates a new account. |
Account object
Returned as stripeAccount by every endpoint below and by the session bootstrap (accounts-endpoints.md). null when the company never started onboarding. Stripe ids are never exposed.
{
"status": "ACTIVE",
"payLaterAvailable": true,
"payLaterMethods": ["affirm", "klarna"],
"payLaterCapabilities": {
"affirm": "active",
"afterpay_clearpay": "pending",
"klarna": "active"
},
"requirementsDue": [],
"approvedAt": "2026-09-28T17:04:11.000Z",
"updatedAt": "2026-09-28T17:04:11.000Z"
}
payLaterAvailable:truewhenstatusisACTIVEand at least one method is active. This is exactly when the doc viewer shows the button.payLaterMethods: the Checkout method types the company can take right now, in the orderaffirm,afterpay_clearpay,klarna,zip,sunbit.payLaterCapabilities: method type → Stripe capability status, one ofactive,pending,restricted,unsupported,unrequested. Affirm, Afterpay and Klarna are requested at onboarding and always listed.zipandsunbitappear only when they are enabled for the company's account (they are not requested). A method can be refused for an individual business even when the account is active.requirementsDue: human-readable descriptions (from Stripe) of what the company must still provide. Empty when nothing is due.approvedAt: when the account first becameACTIVE, ornull.
Endpoints
Create Account Session
For the app's native Stripe Connect SDK. Creates the company's Stripe account on first use, then returns a fresh Account Session. Call it whenever the SDK asks for a client secret (sessions are short-lived); do not store the secret.
- URL:
/payments/stripe/account-session - Method: POST
- Auth: Bearer token,
ADMIN - Request Body: none
Response
201 Created when the Stripe account was created by this call, 200 OK otherwise.
{
"clientSecret": "accs_secret__xxxxxxxx",
"expiresAt": 1790000000,
"publishableKey": "pk_live_xxxxxxxx",
"stripeAccount": {
"status": "PENDING",
"payLaterAvailable": false,
"payLaterMethods": [],
"payLaterCapabilities": {
"affirm": "pending",
"afterpay_clearpay": "pending",
"klarna": "pending"
},
"requirementsDue": ["Business details", "Bank account"],
"approvedAt": null,
"updatedAt": "2026-09-29T17:00:00.000Z"
}
}
clientSecret/expiresAt(epoch seconds): the Account Session for Stripe's embedded components.publishableKey: the environment's Stripe publishable key (pk_live_…) to initialize the SDK with.- Enabled components:
account_onboarding,account_management,notification_banner,payments(refund and dispute management on, capture off),payouts(standard payouts and payout schedule on, instant payouts off),documents. Refunds made in Stripe's payments component are recorded by thecharge.refundedwebhook. - After the onboarding component exits, call Get Account to read the new status.
Errors
401— missing or invalid token.403— the caller is not anADMIN; or onboarding is not open yet in this environment ({"message": "Pay over time is not available yet"}, see Rollout).503— Stripe is not configured for this environment.500— unexpected error.
Create Web Link
The URL of the company's "Pay over time" web page, for the components Stripe offers on the web only (account details, requirement alerts). Open it in the phone's browser (not an in-app web view).
- URL:
/payments/stripe/web-link - Method: POST
- Auth: Bearer token,
ADMIN - Request Body: none, or
{"view": "account"}to open the page in its account-only layout (account details and requirement alerts, no section tabs). The app sends it on iOS/Android, where setup, payments and payouts are native screens; without it the page shows every section.
Response
201 Created when the Stripe account was created by this call, 200 OK otherwise.
{
"url": "https://client.tradesmentechnologies.com/stripe-account/#company=company-123&code=Xk3...&env=prod",
"expiresAt": 1790003600,
"stripeAccount": {
"status": "ACTIVE",
"payLaterAvailable": true,
"payLaterMethods": ["affirm", "klarna"],
"payLaterCapabilities": {
"affirm": "active",
"afterpay_clearpay": "pending",
"klarna": "active"
},
"requirementsDue": [],
"approvedAt": "2026-09-29T18:00:00.000Z",
"updatedAt": "2026-09-29T18:00:00.000Z"
}
}
- The link carries a random access code in the URL fragment (browsers never send the fragment to a server; the page removes it from the address bar). The code is valid for one hour (
expiresAt, epoch seconds) and only its hash is stored. Requesting a new link replaces the previous code, so an older open page stops refreshing. With{"view": "account"}the fragment also carriesview=account.
Errors
Same as Create Account Session, plus 400 for a malformed JSON body.
Create Web Session
Called by the /stripe-account/ page, on load and whenever Stripe's components need a fresh session. Not called by the app.
- URL:
/payments/stripe/web-session - Method: POST
- Auth: none (public; gated by the web link's code)
Request Body
{
"companyId": "company-123",
"code": "Xk3..."
}
Response
{
"clientSecret": "accs_secret__xxxxxxxx",
"expiresAt": 1790000000,
"publishableKey": "pk_live_xxxxxxxx",
"companyName": "Acme HVAC",
"brand": "tradesmen",
"stripeAccount": {
"status": "ACTIVE",
"payLaterAvailable": true,
"payLaterMethods": ["affirm"],
"payLaterCapabilities": {
"affirm": "active",
"afterpay_clearpay": "pending",
"klarna": "pending"
},
"requirementsDue": [],
"approvedAt": "2026-09-29T18:00:00.000Z",
"updatedAt": "2026-09-29T18:00:00.000Z"
}
}
Errors
400— malformed JSON or missingcompanyId/code.403— wrong, expired or replaced code ({"message": "This link has expired. Open Pay over time in the app again."}).503— Stripe is not configured for this environment.500— unexpected error.
companyName is null when the company has no name on file; brand defaults to tradesmen.
Web page
/stripe-account/ shows the requirement alerts (notification banner) above tabs for Setup (onboarding), Account details, Payouts, Payments and Tax documents. It opens on Setup while the account is not ACTIVE or has requirements due, otherwise on Account details. With view=account in the URL fragment (from the {"view": "account"} link) the tabs are hidden and only Account details is shown under the alerts. Stripe's embedded components may open a Stripe sign-in popup for sensitive actions (such as changing the payout bank account); that popup is Stripe's and cannot be removed. The page loads Stripe's connect.js from connect-js.stripe.com through the vendored @stripe/connect-js loader; its Content Security Policy allows connect-js.stripe.com and js.stripe.com on this path only.
Get Account
Returns the company's "Pay over time" status. When an account exists it is refreshed from Stripe first, so the app sees the result of onboarding as soon as the onboarding component exits; if Stripe cannot be reached, the last stored status is returned.
- URL:
/payments/stripe/account - Method: GET
- Auth: Bearer token, any employee
Response
{
"stripeAccount": null,
"onboardingEnabled": true
}
stripeAccount: the account object, ornullwhen onboarding was never started.onboardingEnabled: whether Create Account Session and Create Web Link are open in this environment. The app should hide the setup entry point when it isfalseandstripeAccountisnull.
Errors
401— missing or invalid token.500— unexpected error.
Create Checkout Session
Starts a "Pay over time" payment from the public document viewer and returns the Stripe-hosted Checkout URL to redirect the customer to.
- URL:
/payments/stripe/checkout-session - Method: POST
- Auth: none (public; gated by
documentCode)
Request Body
{
"companyId": "company-123",
"documentId": "doc-456",
"documentCode": "a1b2c3",
"tipPercentage": 15
}
companyId,documentId,documentCode(required): the values from the document link.tipPercentage(optional, invoices only): must be one of the company'stipPercentages. The tip is that percentage of the amount due.
The amount is always computed by the server from the stored document: an estimate's deposit, or an invoice's amount due with its deposit, partial-billing percent and additional charges applied (the same figure the viewer shows), plus the tip, plus the company's payment-time fees — every company charge with appliesTo: "PAYMENT" (see Update Company in accounts-endpoints.md), each as its percentage of that amount due; a "Pay over time" payment pays them all, like a card payment (the ACH waiver does not apply). Any amount sent by the client is ignored. When the session is paid, the webhook records the fees on the payment row and the document (paymentFeeAmount / paymentFees, see payments-endpoints.md), where they stay.
Response
{
"url": "https://checkout.stripe.com/c/pay/cs_test_xxx",
"sessionId": "cs_test_xxx",
"amount": 345.00
}
amount: the total the customer will be asked to pay, in dollars (amount due + tip + payment-time fees).- After paying, Stripe returns the customer to the document link with
&stripe=successappended; after cancelling, with&stripe=cancelled. The viewer waits for the document to readPAID: the payment is recorded only by the webhook, never by the return page. - Checkout offers only the "Pay over time" methods the company has enabled, and hides any that the amount is not eligible for. Cards and bank payments are excluded (they stay on TalusPay).
Errors
400— missing fields or malformed JSON; the company has no active Stripe account; the document is neither an invoice nor an estimate ("This document cannot be paid online"); the estimate has no deposit or can no longer be paid; the tip is not one of the company's options (or was sent for an estimate); the amount is under $0.50; or Stripe declined to open a session (for example, no method is eligible for the amount). The message for an unavailable payment is"Pay over time is not available for this payment. Please choose another way to pay.".403— wrong document code, unknown document, or expired document.409— the invoice or deposit is already paid.503— Stripe is not configured for this environment.500— unexpected error.
Refunds, voids and captures
"Pay over time" payments appear in GET /payments/transactions like any other payment (see the payment row fields in payments-endpoints.md). Their transactionId is a Stripe PaymentIntent id (pi_…).
- Refund: use the existing
POST /payments/transaction/{transactionId}/refundwith thepi_…id. The request body (amountin dollars, omitted for the full remaining amount) and response shape are unchanged;responseCodeisnullandresponseTextis the Stripe refund status (succeeded,pending, …). Refunds issued in Stripe's payments component (app or web page) are recorded automatically too. - Void / Capture: not supported. Pay over time payments are captured immediately; calling void or capture with a
pi_…id returns400.
Webhooks (Stripe → backend)
Not called by clients. Two routes, each registered in the Stripe Dashboard with its own signing secret.
| Route | Stripe registration | Events |
|---|---|---|
POST /payments/webhook/stripe |
Connect webhook endpoint, "events from connected accounts" (snapshot payloads) | checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, charge.refunded, charge.dispute.created, charge.dispute.closed |
POST /payments/webhook/stripe/accounts |
Event destination for Accounts v2 events (thin payloads) | v2.core.account.updated, v2.core.account.closed, v2.core.account[requirements].updated, v2.core.account[configuration.merchant].updated, v2.core.account[configuration.merchant].capability_status_updated |
- Signatures are verified on every request. An invalid signature returns
400; a non-live-mode event returns400. A route whose signing secret is not configured returns500so Stripe keeps retrying. The Connect route also accepts a v1account.updatedsnapshot event (refreshes the account like the thin events) if the endpoint is ever subscribed to it; every other event type is acknowledged and ignored. - Processing is idempotent (Stripe may deliver an event more than once); a processing failure returns
500and Stripe retries. charge.dispute.createdemails the company with the disputed amount; the dispute is answered in Stripe's payments component (app or web page).
Rollout
- Onboarding is open in both environments since the production launch on 2026-10-02. If new sign-ups are ever paused, status, checkout and webhooks keep working for companies that are already set up.