Can UBO information be submitted separately from the business?
Why we ran this
David asked (comment on commerce#retire-lawsuit-intact) whether UBO
information can be submitted separately from the business — e.g. business first,
owners later. The original spike never exercised associations, but its captures
hinted "probably yes": MT models UBOs as separate legal entities linked via
legal_entity_associations, the create-time completeness checklist has no
beneficial-owner rule, and PATCH re-triggers verification. The probe was run to
confirm before answering. The hint was wrong.
What was attempted
Every plausible API surface for attaching a UBO after the business exists:
| # | Attempt | Result | Evidence |
|---|---|---|---|
| 1 | POST /api/legal_entities with embedded legal_entity_associations (control) |
201 — works | 08/05-embedded-at-create.json |
| 2 | PATCH /api/legal_entities/:id, association with embedded child individual |
200, silently dropped | 07/03-patch-attach-ubo.json |
| 3 | PATCH /api/legal_entities/:id, association by child_legal_entity_id to a pre-created individual |
200, silently dropped | 08/03-patch-link-by-id.json |
| 4 | POST /api/legal_entity_associations (standalone resource) |
404 — no such path | 07/04-standalone-association.json |
| 5 | POST /api/legal_entities/:id/legal_entity_associations (nested route) |
404 — no such path | 08/04-nested-route.json |
| 6 | POST /api/legal_entities creating a standalone individual (a would-be UBO on its own) |
201 — but unlinkable | 08/01-individual-create.json |
Evidence paths are relative to snapshots/; 07/ =
07-ubo-probe, 08/ = 08-ubo-attach-variants.
Each file holds the exact request body sent and the raw response.
The two decisive wire captures
PATCH is a silent no-op, not a rejection
Business 912a0ee8 was created with no associations and auto-activated.
A PATCH carrying a complete beneficial owner (name, DOB, address, SSN, 40% ownership)
returned 200 OK — and the response body still shows
"legal_entity_associations": []. No updated event was ever
emitted (the events log stayed [activated, created] through 30s of
polling), so MT didn't just decline the field — it never registered a change at all.
The same happened when linking a pre-created individual by id (attempt 3).
PATCH /api/legal_entities/912a0ee8-5d48-46ba-b2ba-39ece2354082
{ "legal_entity_associations": [ { "relationship_types": ["beneficial_owner"],
"ownership_percentage": 40, "child_legal_entity": { ...complete individual... } } ] }
→ HTTP 200
"legal_entity_associations": [] ← field silently dropped
events after 30s: [activated, created] ← no "updated" ever fired
Embedded at create is the one path that works
The identical association payload, embedded in a fresh business create, persists
fully: the association gets an id, the child becomes a real legal entity with its own
id and pending status, and ownership_percentage round-trips.
POST /api/legal_entities (business + embedded legal_entity_associations)
→ HTTP 201, parent 89fb5992-5647-4424-acdd-57469b9d3ae4 (status: pending)
association 30224e62-c5c1-47eb-9a34-5d05f11066b0
relationship_types: ["beneficial_owner"], ownership_percentage: 40
child_legal_entity: 7fe6fe37-f26d-4cbc-9b0e-b78cb80bde24
(type: individual, status: pending)
Method notes
-
Same harness as the parent spike: raw
fetchagainstapp.moderntreasury.com(no SDK, so error bodies and silent drops are seen exactly as the wire delivers them), creds fromapps/api/.env, every request/response pair snapshotted to JSON. -
Create-only: five new entities, all named
SPIKE RENTAL-594 …, fresh EINs to dodge MT's dedupe. No ledger/book objects touched. - Business creates used the full completeness payload the parent spike established (address, EIN, date formed, intended use, activity volume, proof-of-address document) so a 422 could never mask the association behavior.
- Attempt 3 controls for "maybe the child must exist first": the individual was created standalone (201) before the link-by-id PATCH — the PATCH still dropped it.
What this means for us
- The onboarding packet is atomic: business + all UBOs must be assembled before the single MT create. This reinforces the earlier finding that the stepped/partial experience has to live in Commerce (our requirements model), with MT receiving one complete submission.
- Late UBOs mean re-create: there is no amendment path. A new entity must be created with the associations embedded, and since MT enforces EIN dedupe, the old entity has to be discarded first.
- Softener: our compliance profile does not require UBOs — the create-time completeness checklist has no beneficial-owner rule, and association-less businesses activate in sandbox.
File inventory
| File | Contents |
|---|---|
07-ubo-probe.mjs | Create association-less business → PATCH embedded UBO → standalone endpoint → event polling |
08-ubo-attach-variants.mjs | Standalone individual create → link-by-id PATCH → nested route → embedded-at-create control |
snapshots/07-ubo-probe/ | 7 request/response captures incl. the silent-drop PATCH and 404 |
snapshots/08-ubo-attach-variants/ | 6 captures incl. the working embedded-at-create response |
NOTES.md | Parent spike writeup; UBO findings appended as a follow-up section |