Team access and roles

Give teammates and backend systems access to Wingspan Accounts with Stakeholders, roles, Authorizations, ServiceAccounts, and API keys in the V3 API.

This page shows how to give people and systems access to an Account: invite a teammate with a role, define a custom role, grant extra access with an Authorization, and set up machine credentials. In V1 this was "team members" plus scope-group authorizations.

Who can act on an Account

PrincipalHow it gets accessUse it for
Person (a human with a login)A Stakeholder record on the Account with a roleIdTeammates who sign in to Wingspan or your app
The principal PersonThe single Stakeholder with isPrincipal: trueThe Account's authorized representative. Holds admin authority.
ServiceAccount (a machine identity)Its owner (an Account or Organization), an optional custom roleId, and direct scopesBackends, scheduled jobs, integrations
Anyone above, outside their normal reachAn Authorization grant from the AccountAccess to a specific Account's resources beyond the role
Anyone who needs a parent's child AccountsAn Authorization on the parent with isAccountScopeInclusiveOfDescendants: trueActing on child Accounts with X-Wingspan-Account. Being the parent's principal or a Stakeholder on it doesn't reach its children by itself.

Roles are operational access only. Ownership (ownershipPercentage) and control (isController) are separate compliance attributes on the same Stakeholder record and grant no permissions. They are used for business verification; see Identity verification.

Security requirements for access changes

Changing who can do what is a high-risk action. The endpoints on this page that create or change Stakeholders, Roles, Authorizations, ServiceAccounts, and API keys require:

  • A person signed in directly. ServiceAccount API keys and impersonation sessions can't make these changes.
  • A recent step-up MFA check. Without one you get 403 with code: StepUpMfaRequired and extensions.challengeUri to start the challenge.

Roles

A role is a named set of capabilities. List roles returns the system roles plus any custom roles you can see.

curl https://api.wingspan.app/v3/platform/roles \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "X-Wingspan-Account: Nw7kQ2pLx9RtVb3mHc5dZa"

System roles have stable IDs such as role_admin and role_accounts_payable, and can't be changed or deleted. Read the list from the API rather than hard-coding it, since the catalog can grow.

Create a custom role

Create a role owned by an Account (or by an Organization, to reuse it across every Account in the Organization). Each entry in scopePermissions names a capability and the actions allowed on it. List scopes returns the capability names.

curl -X POST https://api.wingspan.app/v3/platform/roles \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "X-Wingspan-Account: Nw7kQ2pLx9RtVb3mHc5dZa" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Payables reviewer",
    "description": "Reads payees and payables. No write access.",
    "ownerType": "Account",
    "ownerId": "Nw7kQ2pLx9RtVb3mHc5dZa",
    "scopePermissions": [
      { "scope": "payments.payee", "actions": ["Read"] },
      { "scope": "payments.payable", "actions": ["Read"] }
    ]
  }'

Write includes Read. Update a custom role with PATCH /v3/platform/roles/{roleId} and remove it with DELETE /v3/platform/roles/{roleId}. Role changes fire Role.Created, Role.Updated, and Role.Deleted webhooks.

Add a teammate

A teammate is a Stakeholder with subjectType: Person and a roleId. Each Person holds at most one role per Account. For access beyond that role, add an Authorization (below).

1. Create the Stakeholder

You can create it with just an email, before the person has a Wingspan login. Use Create a Stakeholder:

curl -X POST https://api.wingspan.app/v3/platform/accounts/Nw7kQ2pLx9RtVb3mHc5dZa/stakeholders \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "subjectType": "Person",
    "email": "[email protected]",
    "roleId": "role_accounts_payable",
    "externalId": "hr-5521"
  }'
// 201 Created (trimmed)
{
  "id": "St6kPb3nXq8wLm2rVd5tHc",
  "accountId": "Nw7kQ2pLx9RtVb3mHc5dZa",
  "subjectType": "Person",
  "subjectId": null,
  "email": "[email protected]",
  "roleId": "role_accounts_payable",
  "events": { "createdAt": "2026-09-24T15:02:11Z" }
}

subjectId stays null until the invite resolves to a Person. A second Stakeholder for the same Person on the same Account returns 409 ResourceConflict.

2. Send the invite

Invite a Stakeholder. Only the Account's principal can call this.

