Errors

The Wingspan V3 error format, every error code and HTTP status, and which errors to retry, with what backoff.

Handle Wingspan V3 errors consistently by checking the HTTP status and code, then retrying only the responses that support retries. Every V3 endpoint returns errors in the same format, so one error handler covers the whole API.

The error format

Errors come back with an HTTP status of 400 or higher and a body of type application/problem+json, following RFC 9457:

{
  "type": "https://api.wingspan.app/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "Request failed validation.",
  "code": "ValidationError",
  "requestId": "3f2c9a7e1b4d4e0f9a8c6b5d2e1f0a93",
  "errors": [
    { "field": "dueDate", "code": "Required" },
    { "field": "lineItems", "code": "TooShort" }
  ]
}
FieldWhat it's for
typeA stable URI for the kind of error.
titleA short, human-readable name for the error.
statusThe HTTP status, repeated in the body.
detailAn explanation for a person reading logs. Its wording can change, so don't parse it.
codeThe machine-readable error code. Branch your code on this field.
requestIdThe ID of this request. Log it, and quote it when you contact support.
errorsPresent on 422 only. One entry per field that failed, each with field, a code, and sometimes a message.
detailCodeOptional. A finer reason from the service that handled the request, such as payments.PayableCurrencyNotAllowed. Useful for logs and support.
extensionsOptional. Structured data for specific codes, such as the bucket that was exhausted on a 429.

Branch on code, not detailCode

code comes from a fixed list (below) and keeps its meaning. Services can add detailCode values without notice, so your code can encounter values it does not recognize. Use detailCode for logging, dashboards, and support tickets. Make decisions on code and status. If you inspect detailCode, handle unknown values by falling back to code.

detailCode always has the form {service}.{Reason}. The service part tells you which area handled the request: payments, finance, onboarding, compliance, users (identity and Accounts), integrations (accounting connections), operations (async operations), or revenue (plans and subscriptions). Quote requests use the older prefixes currency and amount.

Nothing is dropped silently

If a request contains a field, query parameter, filter, or expand value that the operation doesn't accept, the request fails with 422 ValidationError and the problem is listed in errors[]. Wingspan doesn't ignore the bad part and return success. When a request succeeds, every field you sent was accepted.

HTTP status codes

StatusMeaning
400The body isn't valid JSON (code: ValidationError, no errors[]). Also AccountScopeNotApplicable when you send X-Wingspan-Account to an endpoint that doesn't accept it, and BadRequest on a few operations that require a header.
401The token is missing, invalid, or expired.
402A payment rail returned a failure (PaymentFailed).
403You're authenticated but not allowed to do this.
404The resource doesn't exist, or you can't see it. Wingspan returns 404 rather than 403 so that IDs can't be used to discover other customers' records.
405The path exists but doesn't support this HTTP method. The Allow header lists the methods it does support.
409The request conflicts with the current state: a duplicate, a transition the resource can't make, or an Idempotency-Key in use.
410An invite token has expired or has already been used.
412The If-Match value no longer matches the resource. See Concurrency and ETags.
413The request body is larger than 1 MB (code: ValidationError).
422The request is well formed but a value is invalid. See errors[] when it's present.
423The Account is temporarily not accepting V3 writes.
429You've hit a rate limit. See Rate limiting.
500Rare. An internal consistency check failed. Most unexpected failures return 503.
501The operation is listed in the API but is currently unavailable.
503Wingspan or one of its dependencies failed, usually temporarily. Most unexpected failures return this status. Safe to retry.

A missing or wrong Content-Type returns 422 ValidationError. Send Content-Type: application/json on every request with a body.

Error codes

codeStatusWhen you'll see it
BadRequest400Returned by a few operations when a required header is missing.
AccountScopeNotApplicable400You sent X-Wingspan-Account to an endpoint that doesn't use it, such as a Person-only or global endpoint.
Unauthenticated401The bearer token is missing or invalid.
TokenExpired401The token was valid but has expired.
ReplicationLagAccountUnready401Reserved. It's part of the API's code list, but the API doesn't currently return it. Treat it like Unauthenticated if you ever see it.
PaymentFailed402The payment rail rejected the funds movement.
Forbidden403You don't have the role or permission for this action, or the Account doesn't have the product the operation needs.
ScopeInsufficient403Your token or API key doesn't include a scope this operation requires.
AccountMismatch403The X-Wingspan-Account value, or the Account in the path, is an Account you can't act on.
StepUpMfaRequired403This action needs a recent multi-factor check. Complete the challenge named in extensions and retry.
ResourceNotFound404The resource doesn't exist or isn't visible to you.
MethodNotAllowed405The path doesn't support this method. See the Allow header.
InvalidStateTransition409The action needs the resource to be in a different status, such as paying an invoice that's still a draft.
ResourceConflict409A duplicate, such as a second record with the same externalId, or another uniqueness conflict.
ResourceLocked409The resource is in the middle of a transition. Retry shortly.
IdempotencyKeyConflict409The Idempotency-Key was used with a different body, or the first request with that key is still running. See Idempotency.
EligibilityBlocked409The payee or engagement has unmet blocking requirements.
RequirementNotFulfilled409A specific requirement the action needs hasn't been completed.
PrincipalLockedByActiveEngagement409The Account's principal can't change while an employee engagement is active.
InsufficientFunds409The funding source doesn't cover the amount.
EventCursorExpired409An event-log cursor is too old or doesn't match your query. See Recover missed events.
WebhookSecretRotationInProgress409A webhook secret rotation is already in its grace period.
InviteTokenExpired410The invite token has expired or has already been redeemed. Send a new invite.
PreconditionFailed412Your If-Match value doesn't match the resource's current ETag.
ValidationError422One or more values are invalid. See errors[].
MissingRequiredField422A single required field is missing.
InvalidFormat422A single field has the wrong format.
PrincipalRequired422An employee engagement needs the Account to have a principal Person first.
OwnerRequired422An API key create needs an explicit owner.
AccountQuiescedForV3423The Account is temporarily read-only in V3. Reads still work. Contact support.
RateLimitExceeded429A rate-limit bucket is empty. Wait for Retry-After.
InternalError500An internal consistency check failed. Quote requestId to support if it keeps happening.
NotImplemented501The operation is currently unavailable. Retrying won't help.
ServiceUnavailable503A dependency is temporarily down. Retry with backoff.
PaymentRailUnavailable503A payment rail is temporarily unavailable. Retry with backoff.

