Adopt the shape, not the engine. Stripe's requirements object is a live, recomputed checklist of keys. The data model transfers to Commerce almost wholesale; the thing that doesn't is who computes it — for us that's a projection of Modern Treasury state, not our own risk engine.
Stripe never sends the values back. requirements.currently_due is just a list of dot-path strings — each one names a gap in the account graph. Any UI can render a checklist from keys alone and resolve each key to a form field. That's how the onboarding banner surfaces gaps without holding the data.
"currently_due": [ "business_profile.mcc", "business_profile.url", "company.address.city", "company.address.line1", "company.address.postal_code", "company.address.state", "company.name", "company.owners_provided", // attestation — a boolean the platform asserts "company.phone", "company.tax_id", "external_account", // object that doesn't exist yet "owners.email", // role placeholder — no person exists yet "owners.first_name", "owners.last_name", "representative.address.city", "representative.address.line1", "representative.address.postal_code", "representative.address.state", "representative.dob.day", "representative.dob.month", "representative.dob.year", "representative.email", "representative.first_name", "representative.last_name", "representative.phone", "representative.relationship.title", "representative.ssn_last_4", "settings.payments.statement_descriptor", "tos_acceptance.date", "tos_acceptance.ip" ]
There's no per-field status enum. The same key appears in every bucket it qualifies for, and the buckets nest: everything past due is currently due; everything currently due will eventually be due. Two side channels carry the in-flight and failed work, and one derived flag summarizes the whole object.
current_deadline is null, so unmet keys land here immediatelyssn_last_4 put individual.id_number here){requirement, code, reason} — the key ties each failure to its checklist item; reason is user-presentablerequirements.past_due → …pending_verification → null) feeding the hard gates charges_enabled / payouts_enabled{
"alternatives": [],
"current_deadline": null, // custom accounts: no grace period, unmet ⇒ past_due immediately
"currently_due": [
"external_account" // ┐
], // │ same key, three buckets —
"disabled_reason": "requirements.past_due",
"errors": [], // │ urgency is membership, not a status field
"eventually_due": [
"external_account" // │
], // │
"past_due": [
"external_account" // ┘
],
"pending_verification": [ // async checks in flight — incl. id_number, inferred from ssn_last_4
"individual.address.city",
"individual.address.line1",
"individual.address.postal_code",
"individual.address.state",
"individual.id_number"
]
}
Scenario 1, real numbers: a bare US individual account owes 18 keys at birth. Each API update drains its slice; identity data transits verification; the account was fully enabled seconds after the last field landed.
Company accounts behave the same, with role-keyed placeholders (representative.*, owners.*) satisfied by creating persons — after which each person's own gaps re-key as person_<id>.*.
[
{ "after": "00-bare-account", "currently_due": 18, "pending_verification": 0, "disabled_reason": "requirements.past_due" },
{ "after": "01-business-profile", "currently_due": 15, "pending_verification": 1, "disabled_reason": "requirements.past_due" },
{ "after": "02-tos-acceptance", "currently_due": 13, "pending_verification": 0, "disabled_reason": "requirements.past_due" },
{ "after": "03-individual-identity", "currently_due": 1, "pending_verification": 5, "disabled_reason": "requirements.past_due" },
{ "after": "04-external-account", "currently_due": 0, "pending_verification": 5, "disabled_reason": "requirements.pending_verification" }
]
// seconds later — GET /v1/accounts/acct_1TqEfEH8Vce5CyP6, verbatim:
{
"charges_enabled": true,
"payouts_enabled": true,
"requirements": {
"alternatives": [],
"current_deadline": null,
"currently_due": [],
"disabled_reason": null,
"errors": [],
"eventually_due": [],
"past_due": [],
"pending_verification": []
}
}
The finding the card was really after. Scenario 3: an account with every field satisfied fails identity verification (test id_number 111111111) — and Stripe pushes a requirement that was never in the original list into currently_due, with errors explaining why. The checklist is a live projection of verification state, not a static form.
{
"alternatives": [],
"current_deadline": null,
"currently_due": [
"individual.id_number",
"individual.verification.document" // ← never requested; added by the failed check
],
"disabled_reason": "requirements.past_due",
"errors": [
{
"code": "verification_failed_tax_id_match",
"detailed_code": "verification_failed_tax_id_match",
"reason": "Taxpayer Identification Number (TIN) on the document doesn't match the one
provided by this account. Provide a matching document or update the Taxpayer
Identification Number of this account. Please refer to this support article on
verifying tax ID numbers: https://support.stripe.com/questions/using-irs-
documentation-as-reference-when-entering-business-name-and-tax-id-number-tin-
for-us-based-businesses",
"requirement": "individual.id_number"
},
{
"code": "verification_failed_keyed_identity",
"detailed_code": "verification_failed_keyed_identity",
"reason": "The person's keyed-in identity information could not be verified. Correct
any errors or upload a document that matches the identity fields (e.g., name
and date of birth) entered.",
"requirement": "individual.verification.document"
}
],
"eventually_due": [
"individual.id_number",
"individual.verification.document"
],
"past_due": [
"individual.verification.document" // membership, not state: in all three buckets at once
],
"pending_verification": []
}
The error format is the context worth copying: each entry is {requirement, code, detailed_code, reason} — a machine-stable code for branching, a user-presentable reason (with remediation and a support link inline), and the requirement key tying it back to the checklist item it blocks. Nothing else; no PII.
Also observed: adding a second person appended person_<id>.phone to eventually_due only (threshold-dependent), and the requirement set is a function of which capabilities you request. Change notification in production is the account.updated webhook.
Context that changes everything: we one-shot onboarding data to Modern Treasury, and MT runs the actual KYC/KYB. We hold neither all the data nor a risk engine — so we copy the projection, not the computation.
legal_entity.tax_id, external_account), zero PII in the requirements object.disabled_reason-style summary feeds our real gate: API access after onboarding.owners_provided pattern, if we do beneficial-owner KYB.current_deadline to progressively restrict live accounts; nothing to enforce in v1.person_<id> keys — only if we ever model persons instead of passing them through to MT.errors[] reasons inline. The truth just comes from MT instead of us.
scripts & per-step snapshots: spikes/stripe-connect-requirements/ (NOTES.md is the long-form writeup)
linear: RENTAL-593 + attached doc · headway: commerce#sugar-teach-visa · notebook: notebook@unit-ordinary-grape
feeds: Requirements tracking model (Stripe-style) — commerce#sentence-agree-math