Environments and authentication

This page shows you where to send V3 API requests, how to get a credential, and how to make your first authenticated call.

Environments

Wingspan runs two environments. Use the sandbox to build and test, and production for real money.

EnvironmentAPI base URLWeb app
Productionhttps://api.wingspan.apphttps://my.wingspan.app
Sandboxhttps://stagingapi.wingspan.apphttps://staging-my.wingspan.app

Every V3 path starts with /v3/, so a full URL looks like https://api.wingspan.app/v3/payments/payees. Accounts, credentials, and data don't carry over between environments.

Getting a sandbox. Your Wingspan team sets up your sandbox Account for the flows you plan to build, because some features need configuration before they work there. Before you start, tell us which flows you want to test (for example, inviting contractors, paying payables, or collecting invoices). We'll configure the sandbox, confirm those flows work, and send you instructions for testing them. If something doesn't work the way you expect, send us the request, the response, and its requestId.

Headers

Send these on every request:

HeaderValueWhen
AuthorizationBearer <token>Every authenticated call. The token is a session token, an API key secret, or an OAuth access token.
Content-Typeapplication/jsonAny request with a body. (The OAuth token endpoint is the exception: it takes form-encoded bodies, as OAuth requires.)
Idempotency-KeyA unique string you generate, such as a UUIDCreates and anything that moves money. See Idempotency.
X-Wingspan-AccountAn Account IDOnly when you act on an Account other than your own. See Acting on behalf of Accounts.

Your own ID is never part of a URL. Wingspan works out who you are from the token, and there is no /me endpoint.

Choose a way to authenticate

MethodWho it's forHow you get it
Session tokenA person logging in: your app's users, or you while exploring the APIPOST /v3/platform/sessions with email and password
API keyScripts and server code acting as a person or a service accountPOST /v3/platform/api-keys
Service account plus API keyBackend integrations with no human in the loopPOST /v3/platform/service-accounts, then an API key owned by it
OAuth 2.0 access tokenThird-party apps that act on a Wingspan customer's behalf with their consentPOST /v3/platform/oauth/token

For a server-to-server integration, use a service account and an API key. For a quick test, start with a session token.

Important: Tokens from the V1 or V2 API, and tokens copied from the V1 web app, don't work on V3 endpoints. They return 401 Unauthenticated. Create a V3 session or API key. The first time a person signs in through V3, their Account moves to V3, so read what happens to your Account before you sign in to production.

Log in with a session

POST /v3/platform/sessions exchanges an email and password for a session token. It doesn't need an Authorization header.

curl -X POST https://stagingapi.wingspan.app/v3/platform/sessions \
  -H "Content-Type: application/json" \
  -d "{
    \"sessionType\": \"Person\",
    \"email\": \"[email protected]\",
    \"password\": \"$WINGSPAN_PASSWORD\"
  }"
// 201 Created (trimmed)
{
  "id": "Sn4bT8yK1pWq_Mz6RxLd2v",
  "sessionType": "Person",
  "token": "[redacted]",
  "refreshToken": "[redacted]",
  "personId": "Hs1mX6kQ9wLp_Rv3ZtNc8a",
  "expiresAt": "2026-09-24T18:00:00Z",
  "boundAccountId": null,
  "accountAccess": [
    { "accountId": "Nw7kQp2LmX9vRt4ZcY1bHd", "isPrincipal": true, "roleIds": [], "scopes": [], "allowedScopeGroupIds": [], "scopePermissions": [] }
  ]
}

Send token as Authorization: Bearer <token> on later calls. accountAccess lists the Accounts this person can act on, which is where you find your Account ID. Never log token or refreshToken.

Related session calls:

CallWhat it does
POST /v3/platform/sessions/refreshTrades refreshToken for a new token and a new refreshToken. Each refresh token works once. Reusing a spent one revokes the session.
POST /v3/platform/sessions/otp and PATCH /v3/platform/sessions/otpPasswordless login: send a one-time code by email or SMS, then verify it to get a session.
PATCH /v3/platform/sessions/{sessionId}Binds your session to one Account (see below) and returns a replacement token.
DELETE /v3/platform/sessions/{sessionId}Logs out. If a retry of this call returns 401, the logout already worked.

If your account requires MFA, login returns 403 with code: StepUpMfaRequired. The same code appears on some sensitive calls, such as creating an API key or paying a payable, when your session hasn't completed MFA recently. The response's extensions tell you which challenge to complete through /v3/platform/mfa-challenges. Complete it and retry the call.

Create an API key

An API key is a long-lived credential for code. You create it while logged in, and it acts as the person or service account that owns it.

curl -X POST https://stagingapi.wingspan.app/v3/platform/api-keys \
  -H "Authorization: Bearer $WINGSPAN_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "name": "Northwind payroll sync (sandbox)" }'
