Vendors
Vendors
Section titled “Vendors”Last updated: 2026-07-26 — payment and compliance switched from Stripe test mode to live mode (real sk_live_... key + new live-mode webhook endpoints for both, prior test-mode endpoints superseded not deleted). Prior: Stripe Identity added as the compliance Worker’s KYC/KYB provider, replacing the Persona integration removed the same day (see the Stripe Identity entry below). OFAC remains deferred per docs/cf-rewrite-deferred-scope.md. Prior: 2026-07-25, ADR-9: finstack-rs deleted, finstack is the sole backend. Secrets/storage locations rewritten where they pointed at deleted Fly/Postgres infra; per-vendor integration code now lives in the relevant finstack/workers/* Worker rather than a Rust crate.
Stripe
Section titled “Stripe”- Purpose: Payment processor — PaymentIntents API for card capture, Stripe-hosted checkout, refunds
- Integration:
finstack’spaymentWorker (workers/payment/src/adapters/stripe.ts) - Accounts per env: DEV/TEST:
acct_1RNJATPEqBRBRw4k(Tap2 test keys),acct_1TdsnHAAGi1faYZS(“Finstack sandbox” — FinStack’s own designated test account, used for the 2026-07-25/26 CF-rewrite live-verification work and ADR-12 Task 8) | PROD (live mode): switched 2026-07-26 —paymentnow runs a realsk_live_...key againstacct_1TdsmwAZwgWtny1w(“Finstack”, a separate account from the sandbox one above);livemode: trueconfirmed via a read-onlyGET /v1/balanceprobe. No real charge has been run yet — real end-to-end verification awaits actual production traffic or an explicit user-authorized live test (see BUGS.md/CHANGELOG.md for what’s verified vs. not). - Webhook endpoint (live mode):
we_1TxdzwAZwgWtny1wnu7fvwBEathttps://webhooks.finstack.sh/webhooks/stripe, registered 2026-07-26, subscribed topayment_intent.{succeeded,payment_failed,canceled}/refund.{created,updated}. Prior test-mode endpointwe_1Tx5inAAGi1faYZSdM4OH72S(also atwebhooks.finstack.sh) is superseded, not deleted — still valid if test mode is needed again. The pre-2026-07-25 Fly-backend endpoint (we_1TfnK7PEqBRBRw4kZfqxhREw) is dead now thatfinstack-rsis deleted. - Secrets location: Worker secrets
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRETon thepaymentWorker — live-mode values as of 2026-07-26, stored in vault asstripe/FINSTACK_LIVE_SECRET_KEY/stripe/FINSTACK_LIVE_WEBHOOK_SECRET(test-mode values remain atstripe/FINSTACK_TEST_SECRET_KEY/stripe/FINSTACK_WEBHOOK_SECRET/finstack-cf/PAYMENT_STRIPE_WEBHOOK_SECRETif a rollback to test mode is ever needed) - API version: PaymentIntents v1 — manual capture flow (
capture_method=manual) - Idempotency: All Stripe API calls set
Idempotency-Key: {payment_id}(create),{payment_id}/capture,{payment_id}/cancel,{refund_id}(refund),{payout_id}/transfer(Connect Transfer — see Stripe Connect entry below) - Webhook:
POST /webhooks/stripe— HMAC-SHA256 signature validation, idempotency guard (seepaymentWorker’swebhook-idempotencyhandling) - Data storage:
paymentWorker’s D1 database (seefinstack/workers/payment/migrations/)
Stripe Connect (ADR-12, added 2026-07-28 — payout rail, Task 8 live-verified end-to-end)
Section titled “Stripe Connect (ADR-12, added 2026-07-28 — payout rail, Task 8 live-verified end-to-end)”- Purpose: Payout rail for finstack’s own money owed to recipients — Custom connected accounts, “separate charges and transfers” (a standalone
Transferagainst finstack’s own balance, not a destination charge on the original PaymentIntent). First of two planned payout rails (Moov second), both routed throughprocessor_configs/ProcessorRouter. - Integration — Transfer side:
finstack’sroutingWorker (workers/routing/src/adapters/stripe.ts—checkTransfersCapability(),createTransfer()), PR #506 (Task 2), called byPOST /payouts/execute(PR #507, Task 3), called for real bypayout’sPOST /payouts/:id/process(PR #513, Task 7). - Integration — account onboarding:
finstack’srecipientWorker (workers/recipient/src/stripe-account.ts—buildCustomAccountPayload(),buildPersonPayload(),createCustomAccount(),createPerson()), Task 5.POST /recipients/:id/stripe-accountaccepts an individual or business payload, creates the Custom account viaPOST /v1/accounts, then — for a business — creates the representative and each beneficial owner viaPOST /v1/accounts/{id}/persons(a separate sub-resource call per person, not inline account fields). All Stripe calls in the chain use a{recipientId}/...-derivedIdempotency-Key; nothing is persisted onto therecipientsrow until every call succeeds, so a caller retrying after a partial failure (e.g. a rejected beneficial-owner SSN) safely resumes into the same Stripe resources. - Endpoints used:
GET /v1/accounts/:id(capability pre-check — must confirmcapabilities.transfers === 'active'before ever attempting a Transfer),POST /v1/transfers(Transfer creation),POST /v1/accounts(Custom account creation),POST /v1/accounts/{id}/persons(representative/beneficial-owner creation). - Account:
acct_1TdsnHAAGi1faYZS(“Finstack sandbox”) — FinStack’s designated test/sandbox Stripe account, used for test-mode verification per Task 8’s own instruction (not real money). This is a separate Stripe account fromacct_1TdsmwAZwgWtny1w(“Finstack”), the production accountpayment/compliancerun live against — the two are independent accounts with independent capabilities and Connect-enrollment status, not test/live modes of one account. Enrolled in Stripe Connect 2026-07-28 (was not enrolled when Task 8 first attempted live verification; the user enrolled it via the Dashboard the same day, confirmed by a realPOST /v1/accountssucceeding). typeparam bug found + fixed 2026-07-28 (Task 8):buildCustomAccountPayload()sent bothtype: 'custom'and thecontroller[...]params — Stripe now rejects this combination outright (“mutually exclusive”;typeis deprecated,controlleris its replacement per Stripe’s current docs). This meant everyPOST /recipients/:id/stripe-accountcall would have 400’d against real Stripe since Task 5 shipped, masked becauserecipient’s last real code deploy predated Task 5 by a week (see below). Fixed by droppingtypeentirely; live-verified the mutual-exclusivity error is gone.- Live-verified end-to-end 2026-07-28 (Task 8, final): create recipient →
POST /recipients/:id/stripe-account(real Custom accountacct_1TyKQgA...) → funded itstransferscapability toactive(added a test external bank account +business_profile.url, both required by Stripe before a capability activates) → manual KYC approve (/compliance/kyc→/review→/decide, propagated torecipients.status = kyc_approved) →POST /payouts→POST /payouts/:id/process. First attempt hitbalance_insufficient— the sandbox platform account had zero available balance to fund a Transfer from, an ordinary test-account state, not a bug; correctly classified as an ambiguous failure by Task 7’s design (left inprocessing/processor_status: unknown, not reverted — see the/processentry in ARCHITECTURE.md). Resolved by funding the platform’s available balance with Stripe’stok_bypassPendingtest token (bypasses the pending period), then/fail-ing the stuck payout (the plan’s own documented exit path for that state) and creating a fresh one. Second attempt succeeded:POST /payouts/:id/processreturned200with a realprocessor_payout_id(tr_1TyKWOAAGi1faYZS5BGolR8P), independently confirmed via a directGET /v1/transfers/:idagainst Stripe (amount: 500,destination: acct_1TyKQgA...,reversed: false). All test D1 rows (recipient, both payouts, the KYC check) and the disposablefinstack-cf/ADR12_TASK8_LIVE_VERIFY_KEYwere deleted after; the Stripe-side test objects (connected account, Transfer) were left in place as they’re test-mode, zero-cost, and useful as an audit trail. - Secrets location — two different mechanisms on this one integration: the Transfer side (routing) has no static Worker secret — its key is decrypted at call time from
processor_configs.credentials(AES-256-GCM,ROUTING_CREDENTIALS_MASTER_KEYonrouting— see SECURITY.md’s “Encryption at Rest” section), selected per-tenant viaroutePayout().ROUTING_CREDENTIALS_MASTER_KEYhad never actually been set in production despite Task 1 shipping the encryption code (found + fixed 2026-07-28, Task 8 — see ENVIRONMENTS.md). Account onboarding (recipient) uses a staticSTRIPE_SECRET_KEYWorker secret instead. Correction: this entry and ENVIRONMENTS.md previously claimed that secret was set with the live key (stripe/FINSTACK_LIVE_SECRET_KEY) when Task 5 shipped — it had never actually been set at all (same discovery,wrangler secret list); now genuinely set, using the test-mode key (stripe/FINSTACK_TEST_SECRET_KEY), matching Task 8’s own instruction to verify with test-mode, not live, credentials. - Idempotency:
Idempotency-Key: {payout_id}/transferonPOST /v1/transfers;{recipientId}/stripe-account,{recipientId}/representative,{recipientId}/owner/{index}on the three account-onboarding calls. - Known gap Stripe’s own API creates: the Transfer object has no
statusfield (unlike Charges/PaymentIntents/Payouts) — a non-throwing response is the success signal;reversed/amount_reversedare the only post-creation state exposed. - Data storage:
recipients.payout_method/recipients.stripe_account_id(Task 4/5);payout’spayouts.processor_payout_id/processor_status(Task 7).
Stripe Identity (added 2026-07-26, replaces Persona)
Section titled “Stripe Identity (added 2026-07-26, replaces Persona)”- Purpose: Embedded/client-side document verification for KYC (recipients) and primary-signer KYB (merchants) — the
complianceWorker’sprovider: 'stripe_identity'path onPOST /compliance/kycandPOST /compliance/kyb, alongside the pre-existing fully-manual operator-driven flow (both providers coexist; manual/review//decideis unchanged). - Integration:
finstack’scomplianceWorker (workers/compliance/src/stripe-identity.ts—StripeIdentityClient+verifyStripeIdentityWebhook) - Account: Same Stripe account as
payment— Identity is a separately-activated product capability, not a separate account. Switched to live mode 2026-07-26 alongsidepayment(see the Stripe entry above); test-mode verification (acct_1TdsnHAAGi1faYZS) confirmed enabled via a livePOST /v1/identity/verification_sessionsprobe before the live-mode swap. - Webhook endpoint (live mode):
we_1TxdzwAZwgWtny1wIWMsKpHNathttps://webhooks.finstack.sh/webhooks/stripe-identity, registered 2026-07-26, subscribed toidentity.verification_session.{verified,requires_input,canceled}. Prior test-mode endpointwe_1TxXsiAAGi1faYZSKKJIVXUf(same URL) is superseded, not deleted. - Secrets location: Worker secrets
STRIPE_IDENTITY_SECRET_KEY(same raw key aspayment’sSTRIPE_SECRET_KEY, now the live key),STRIPE_IDENTITY_WEBHOOK_SECRETon thecomplianceWorker — live-mode values stored in vault asstripe/FINSTACK_LIVE_SECRET_KEY/stripe/FINSTACK_LIVE_IDENTITY_WEBHOOK_SECRET(test-mode value remains atfinstack-cf/STRIPE_IDENTITY_WEBHOOK_SECRETif a rollback is ever needed) - Webhook:
POST /webhooks/stripe-identity— HMAC-SHA256 signature validation (samet=/v1=scheme aspayment’s Stripe webhook), two-phase idempotency viastripe_identity_webhook_events(migration0003_stripe_identity.sql) - Closing the loop: on a terminal event (
verified→approved,canceled→rejected), the webhook handler callsRECIPIENT’sPOST /recipients/:id/update-kyc-statusorMERCHANT’sPATCH /merchants/:idbefore committing compliance’s own local state transition — a retry-safety invariant verified by a dedicated adversarial test (test/index.test.ts’s “CRITICAL” describe block). A 404 from either downstream Worker (subject_id permanently unresolvable) is distinguished from a 5xx (transient) viaSubjectNotFoundError— the former is marked processed to stop Stripe’s retries, the latter is left unprocessed for retry. - Scope: primary-signer identity verification only, not full beneficial-ownership KYB completeness — multi-owner cases still route through the existing manual
/beneficial-owners+/decideflow. - Data storage:
complianceWorker’s D1 database (compliance_kyc_checks/compliance_kyb_checks, unchanged shape; newstripe_identity_webhook_eventstable) - Live-verified 2026-07-26 against production (
gateway.glcf.workers.dev/webhooks.finstack.sh): realVerificationSessioncreated for both a real recipient and a real merchant, real signed webhook delivered for each, both closed the loop (recipients.status→kyc_approved,merchant_accounts.status→active) — all test rows and the disposable verificationsk_*key deleted after. - Design/plan:
docs/superpowers/specs/2026-07-26-stripe-identity-kyc-design.md,docs/superpowers/plans/2026-07-26-stripe-identity-kyc.md
Persona (removed 2026-07-26, superseded by Stripe Identity above)
Section titled “Persona (removed 2026-07-26, superseded by Stripe Identity above)”Persona was never live on finstack (PERSONA_API_KEY was always unvaulted) and an audit found the integration wasn’t actually reachable end-to-end even if the key had been configured — gateway never routed /webhooks/persona, and no code connected a KYC decision to recipients.status/payout gating. Removed outright rather than finished: PersonaClient, the webhook receiver, and persona_webhook_events (migration 0002_drop_persona.sql) were deleted from the compliance Worker.
US Treasury OFAC SDN List
Section titled “US Treasury OFAC SDN List”Stubbed no-op on finstack (not CF-specific — was also stubbed on the now-deleted Rust backend; see docs/cf-rewrite-deferred-scope.md). Description below is the target contract, not a current integration. Clarified 2026-07-28 (user): this is a gap in compliance’s own independent screening, not a claim that no sanctions screening happens anywhere in FinStack’s money-movement paths — Stripe (Connect Custom account underwriting for the payout rail, Radar/card-network compliance for charges) and Moov (once integrated) both perform their own screening as regulated processors. See SECURITY.md’s OFAC entry.
- Purpose: Sanctions screening — OFAC Specially Designated Nationals (SDN) list fuzzy name matching
- Source URL:
https://www.treasury.gov/ofac/downloads/sdn.xml - Auth: None — public endpoint
- Caching: In-memory
Arc<RwLock<SdnList>>refreshed every 24h via background tokio task started at server startup - Matching algorithm: Jaro-Winkler similarity ≥ 0.88 = Hit; ≥ 0.78 = Inconclusive; < 0.78 = Clear
- Data storage: No persistent storage — in-memory only; re-loaded on restart + 24h refresh
Resend
Section titled “Resend”- Purpose: Transactional email delivery (
POST /v1/messages/email) - Integration:
finstack’smessagingWorker - API Endpoint:
https://api.resend.com/emails - Auth: Bearer token —
RESEND_API_KEYWorker secret - Env vars:
RESEND_API_KEY,MESSAGING_FROM_EMAIL(default sender) - Error handling: Non-2xx Resend response → hard error (500), not silent
- Scope required:
messaging:write
Twilio
Section titled “Twilio”- Purpose: General SMS delivery (
POST /v1/messages/sms) via themessagingWorker. Also the delivery channel for consumer OTP auth (svc-auth’sPOST /v1/auth/otp/send, shipped PR #511 — see CLAUDE.md’s Current State and INTERFACES.md’s Consumer OTP auth entry). The OTP code is always generated locally (generateOtpCode()inconsumer-auth.ts,crypto.getRandomValues-backed) and sent as real custom SMS text — never a vendor-generated or vendor-echoed code. See CLAUDE.md’s Current State for why: a same-day incident (PR #512, reverted) briefly shipped a workaround that used Twilio’s own auto-generated code instead, which turned out to be a fixed constant on this trial account rather than random per-request — a live authentication bypass. Do not reintroduce that pattern without independently verifying per-request randomness across multiple live calls first. - Integration:
finstack’smessagingWorker - API Endpoint:
https://api.twilio.com/2010-04-01/Accounts/{SID}/Messages.json - Auth: HTTP Basic —
TWILIO_ACCOUNT_SID(username) +TWILIO_AUTH_TOKEN(password), Worker secrets, vaulted undertwilio/ - Env vars:
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_FROM_NUMBER(E.164 format) - Error handling: Non-2xx Twilio response → hard error, not silent
- Scope required:
messaging:write - Account tier: trial (provisioned 2026-07-28), not upgraded. Two trial-only restrictions currently block
otp/sendfrom actually delivering (fails loud,502 sms_send_failed— this is the correct, safe state, not a bug):- Custom SMS body text is rejected (error 572006, “Invalid template name”) — trial accounts only accept predefined template names as
Body, not real message text.otp/send’s real custom-text message (“Your FinStack verification code is…”) hits this every time on the current trial tier. - SMS delivery is restricted to pre-verified destination numbers (separate from the above) — a number must be added under Verified Caller IDs before Twilio will send to it, via a voice-call verification flow. Also observed: brand-new trial accounts can hit a third restriction (verification calls themselves policy-blocked, likely anti-fraud, resolves without action after the account ages).
- Revisit trigger: upgrade the Twilio account (removes both restrictions at once) before any real, non-test Tap2 Wallet user needs an OTP. No code change is needed when that happens —
handleOtpSend’s existing locally-generated-code design already sends real custom text; it will simply start succeeding instead of 502ing.
- Custom SMS body text is rejected (error 572006, “Invalid template name”) — trial accounts only accept predefined template names as
Neureus
Section titled “Neureus”- Purpose: Multi-tenant AI gateway — routes LLM calls for the
aiWorker’s configurable task execution engine to OpenAI/Anthropic/Workers AI depending on the selected model - Integration:
finstack’saiWorker (workers/ai/src/neureus.ts). Contract confirmed via neureus/neureus#247/#248 — request/response are OpenAI-envelope-compatible (choices[0].message.content, snake_caseusage.prompt_tokens/completion_tokens) - API Endpoint:
{NEUREUS_BASE_URL}/ai/chat(chat completion),{NEUREUS_BASE_URL}/ai/models(model catalog); default base URLhttps://api.neureus.ai - Auth: Bearer token —
NEUREUS_API_KEYWorker secret; single platform-level tenantfinstack-platform - Env vars:
NEUREUS_API_KEY,NEUREUS_BASE_URL - Error handling:
env.NEUREUS_API_KEYunset → hard error, not a silent fallback - Data storage:
aiWorker’s D1 database - Scope required:
ai:write(task creation/run),ai:read(task/run listing)
API Consumers (Tenants)
Section titled “API Consumers (Tenants)”Products outside this repo that call finstack as their backend. Distinct from Vendors above (services FinStack depends on) — these depend on FinStack.
Tap2 Wallet / Tap2 One (g-a-l-a-c-t-i-c/tap2, apps/wallet-api + apps/merchant-api)
Section titled “Tap2 Wallet / Tap2 One (g-a-l-a-c-t-i-c/tap2, apps/wallet-api + apps/merchant-api)”- Purpose: Tap2’s consumer wallet (balance, topup, pay, disputes, seller KYC/payout) and merchant POS (customers, payments, catalog) proxy to FinStack instead of Tap2’s own deprecated backend.
- Tenant ID:
bbaaa798-374f-4bbd-a1d9-df40674ab9d6(fixed, not rotated as part of this onboarding — matches what Tap2’swallet-apialready has configured asFINSTACK_WALLET_TENANT) - Scopes:
wallet:read,wallet:write,compliance:read,compliance:write,recipients:read,recipients:write - Secrets location:
sk_live_*key minted 2026-07-27, stored in vaultfinstack-cf/TAP2_WALLET_API_KEY— Tap2 sets this as their ownFINSTACK_SVKWorker secret (wrangler secret put FINSTACK_SVKonwallet-api/merchant-api) - Status as of 2026-07-27: FinStack side ready; Tap2’s client code is not yet compatible — confirmed broken in production (wrong credential type, wrong tenant, and a field-naming mismatch across the
wallet/recipientdomains). Full detail, live-verification evidence, and the exact fixes needed:docs/tap2-wallet-integration-readiness.md. Filed as tap2#107 (auth) and tap2#108 (API contract). - Data storage: Tap2’s wallet ledger is entirely fresh in
finstack’swalletWorker (per-consumer Durable Objects) — no migration from Tap2’s pre-cutoverfinstack-rs/Neon data exists or is planned. Any real balances predating the 2026-07-25 cutover are not reflected. The source Neon instance was deleted 2026-07-27 (ADR-11) after a full export; if reconciliation is ever decided on, it now has to come from the local backup (/Users/am/backups/finstack-neon-2026-07-27/, see DEPLOYMENT.md) rather than a live query.
Vendor Template
Section titled “Vendor Template”[Vendor Name]
Section titled “[Vendor Name]”- Purpose:
- Accounts per env: DEV: | STAGE: | PROD:
- Secrets location: [Cloudflare Worker secret VENDOR_API_KEY]
- API version:
- Pricing:
- Data storage:
- Reconciliation: