Stripe
What the Stripe connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/stripe_conn.py. Kind: facts only. Display name: "Stripe".
This is the data connector that reads a customer's own Stripe account for refund/credit facts. It is unrelated to CommitLayer's own subscription billing (app/billing/stripe_billing.py), which uses the platform's Stripe account and separate keys.
Purpose
Reads refunds, credit notes and subscription cancellations so that the refund_or_credit_within, refund_posted_within and subscription-related signals can be evaluated against claims from help-desk vendors.
Since 0.9.5 it also reads outbound payments from the account's financial accounts (Treasury v1 and Money Management v2, the API behind Global Payouts and stripe pay), so that a transfer an agent sends can be verified under the stripe_b2b_transfer library contract: did it exist, go to the intended recipient, once, for the amount asked, reach posted, and stay there.
Auth and scopes
| Item | Value |
|---|---|
| Method | Restricted API key, sent as Authorization: Bearer, against https://api.stripe.com |
| Credentials stored (encrypted) | api_key; optional base_url |
| Least-privilege permissions for the restricted key | Refunds: read, Credit notes: read, Subscriptions: read, Customers: read (needed only to expand the customer on refunds/credit notes for email hashing), Charges: read (implied by expanding charge.customer on refunds). To verify transfers an agent sends, add Financial accounts: read and Outbound payments: read (Treasury), and Money Management Outbound Payments: read (v2). Everything else: none. A key without the transfer permissions still syncs refunds; the connection's health message says what to grant. |
| Connection test | GET /v1/refunds?limit=1 |
| Non-secret config | none |
Fields pulled and why
| Endpoint | Fields | Used for |
|---|---|---|
GET /v1/refunds?created[gt]=since&expand[]=data.charge.customer |
id, amount, currency, created, charge.customer.id, charge.customer.email |
Refund facts, subject id, hashed email, watermark |
GET /v1/credit_notes?created[gt]=since&expand[]=data.customer |
id, amount, currency, created, customer.id, customer.email |
Credit facts, subject id, hashed email |
GET /v1/subscriptions?created[gt]=since |
id, status, canceled_at, customer |
Subscription-cancellation facts |
GET /v1/treasury/financial_accounts then GET /v1/treasury/outbound_payments?financial_account=&created[gt]=since-35d |
id, amount, currency, status, status_transitions, customer / destination_payment_method, financial_account, created, returned_details.code |
Outbound-payment facts (Treasury v1) |
GET /v2/money_management/outbound_payments?created_gt=since-35d (header Stripe-Version: 2026-04-22.preview, pages followed by next_page_url) |
id, amount.value/currency, status, status_transitions, to.recipient / to.payout_method, from.financial_account, created, status_details.returned.reason |
Outbound-payment facts (Money Management v2) |
Amounts are converted from minor units (amount / 100). Outbound payments are re-read 35 days behind the watermark on every sync, because a payment changes status after it is created (posted, returned) and the facts for those statuses are keyed on the payment; the re-read is idempotent.
What is hashed / never stored
- Customer email (from the expanded customer object) →
customer_hid(HMAC-SHA256, per-tenant key); the email is discarded. - Card details, payment method data, customer names, addresses, invoice line items, charge descriptions and metadata are never requested or stored.
- The restricted key is stored only as ciphertext.
Incremental sync and watermark
- All three lists use
created[gt] = <watermark epoch>(0 on first sync), page size 100, cursor viastarting_after = <last id>whilehas_more. - Watermark = maximum refund
created(credit notes and subscriptions do not advance it). - Upserts are idempotent.
Rate-limit handling
Shared HttpClient (Retry-After, exponential backoff on 429/5xx, 6 attempts, immediate reauth failure on 401/403).
Fact types produced
Subject id is the Stripe customer: stripe_customer_{customer_id}.
fact_type |
Subject kind | When | Attributes | raw_ref |
|---|---|---|---|---|
refund |
customer |
Each refund | amount (major units), currency, customer_hid |
stripe:refund:{refund_id} |
credit |
customer |
Each credit note | amount, currency, customer_hid |
stripe:credit_note:{credit_note_id} |
subscription_change |
subscription |
Each listed subscription with canceled_at set |
status, change: "canceled" |
stripe:subscription:{subscription_id}:canceled |
outbound_payment_created |
recipient |
Each outbound payment, at created; attributes refresh on every sync |
amount, currency, payment_id, status, financial_account |
stripe:outbound_payment:{id} |
outbound_payment_posted |
recipient |
Status posted, at status_transitions.posted_at |
amount, currency, payment_id |
stripe:outbound_payment:{id}:posted |
outbound_payment_returned |
recipient |
Status returned, at returned_at |
amount, currency, payment_id, reason |
stripe:outbound_payment:{id}:returned |
outbound_payment_failed |
recipient |
Status failed or canceled, at failed_at / canceled_at |
amount, currency, payment_id, status |
stripe:outbound_payment:{id}:{status} |
Outbound-payment facts use the subject stripe_recipient_{id}, where the id is the recipient as Stripe names it: to.recipient (an acct_…) or to.payout_method on v2, the customer or destination_payment_method on v1. An agent that records a stripe_b2b_transfer action uses the same subject (the --json output of stripe pay carries it), or pushes a links entry from the profile handle it was told to pay to that id.
Claims produced
None.
Subject id format
stripe_customer_{cus_…}. Because help-desk claims name a conversation or ticket, Stripe facts only become relevant to a claim through a subject link between the conversation subject and the Stripe customer subject. The link is made by matching customer_hid (hashed email) between the two sources; the library function app.connectors.base.resolve_identities performs this, and a scheduled identity-resolution pass is a planned addition (see ARCHITECTURE.md §4). Until then, links can be supplied via a CSV facts upload whose subject_id equals the conversation subject.
Known limitations
- Subscription cancellations are discovered through
created[gt], i.e. only subscriptions created after the watermark are listed; cancellations of older subscriptions are missed. This is a known gap. - Refunds without an expandable charge/customer (e.g. some Checkout or PaymentIntent flows) produce a subject id with an empty customer id and cannot be linked.
- No
customer_hidon subscription facts (customer is referenced by id only). - Partial refunds and full refunds are not distinguished in the fact type;
amountis available in attributes. - Disputes/chargebacks are not read.
- A recipient Stripe has never paid has no record at all, so an agent's claim to have paid it, when nothing was created, answers UNKNOWN (
identity_unmatched, "subject not found") rather than FAIL; once any payment to that recipient exists, a missing one is a FAIL. The v2 list is read under a pinned preview API version (V2_VERSIONin the module); Stripe may move it. - Financial-account balances are not read: Stripe's
postedstatus is its own statement that funds left the account, and a balance snapshot taken at sync time cannot be aligned with the action's moment.