// 201 Created (trimmed)
{
  "id": "Ak7pW2rN5tQz_Lm8XcVb3d",
  "name": "Northwind payroll sync (sandbox)",
  "status": "Active",
  "ownerType": "Person",
  "ownerId": "Hs1mX6kQ9wLp_Rv3ZtNc8a",
  "scopes": [],
  "secret": "[redacted]"
}

Important: secret is returned once, on this response only. Store it in your secrets manager right away. If you lose it, rotate the key with POST /v3/platform/api-keys/{apiKeyId}/rotate. Replaying the create with the same Idempotency-Key returns 409 ResourceConflict, not the secret.

Use the secret as a bearer token:

export WINGSPAN_TOKEN="$WINGSPAN_API_KEY"
curl https://stagingapi.wingspan.app/v3/platform/api-keys \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"

A 200 with a list of your keys means the key works. Keys can carry an expiresAt. To rename a key or change its scopes or expiry, use PATCH /v3/platform/api-keys/{apiKeyId} with at least one field (an empty body returns 422). DELETE /v3/platform/api-keys/{apiKeyId} revokes a key immediately.

Use a service account for backend integrations

A service account is a machine identity owned by an Account (or an Organization). Its permissions come from a role (roleId) plus any extra scopes you grant, written as domain.resource:read or domain.resource:write (for example, payments.payable:write).

  1. Create the service account with POST /v3/platform/service-accounts:

    curl -X POST https://stagingapi.wingspan.app/v3/platform/service-accounts \
      -H "Authorization: Bearer $WINGSPAN_SESSION_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "name": "Payroll sync",
        "ownerType": "Account",
        "ownerId": "Nw7kQp2LmX9vRt4ZcY1bHd",
        "scopes": ["payments.payee:write", "payments.payable:write"]
      }'

    It starts in Created so you can review its access.

  2. Activate it with POST /v3/platform/service-accounts/{serviceAccountId}/activate.

  3. Create an API key for it: POST /v3/platform/api-keys with "ownerType": "ServiceAccount" and "ownerId" set to the service account's ID. The key's scopes must stay within the service account's own.

Disabling a service account also disables every API key it owns. Inactivating it makes its keys stop working until you reactivate it.

Mint a short-lived session for one Account

A service account can mint a session bound to a single Account with POST /v3/platform/accounts/{accountId}/sessions. This is useful when you embed Wingspan and want to hand a narrowly scoped token to a front end. Send X-Wingspan-Account equal to the accountId in the path. You can set its expiresAt up to one hour ahead. The session acts as the Account, not a person, so endpoints that need a Person (such as a person's notification inbox) reject it with 400 AccountScopeNotApplicable. Set shouldIncludeDescendants: true to let it act on that Account's children too.

OAuth 2.0 for connected apps

If you're building an app that other Wingspan customers connect to, use OAuth 2.0 instead of asking them for an API key. Wingspan supports the authorization_code (with PKCE), refresh_token, and client_credentials grants at POST /v3/platform/oauth/token. Clients discover the endpoints and signing keys from https://api.wingspan.app/.well-known/openid-configuration and https://api.wingspan.app/.well-known/oauth-authorization-server. Contact Wingspan to register an OAuth client. A client can also identify itself with an https URL as its client_id, pointing to a client ID metadata document it hosts. For those clients, the consent screen shows the host of that URL next to the app's name and logo, so the person approving can see where the app comes from.

Acting on behalf of another Account

By default, a request acts on the Account your credential belongs to. To act on a child Account, or on an Account that has granted you access, add X-Wingspan-Account:

curl https://api.wingspan.app/v3/payments/payees \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "X-Wingspan-Account: Ch6wP3nL8qTr_Kv1ZxMb4s"

Everything in that request (reads, writes, webhooks, and rate limits) applies to the target Account. If you can't act on it, you get 403 AccountMismatch. Endpoints that belong to a person, such as /v3/platform/persons/{personId}/notifications, reject the header with 400 AccountScopeNotApplicable.

A person's session can also be bound to one Account with PATCH /v3/platform/sessions/{sessionId}. A bound session acts on that Account when you leave out the header. Acting on behalf of Accounts covers the full rules, including caching.

What can go wrong

StatuscodeWhat it meansWhat to do
401UnauthenticatedThe token is missing, malformed, revoked, wrong for this environment, or a V1/V2 token.Check the header, that you're using a sandbox token against the sandbox, and that the token came from a V3 session or API key.
401TokenExpiredThe session expired.Refresh it, or log in again.
403StepUpMfaRequiredThis action needs recent MFA.Complete the challenge in extensions, then retry.
403Forbidden, ScopeInsufficientYour credential lacks the role or scope.Grant the scope, or use a credential that has it.
403AccountMismatchX-Wingspan-Account names an Account you can't act on.Check the ID and your access.
400AccountScopeNotApplicableYou sent X-Wingspan-Account to a person-only endpoint, or used an Account session there.Drop the header, or use a person's session.
429RateLimitExceededYou're over a rate limit.Wait Retry-After seconds. See Rate limiting.

Every error body has the same shape. See Errors.

Next steps


Did this page help you?