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}/move and /associate return 409 with code: InvalidStateTransition and detailCode: 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-Account on 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

StatuscodeWhy
401UnauthenticatedThe bearer isn't a Person session (for example, a ServiceAccount API key).
403AccountMismatchThe session is bound to an Account, or you lack Write access on the parent.
409ResourceConflictThe externalId is already used. Create is never an upsert. Look up the existing Account with filter[externalId][eq].
422ValidationErrorA 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:

ToCall
Walk up from a child to the root (breadcrumbs)GET /v3/platform/accounts/{accountId}/ancestors
Find a child by your own IDGET /v3/platform/accounts?filter[externalId][eq]=entity-alpha
List all children of a parent, filteredGET /v3/platform/accounts?filter[parentAccountId][eq]={parentId}&filter[status][eq]=Active
Read one AccountGET /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-Account in 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

StatusMeaning
ActiveThe initial and normal state.
SuspendedA recoverable hold. The Account stays readable so an authorized person can resolve it.
ClosedTerminal. 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


Did this page help you?