Field error codes

Each entry in errors[] has one of these codes:

errors[].codeMeaning
RequiredThe field is missing.
InvalidFormatThe value has the wrong type or format.
OutOfRangeA number is too small or too large.
TooShort, TooLongA string or list is too short or too long.
DuplicateValueA value that must be unique is repeated.
UnsupportedValueThe value isn't one of the allowed values, or the field isn't accepted by this operation.
ConflictingFieldsTwo fields that can't be used together were both sent.
EligibilityBlockedThe field refers to something that isn't eligible yet.
NotApplicableForMemberTypeThe field doesn't apply to this kind of record.
RoundingResidualSplit percentages don't add up to 100% and no remainder is set.
CurrencyMismatchAmounts on one document use more than one currency.
MismatchA value doesn't match its verified source.
ResourceNotFoundThe field refers to a resource that doesn't exist.

Handling errors well

Which errors to retry

ResponseRetry?How
429 RateLimitExceededYesWait the number of seconds in Retry-After, then retry. See Rate limiting.
503 ServiceUnavailable, 503 PaymentRailUnavailableYesExponential backoff with jitter. Use extensions.retryAfter or Retry-After if present.
500 InternalErrorA few timesExponential backoff with jitter. If it keeps failing, stop and contact support with the requestId.
Network timeout, connection resetYesRetry with the same Idempotency-Key so the work isn't repeated.
409 IdempotencyKeyConflict with extensions.retryAfterYesThe first request with this key is still running. Wait, then retry with the same key and body.
409 ResourceLockedYesRetry after a short delay.
401 TokenExpiredAfter refreshingGet a new token, then retry.
412 PreconditionFailedAfter re-readingGET the resource, reapply your change to the current version, and retry with the new ETag.
409 IdempotencyKeyConflict without retryAfterNoYou reused a key with a different body. Send a new request with a new key.
409 InvalidStateTransition, ResourceConflict, EligibilityBlocked, RequirementNotFulfilled, InsufficientFundsNoRead the resource, fix the underlying state, then send a new request.
400, 413, 422NoFix the request using errors[] and detail. For a 413, send a smaller body.
401 Unauthenticated, 403, 404NoFix the token, permissions, X-Wingspan-Account, or ID.
423 AccountQuiescedForV3NoContact support.
501 NotImplementedNoThe operation isn't available.

Start exponential backoff at about one second and double the delay after each attempt. Add random jitter, cap the delay at about 30 seconds, and stop after a limited number of attempts. Always honor Retry-After when it is present.

Retrying creates and money movement

Send an Idempotency-Key on every create and every money-moving request, and reuse the same key when you retry. Wingspan stores the response and replays it, so a retry doesn't create a second record or move money twice.

Responses with a 5xx status aren't stored, so a retry after a 5xx runs the request again. The first attempt may have finished before the error. Before you retry a create after a 5xx, look the record up by its externalId if you set one, or re-read the resource you were changing. See Idempotency.

429 compared with 503

A 429 means you sent more requests than your quota allows. Slow down and follow Retry-After. A 503 means Wingspan or one of its providers is temporarily unavailable, and it has nothing to do with your request volume. Back off and retry. Wingspan never returns 429 for its own outages, so a 429 always means you should reduce your request rate.

409 compared with 412

A 409 means the request conflicts with the resource's state: the resource is in the wrong status, or a record like it already exists. Resending the same request won't help until the state changes. A 412 means someone changed the resource after you read it. Your request may still be valid, but it was written against an old version. Re-read the resource, check that your change still makes sense, and retry with the new ETag.

Use requestId with support

Log requestId with every error, alongside the method, path, and status. When you contact support, include the requestId and the time of the request. It lets us find the exact request in our logs without you sending payloads that may contain personal or financial data.

Check every item in a bulk operation

A batch can finish while individual items fail. After a batch completes, list its items with filter[status][anyOf][]=Failed and read each item's statusReason. See Batches and bulk operations.

From V1

  • V1 errors had a numeric code and a message. V3 errors have an HTTP status, a string code from the table above, and per-field errors[] on 422.
  • V1 returned 400 for most validation failures. V3 uses 400 only for requests that can't be read, and 422 for invalid values.
  • V1 returned 208 Already Reported for a duplicate bulk payable item. V3 returns the original response when you retry with the same Idempotency-Key, and 409 ResourceConflict for a duplicate externalId.
  • V1 put bulk item errors in metadata.errorMessage. V3 puts them in each batch item's statusReason.

Related pages


Did this page help you?