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.
| Environment | API base URL | Web app |
|---|---|---|
| Production | https://api.wingspan.app | https://my.wingspan.app |
| Sandbox | https://stagingapi.wingspan.app | https://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:
| Header | Value | When |
|---|---|---|
Authorization | Bearer <token> | Every authenticated call. The token is a session token, an API key secret, or an OAuth access token. |
Content-Type | application/json | Any request with a body. (The OAuth token endpoint is the exception: it takes form-encoded bodies, as OAuth requires.) |
Idempotency-Key | A unique string you generate, such as a UUID | Creates and anything that moves money. See Idempotency. |
X-Wingspan-Account | An Account ID | Only 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
| Method | Who it's for | How you get it |
|---|---|---|
| Session token | A person logging in: your app's users, or you while exploring the API | POST /v3/platform/sessions with email and password |
| API key | Scripts and server code acting as a person or a service account | POST /v3/platform/api-keys |
| Service account plus API key | Backend integrations with no human in the loop | POST /v3/platform/service-accounts, then an API key owned by it |
| OAuth 2.0 access token | Third-party apps that act on a Wingspan customer's behalf with their consent | POST /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:
| Call | What it does |
|---|---|
POST /v3/platform/sessions/refresh | Trades 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/otp | Passwordless 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:
secretis returned once, on this response only. Store it in your secrets manager right away. If you lose it, rotate the key withPOST /v3/platform/api-keys/{apiKeyId}/rotate. Replaying the create with the sameIdempotency-Keyreturns409 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).
-
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
Createdso you can review its access. -
Activate it with
POST /v3/platform/service-accounts/{serviceAccountId}/activate. -
Create an API key for it:
POST /v3/platform/api-keyswith"ownerType": "ServiceAccount"and"ownerId"set to the service account's ID. The key'sscopesmust 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
| Status | code | What it means | What to do |
|---|---|---|---|
401 | Unauthenticated | The 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. |
401 | TokenExpired | The session expired. | Refresh it, or log in again. |
403 | StepUpMfaRequired | This action needs recent MFA. | Complete the challenge in extensions, then retry. |
403 | Forbidden, ScopeInsufficient | Your credential lacks the role or scope. | Grant the scope, or use a credential that has it. |
403 | AccountMismatch | X-Wingspan-Account names an Account you can't act on. | Check the ID and your access. |
400 | AccountScopeNotApplicable | You sent X-Wingspan-Account to a person-only endpoint, or used an Account session there. | Drop the header, or use a person's session. |
429 | RateLimitExceeded | You're over a rate limit. | Wait Retry-After seconds. See Rate limiting. |
Every error body has the same shape. See Errors.
Next steps
Updated 10 days ago