Skip to content

Stripe Integration

DayZero integrates with Stripe Connect for multi-tenant payment processing. Each business connects its own Stripe account so customers can pay DayZero invoices online, and the resulting payments, fees and payouts are booked automatically.

The /api/v1/stripe/* endpoints below work with an API token but are not part of the public API Reference — they back DayZero's own Integrations page and may change. The results show up through the public Invoices and Journal Entries endpoints, which is where integrations should read from.

Connect Stripe

The simplest path is the Integrations page in the DayZero dashboard, which starts the Stripe Connect OAuth flow. Programmatically, request the connect URL:

bash
curl "https://api.ondayzero.com/api/v1/stripe/connect-url" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

This call needs admin permission. The response's oauth_url sends the user to Stripe to authorize (Stripe Connect OAuth, read_write scope); after the callback the business's stripe_account_id (visible on GET /api/v1/businesses/{business_id}) is stored automatically. If the business already has a connected account, no URL is generated and the response has status: already_connected with the existing stripe_account_id. The connect URL is bound to the browser that requested it via a cookie, so it must be opened in the same browser session — it is not something to hand to a headless client.

Check the connection with:

bash
curl "https://api.ondayzero.com/api/v1/stripe/account-status" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

data.status is connected, not_connected or connected_but_error (with error / message explaining why, for example access revoked or a transient Stripe error), alongside the stripe_account_id. When connected, account_details carries the account type, country, email, details_submitted, charges_enabled and payouts_enabled. If the account exists but Stripe onboarding was never finished, GET /api/v1/stripe/onboarding-link returns a link to complete it. POST /api/v1/stripe/dashboard-link returns a link into the business's own Stripe dashboard in url (a one-time login link for Express accounts, with login_link: true; the plain dashboard URL for Standard accounts, where the owner signs in with their own Stripe credentials).

Invoice Payments

When an invoice is finalized for a business with Stripe connected, DayZero mirrors it to Stripe and the invoice gains stripe_invoice_id and hosted_invoice_url — Stripe's hosted payment page. Customers pay there; the confirmation arrives via webhook and automatically:

  1. Updates the invoice status to stripe_paid, sets total_paid and stamps the real payment date from Stripe
  2. Creates the payment journal entry, net of fees
  3. Records the Stripe processing fee (and any platform fee) as an expense

Two cases book no Stripe journal entry: an invoice that Stripe settled without a card or bank charge (customer balance or bank-transfer funding) still becomes stripe_paid, and an invoice that Stripe settled entirely from the customer's credit balance (zero amount collected) becomes stripe_paid, with the credit recorded against a matching credit memo when the customer has one. Invoices created directly in Stripe, not through DayZero, are ignored.

No webhook configuration is needed on your side — DayZero receives Stripe's events for every connected account. See Invoices for the invoice lifecycle and how to read payments back.

Automatic Journal Entries

Stripe event Journal entry
invoice.finalized No new entry — the AR / revenue entry was posted when the invoice was finalized in DayZero. The webhook only verifies that entry matches what Stripe recorded.
invoice.paid Debit Stripe Merchant Account (amount paid less fees) and Merchant Fees Expense (fees) / Credit Accounts Receivable (amount paid). Payments marked paid out-of-band in Stripe set the invoice to manual_paid instead and post no Stripe entry.
invoice.voided No new entry — the void entry was posted when the invoice was voided in DayZero; the webhook verifies it.
payout.paid Debit Transfers Between Accounts / Credit Stripe Merchant Account for the payout amount. When the deposit shows up in a connected bank feed, DayZero matches it by amount and arrival window and books the bank line against Transfers Between Accounts, completing the move into the bank ledger.
payout.failed / payout.canceled Updates the stored payout status; no entry

Stripe Merchant Account is a clearing ledger: it fills as invoices are paid and drains as payouts land, so its balance should hover near the amount Stripe is still holding. Each posted payout is also stored with its gross, fee and invoice breakdown; POST /api/v1/stripe/payouts/{stripe_payout_id}/resync re-fetches the balance transactions from Stripe and rebuilds that breakdown if it looks wrong (admin permission).

Payout Reconciliation

These endpoints (same caveat as above: internal, not in the public API Reference) pull payouts and pair them with bank deposits:

Endpoint What it does
POST /api/v1/stripe/payouts/sync Pulls the connected account's payouts that arrived in the last since_days days (1-90, default 35; payouts still in transit are included), stores each with gross, fee, net and invoice breakdown, posts the transfer entry when none exists, and (unless match: false) matches payouts to bank deposits. Idempotent on the Stripe payout id. It also runs daily at 03:00 UTC and after every bank sync.
GET /api/v1/stripe/payouts/matches Payouts with no bank deposit and Stripe-looking deposits with no payout, plus suggestions of pairs that agree on amount and date. status is unmatched (default), matched or all; include_held=true adds held_payouts (paid payouts with no matching deposit a day after arrival, each with a held_reason).
POST /api/v1/stripe/payouts/import Imports payouts from a Stripe dashboard Balance transactions or Payouts CSV, sent as csv_text or an uploaded s3_key (exactly one), with dry_run to preview. Calls no Stripe API; idempotent on the Stripe payout id. Charges are linked to invoices by invoice number, Stripe invoice id, or customer email and amount.
POST /api/v1/stripe/payouts/{stripe_payout_id}/reapply-gross Rewrites the matched bank deposit's journal entry as Dr Bank (net) and Dr Merchant Fees (fees) / Cr Accounts Receivable (gross), and records each invoice payment at gross with the fee on the payment so invoices close. Optional bank_transaction_id, allocations (invoice_id, gross_cents, fee_cents) and dry_run.
POST /api/v1/stripe/payouts/repair-fee-shortfalls Closes invoices left partially paid by exactly their Stripe fee. dry_run defaults to true.

Amounts in these requests and responses are integer cents.

Each has an MCP tool for agents (payouts/resync has none): start_stripe_payout_sync (since_days), list_stripe_payout_matches (status, include_held), import_stripe_payout_export (csv_text or s3_key), apply_stripe_payout (stripe_payout_id, optional bank_transaction_id and allocations) and repair_stripe_fee_shortfalls. The writes take dry_run or preview_only; see Using DayZero over MCP.

Card issuing for Partner Credit

A business that runs a Partner Credit program can issue its merchants' cards on Stripe Issuing from the same connected account. Choose stripe_issuing as the program's issuing_provider. GET /api/v1/credit/cards/stripe-issuing/status reports whether the account's card_issuing capability is active, its Issuing balance, whether the Issuing terms were accepted, the fields Stripe still needs, and whether DayZero's webhook endpoint is subscribed to the issuing_* events; POST /api/v1/credit/cards/stripe-issuing/setup submits what is missing and requests the capability. Real-time authorizations, cleared transactions, card changes and disputes arrive on the same webhook endpoint as payments, and DayZero answers each authorization against the merchant's available credit. See Stripe Issuing in the credit guide.

Account ownership and credentials

Status and setup default to the business's canonical Stripe connection. An explicit acct_... must be that connection or a per-line account whose Stripe metadata identifies this business and an existing owned credit line. A program's editable issuer_config, including line_stripe_accounts, is only an account locator; it does not prove ownership.

For status and setup only, the platform selector and fallback when a business has no canonical connection require a signed-in platform superuser. An ordinary business operator or an API token, including a superuser-owned token, receives 403. That diagnostic/setup permission does not grant platform card issuance: normal card operations require an owned connected account, with no platform fallback. Live setup and line Connect are console operator actions, so API tokens cannot perform them. Platform webhook changes requested with configure_webhooks are applied only for signed-in platform superusers and skipped for other operators. Connected-account diagnostics inspect account-scoped webhooks; a platform operator must verify the shared Connect webhook configuration separately when those diagnostics cannot confirm it.

Sandbox businesses receive 403 from Stripe status, setup and line Connect before any Stripe call, regardless of the configured API key or caller's privileges. Rehearse card operations through the sandbox mock issuer. Sandbox readiness does not contact Stripe. On live programs, invalid account configuration or unverified ownership produces a failing cards.issuer_connected readiness item with a configuration action instead of aborting the report.

Per-line Connect and retry behavior

POST /api/v1/credit/lines/{credit_line_id}/cards/stripe-issuing/connect validates the line and its program before Stripe mutations. An explicitly selected account must be the business's canonical connection or carry Stripe metadata matching this business and this line. The endpoint cannot attach the platform account.

Omitting stripe_account_id reuses the verified stored line account, recovers an account from Stripe metadata after a failed local save, or creates a Custom account. Creation uses a stable business/line idempotency key. Recovery checks all account-list pages and refuses ambiguous matches; it does not rely solely on Stripe retaining an idempotency key. The local mapping is flushed before credit-policy or funding-obligation operations.

Supply return_url and refresh_url together to request an onboarding link. Both must match the configured product-app origin (APP_URL / settings.app_url, including scheme, host and port), without credentials or fragments. Invalid URLs are rejected before account creation or mutation. Setup also validates any supplied program_id before mutations and requires it when creating a default cardholder.

Gateway integration contract for maintainers

CreditCardService._gateway calls resolve_owned_stripe_issuer_config(session, business_id, program, credit_line_id=...) from app/api/v1/services/credit_stripe_ownership.py before constructing the Stripe adapter. It validates the persisted program/line, resolves supported account aliases through the onboarding ownership boundary, supplies the server credential, and forces sandbox configuration to a credential-free mock. It rejects implicit or explicit platform issuance. Normal issuance, control changes, reconciliation, embed-token calls and the durable delivery worker use this gateway boundary.

The selector covers line_stripe_accounts[line_id], stripe_account_id, connected_account_id and the legacy ramp_spend_program_id account locator, then defaults to the canonical business connection. Conflicting aliases are rejected. Stripe entries in _card_gateways are keyed by program and line, including when the account came from a program-level alias, so a verified account for one line is not reused as authority for another. Live programs require a configured server key; JSON credentials and live/mock flags cannot override that decision. The adapter reads its API credential from server settings and requires an acct_... connected-account scope for live card requests. Keep the sanitized configuration intact when extending this path.

Shared-platform card issuance needs a separate server-controlled binding of business and program to the platform account. No such grant can be inferred from issuer_config, ramp_spend_program_id, a JSON key, or an earlier superuser setup call. Before enabling that mode, the adapter must also verify tenant/program ownership of supplied cardholder IDs and every card read, update, suspension, termination, transaction query and embed-token request. Newly created platform resources need server-stamped business/program metadata; existing resources need trusted reconciliation. A platform program grant alone does not authorize access to another tenant's cards in the same Stripe account. The owned-account helper deliberately fails closed until that shared-platform authorization path is implemented.

Issuer controls and deployment

Stripe credit cards follow the common durable issuer-control contract. New cards are provisioned at zero capacity. Financial and control transactions enqueue a coalesced credit_issuer_sync_requests row; after commit, the worker reads fresh locked state, freezes managed funds, confirms decreases before increases, installs a fair share of aggregate line headroom within requested lifetime caps, and resumes eligible cards only after the complete budget is delivered. TOTAL maps to Stripe all_time; new periodic limits are rejected and legacy periodic cards are frozen pending explicit conversion. Active partner-managed funds found on a mixed line block managed-card rebudgeting. Explicit card suspension and termination remain synchronous decreases; reactivation and budget changes are queued.

requested_spend_limit_cents is the retained lifetime cap; reset_spend_limit_cap: true on the limits endpoint returns the card to automatic shared-headroom allocation. spend_limit_cents is the last issuer-reported ceiling. Treat issuer_sync_status: queued as pending delivery, inspect failed with failure_reason, and verify pushed with the reported ceiling and sync time. A saved payment, a successful setup call or an active local card status alone does not confirm issuer capacity.

Deploy Alembic c8d4e2f6a901 after d9e4b2f8a731 before the new API/worker code. The migration preserves legacy ceilings as requested caps and seeds the tenant-isolated queue. Deploy the worker's CreditIssuerSyncWorkflow / CreditIssuerSyncActivities and the scheduler's one-minute credit-issuer-sync-poller; successful deliveries remain due for five-minute drift repair. Verify the schedule is running on the worker's task queue and that pending requests progress. Failed attempts retain their errors and retry with backoff; after correcting ownership, credentials or control conflicts, request POST /api/v1/credit/lines/{id}/cards/spend-limit-sync and inspect the later outcome. See deployment and recovery for queue fields and legacy-card recovery.

These changes have not been verified against live Stripe providers or a production deployment. Local and mocked tests do not establish live capability approval, webhook delivery or issuer enforcement. Existing audit events and failed-attempt evidence remain the history; deployment does not rewrite them.

Disconnect

bash
curl -X DELETE "https://api.ondayzero.com/api/v1/stripe/disconnect" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Removes the connection from the business. Existing invoices keep their payment history; new invoices are no longer mirrored to Stripe and have no hosted_invoice_url.

Common errors

Status Cause Fix
403 The token's user is not an admin of the business (connect, disconnect, payout sync, resync and repair) Use an admin token
400 payouts/import with neither csv_text nor s3_key (if both are sent, csv_text is used) Send one source
422 since_days outside 1-90 Keep it in range
404 reapply-gross with a stripe_payout_id or bank_transaction_id that does not exist in this business List candidates with GET /api/v1/stripe/payouts/matches?status=all