curl -X POST https://api.wingspan.app/v3/platform/accounts/Nw7kQ2pLx9RtVb3mHc5dZa/stakeholders/St6kPb3nXq8wLm2rVd5tHc/invite \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "expiresIn": "P14D" }'

Wingspan emails the invite. The link is never returned to you. Invites last 14 days by default and 30 days at most. Set shouldSendEmail: false to skip the email; the person can then request a link from the sign-in page.

Change or remove access

ToCall
Change the rolePATCH /v3/platform/accounts/{accountId}/stakeholders/{stakeholderId} with { "roleId": "role_admin" }
Remove the teammateDELETE /v3/platform/accounts/{accountId}/stakeholders/{stakeholderId}. Returns 204. The record is kept with events.revokedAt for audit.
See who has accessGET /v3/platform/accounts/{accountId}/stakeholders

The principal

Whoever creates a root Account becomes its principal and holds admin authority. A child Account has no principal until you create a Stakeholder with isPrincipal: true. There is at most one principal per Account.

To hand the role to someone else, call Transfer the Account principal with the new principal's personId. They must already be an active Person Stakeholder on the Account. You can't make this change with a Stakeholder PATCH, and you can't revoke the only active principal.

Transfers are blocked with 409 PrincipalLockedByActiveEngagement while the Account has an Employee or EmployeeOfRecord engagement in Created, Activated, or Suspended, because the principal is the person those W-2 records attach to.

Grant extra access with an Authorization

An Authorization lets a Person or ServiceAccount use specific capabilities on an Account it wouldn't otherwise reach. The Account granting access comes from X-Wingspan-Account (or an Account-bound session), never from the body.

curl -X POST https://api.wingspan.app/v3/platform/authorizations \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "X-Wingspan-Account: Ap4tYs8KqW2nLm6xRb1cVe" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "granteeType": "Person",
    "granteeId": "Pr5sHa2kQm8tXv4nLc7dWb",
    "scopes": ["payments.payable", "payments.payee"],
    "actions": ["Read"],
    "expiresAt": "2026-12-31T23:59:59Z"
  }'
  • scopes are base names (payments.payable). Put Read or Write in actions; don't append :read or :write.
  • Instead of scopes, pass allowedScopeGroupId from List scope groups to grant a predefined bundle (the V3 form of V1 scope groups such as Finances or Documents).
  • The grant covers only the granting Account unless you set isAccountScopeInclusiveOfDescendants: true.
  • Accounts can't be grantees. Grant access to a Person or ServiceAccount instead.

Review grants with GET /v3/platform/authorizations. Change one with PATCH /v3/platform/authorizations/{authorizationId}: send only the fields you're changing, and at least one field, since an empty body returns 422. Remove one with DELETE /v3/platform/authorizations/{authorizationId}.

Machine access: ServiceAccounts and API keys

Use a ServiceAccount for any process with no person in the loop. It's owned by an Account (covering that Account and its descendants) or an Organization (covering the Accounts in it), never by a Person. Its effective access is its custom roleId plus direct scopes such as payments.payable:write. API keys issued to it can only hold a subset of those scopes.

ServiceAccount statusMeaning
CreatedNewly created. Review scopes, then activate.
ActiveCan authenticate.
InactiveSuspended. POST .../reactivate restores it.
DisabledTerminal. Disabling also disables every API key it owns.

A Person can also hold personal API keys (ownerType: Person) for their own scripting. Actions taken with those keys are attributed to that Person. For backends shared by a team, use a ServiceAccount so access doesn't depend on one employee.

Setup steps and examples are in Embed Wingspan in your app. ServiceAccount and API key lifecycle events (ServiceAccount.Created, ServiceAccount.Disabled, ApiKey.Created, ApiKey.Revoked, and others) are available as webhooks.

What can go wrong

StatuscodeWhy
403StepUpMfaRequiredThe change needs a recent MFA check.
403Forbidden or ScopeInsufficientThe caller lacks the capability, or used an API key where a direct sign-in is required.
409ResourceConflictDuplicate Stakeholder, duplicate externalId, or an Authorization create that conflicts with the grant's current state.
409PrincipalLockedByActiveEngagementPrincipal transfer or revocation while a W-2 engagement is active.
422ValidationErrorFor example, roleId or email on an Account-type Stakeholder, :write appended to an Authorization scope, or an empty body on an Authorization or API key update.

Related pages


Did this page help you?