Parent and child Accounts
Create child Accounts under a parent, list and navigate the hierarchy, and act on a child Account with one credential in the Wingspan V3 API.
Use this page to create a child Account under a parent Account, find Accounts in your hierarchy, and send requests on behalf of a child. In V1 these were "organization child accounts" created with POST /users/organization/user and then associated; in V3 it's one call.
How the hierarchy works
Every Account can have one parent (parentAccountId). A caller with access to a child Account can act on it by sending X-Wingspan-Account: {childAccountId}. Access to the parent doesn't reach the child by itself: a Person reaches a child when they created it, are its Principal, or hold an Authorization on it or on an ancestor granted with isAccountScopeInclusiveOfDescendants: true. See Team access and roles. The request then runs as if the child made it: resources are read from and written to the child, rate limits are charged to the child (and its Organization), and webhooks are routed to the child.
Two rules shape how you build the tree:
- The parent is set at creation and can't change. Moving an Account to a new parent or attaching an existing standalone Account to a parent isn't available.
POST /v3/platform/accounts/{accountId}/moveand/associatereturn409withcode: InvalidStateTransitionanddetailCode: users.AccountOwnershipImmutable. - Settings aren't inherited. V1's
inheritanceStrategy(copy branding or payment configuration from the parent) isn't available in the V3 API yet. Configure each Account directly.
Prerequisites
- A Person session (a user login), not a ServiceAccount API key. Creating an Account is a Person-scoped bootstrap action, so ServiceAccounts can't call it. See Team access and roles.
- The session must be unbound: not tied to a specific Account. An Account-bound session gets
403 AccountMismatch. - Write access on the parent Account.
- Don't send
X-Wingspan-Accounton the create call. The Account you're creating doesn't exist yet.
1. Create the child Account
Call Create an Account with parentAccountId. The child joins the parent's Organization automatically.
curl -X POST https://api.wingspan.app/v3/platform/accounts \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"parentAccountId": "Nw7kQ2pLx9RtVb3mHc5dZa",
"externalId": "entity-alpha",
"profile": {
"supportEmail": "[email protected]",
"timezone": "America/Los_Angeles"
},
"metadata": { "region": "west" }
}'// 201 Created (trimmed)
{
"id": "Ap4tYs8KqW2nLm6xRb1cVe",
"status": "Active",
"parentAccountId": "Nw7kQ2pLx9RtVb3mHc5dZa",
"organizationId": "Og3hTq8VnK1pXw6cMd4bRs",
"externalId": "entity-alpha",
"profile": {
"supportEmail": "[email protected]",
"timezone": "America/Los_Angeles"
},
"metadata": { "region": "west" }
}profile is display information: logoUrl, brandColor, supportEmail, supportPhone, locale, and timezone (see Branding and customization). The legal and tax identity of record (legal name, EIN, entity type) lives on the Account's compliance entity, not here. When you update an Account later, sending profile replaces the whole profile, so include every field you want to keep. See Tax information and Identity verification.
A child Account has no principal (the person who represents it) until you add one. The creator is not assumed to be the child's principal. To designate one, create a Stakeholder with isPrincipal: true. See Team access and roles.
What can go wrong
| Status | code | Why |
|---|---|---|
401 | Unauthenticated | The bearer isn't a Person session (for example, a ServiceAccount API key). |
403 | AccountMismatch | The session is bound to an Account, or you lack Write access on the parent. |
409 | ResourceConflict | The externalId is already used. Create is never an upsert. Look up the existing Account with filter[externalId][eq]. |
422 | ValidationError | A field is invalid, for example an organizationId that doesn't match the parent's Organization. |
2. Find Accounts in the hierarchy
List a parent's direct children with List child Accounts:
curl https://api.wingspan.app/v3/platform/accounts/Nw7kQ2pLx9RtVb3mHc5dZa/children \
-H "Authorization: Bearer $WINGSPAN_TOKEN"Other lookups:
| To | Call |
|---|---|
| Walk up from a child to the root (breadcrumbs) | GET /v3/platform/accounts/{accountId}/ancestors |
| Find a child by your own ID | GET /v3/platform/accounts?filter[externalId][eq]=entity-alpha |
| List all children of a parent, filtered | GET /v3/platform/accounts?filter[parentAccountId][eq]={parentId}&filter[status][eq]=Active |
| Read one Account | GET /v3/platform/accounts/{accountId} |
Closed Accounts are never returned. A full-subtree hierarchy read isn't available in the V3 API yet; walk children level by level.
3. Act on a child Account
Send the child's ID in X-Wingspan-Account. Creating a child Account gives you Admin access to it, so the Person who created it can do this right away. Here that Person creates a payee owned by the child:
curl -X POST https://api.wingspan.app/v3/payments/payees \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "X-Wingspan-Account: Ap4tYs8KqW2nLm6xRb1cVe" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "context": "Contractor", "externalId": "alpha-c-1042" }'The payee belongs to Alpha LLC, not to the parent. A header that points outside the caller's reach returns 403 AccountMismatch. Person-only endpoints (under /v3/platform/persons/{personId}/...) reject the header with 400 AccountScopeNotApplicable. Full rules are in Acting on behalf of Accounts.
Important: If you cache API responses in a shared layer, include
X-Wingspan-Accountin the cache key. Leaving it out can serve one Account's data to another.
Bind a session to an Account instead of sending the header
If a user works inside one Account for a whole session, replace their session with one bound to that Account. Calls without the header then default to the bound Account. Use Update a session on the caller's own session:
curl -X PATCH https://api.wingspan.app/v3/platform/sessions/Ss1dKw6tQm3pXn8vLr2bHc \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountScope": {
"accountId": "Nw7kQ2pLx9RtVb3mHc5dZa",
"shouldIncludeDescendants": true
}
}'The response carries a replacement token. Use it for every later call; the old session is revoked. Send "accountScope": null to go back to an unbound session.
For a backend with no human in the loop, a ServiceAccount can mint an Account-scoped session instead. See Embed Wingspan in your app.
4. Close an Account
DELETE /v3/platform/accounts/{accountId} closes an Account and returns 204. Closed is terminal: the Account can't be used as an access context afterward. It returns 409 PrincipalLockedByActiveEngagement while the Account has an active Employee or EmployeeOfRecord engagement.
Account statuses
| Status | Meaning |
|---|---|
Active | The initial and normal state. |
Suspended | A recoverable hold. The Account stays readable so an authorized person can resolve it. |
Closed | Terminal. Not returned by list calls and not usable as an access context. |
An Active Account can still be blocked from moving money until its verification requirements are complete. See Requirements and eligibility.
Events
Account.Updated and Account.Closed are available as webhooks. To receive events for a parent and all its children on one endpoint, subscribe with an Account scope and shouldIncludeDescendants: true:
{
"url": "https://hooks.example.com/wingspan",
"subscribedEvents": ["Payable.*", "Invoice.*", "Account.*"],
"scope": { "type": "Account", "id": "Nw7kQ2pLx9RtVb3mHc5dZa", "shouldIncludeDescendants": true }
}Each event carries routingSubject, the Account it's about, so you can route it. There's no webhook for Account creation yet; record the ID from the 201 response. See Create a subscription.
Next steps
- Organizations for when to split your business into Accounts
- Team access and roles to give people and systems access to each Account
- Embed Wingspan in your app if the child Accounts are your customers
Updated 10 days ago