Tax information

Collect a tax identity as a ComplianceEntity, understand how a payer-held W-9 is produced from it, and let a payee share or revoke tax information with a payer.

This page explains where a contractor's tax identity lives in the V3 API, how to collect and correct it, how the W-9 a payer holds is produced from it, and how a payee shares that information with a payer or takes it back.

This page describes how the API behaves. It isn't tax or legal advice, and it doesn't describe what the IRS or any other authority requires of you. Consult a qualified professional about your obligations.

Where tax information lives

Legal name, taxpayer ID, tax classification, and address are stored on a ComplianceEntity, not on the Payee or the Account profile. One record holds the tax identity, and its verification history stays with it.

Who owns the ComplianceEntityWhen it's used
The contractor's own Account (a business) or their Person (an individual)The contractor entered their own tax information. This is the usual case once they've linked.
The payer's Account, with subject: { "type": "Payee", "id": "<payeeId>" }The payer supplied tax information for a Payee who hasn't linked or hasn't entered it themselves.

The Payee's read-only identitySource tells you which is in use: PayerProvided when a payer-supplied record is attached, or PayeeOwned when the Payee is linked to an Account and no payer-supplied record is attached. It isn't a verification result.

Collect tax information

A contractor creates their ComplianceEntity with POST /v3/platform/compliance-entities. type (Individual or Business) and jurisdictionCountry are required.

An individual (a sole proprietor using their SSN):

curl -X POST https://api.wingspan.app/v3/platform/compliance-entities \
  -H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "Individual",
    "jurisdictionCountry": "US",
    "individualLegalName": { "givenName": "Priya", "familyName": "Shah" },
    "dateOfBirth": "1990-04-12",
    "physicalAddress": {
      "line1": "100 Example St", "city": "Austin", "state": "TX",
      "postalCode": "78701", "country": "US"
    },
    "taxIdentifiers": [
      { "countryCode": "US", "type": "SSN", "value": "000000000" }
    ]
  }'

A business:

{
  "type": "Business",
  "jurisdictionCountry": "US",
  "legalName": "Shah Care Consulting LLC",
  "form": "LimitedLiability",
  "taxClassification": "TaxedAsDisregardedEntity",
  "formationJurisdiction": { "countryCode": "US", "subdivisionCode": "US-TX" },
  "physicalAddress": { "line1": "100 Example St", "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US" },
  "taxIdentifiers": [ { "countryCode": "US", "type": "EIN", "value": "000000000" } ]
}
// 201 Created (trimmed)
{
  "id": "TRX4OwJ0iZI280HtBM5V1I",
  "ownerType": "OwnerAccount",
  "ownerId": "qY0t19FmElxN3ur1EiW2Ov",
  "type": "Business",
  "jurisdictionCountry": "US",
  "legalName": "Shah Care Consulting LLC",
  "taxIdentifiers": [ { "countryCode": "US", "type": "EIN", "hasValue": true } ],
  "usTaxStatus": "Undetermined",
  "startDate": "2026-09-24T17:00:00Z",
  "endDate": null,
  "previousId": null,
  "nextId": null
}

Things to know:

  • Tax IDs are write-only. Reads show hasValue: true, never the number, not even masked. Don't build a screen that expects to display a stored SSN or EIN.
  • Who owns it. An Individual record without a subject belongs to the Person, so create it without X-Wingspan-Account. A Business record belongs to the Account.
  • One current record per subject. A second create returns 409 ResourceConflict. Use advance to replace it (below).
  • legalAddress, address, stakeholders, and non-empty metadata aren't supported on create yet and return 422. Use physicalAddress, and record owners as Stakeholders (see Identity verification).
  • usTaxStatus (UsPerson, ForeignPerson, or Undetermined) is computed by Wingspan from the tax document on file and fatcaChapter4Status.

Then verify it with POST /v3/platform/compliance-entities/{complianceEntityId}/verify and {"level": "Tax"}. See Identity verification.

Correct or replace tax information

  • Before any verification passes, fix mistakes in place with PATCH /v3/platform/compliance-entities/{complianceEntityId}.
  • After a verification passes, minor changes such as a new address or trade name still PATCH in place. A material change, such as a new legal name, date of birth, entity form or classification, jurisdiction, or a different value for an existing tax ID, returns 409 with detailCode: users.MaterialChangeRequiresAdvance.
  • For a material change, call POST /v3/platform/compliance-entities/{complianceEntityId}/advance with the complete new record. Wingspan end-dates the current record (it still covers the period it applied to), creates a successor linked through previousId and nextId, and starts the successor's verification from NotStarted. The successor must have the same type as the record it replaces. effectiveDate defaults to now and can't be in the future.

This keeps history intact. For example, when a sole proprietor forms an LLC midyear, the earlier record still describes who they were for the first part of the year.

GET /v3/platform/compliance-entities returns the chain newest first; the first record is current and has endDate: null.

Payer-supplied tax information

If you have a Payee's tax information and the Payee hasn't entered it, you can create a ComplianceEntity under your own Account with subject: { "type": "Payee", "id": "<payeeId>" }, then Tax-verify it. The subject can't be changed later.

{
  "subject": { "type": "Payee", "id": "M9ISYbzJElXs4zIHv76rjT" },
  "type": "Individual",
  "jurisdictionCountry": "US",
  "individualLegalName": { "givenName": "Priya", "familyName": "Shah" },
  "physicalAddress": { "line1": "100 Example St", "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US" },
  "taxIdentifiers": [ { "countryCode": "US", "type": "SSN", "value": "000000000" } ]
}

To find it again, list with both filter[subjectType][eq]=Payee and filter[subjectId][eq]=<payeeId>. A W-9 produced from your record has no source.shareId, because the payee didn't share it.

Which W-9 Wingspan uses for a Payee depends on the Payee's w9SourcePolicy: PayeeSupplied, PayerSupplied, or PayeeSuppliedFallbackToPayer. The last is the default for new Payees: it uses the payee's own shared information and falls back to yours when theirs isn't shared or is incomplete. Your record isn't deleted when the payee's takes over. See Payees.

Choose whose tax identity a read shows

When both your record and the payee's own ComplianceEntity exist, complianceEntitySourceStrategy on the Payee chooses which one a Payee read shows. It changes what you see, not which W-9 is used (that's w9SourcePolicy, above).

complianceEntitySourceStrategyWhat ?expand=ComplianceEntity shows
Automatic (default)The payee's own ComplianceEntity when their Account is linked and has one. Otherwise the one you supplied.
PayerProvidedOnly the one you supplied.
PayeeProvidedOnly the payee's own.

Set it with PATCH /v3/payments/payees/{payeeId}:

curl -X PATCH https://api.wingspan.app/v3/payments/payees/M9ISYbzJElXs4zIHv76rjT \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "complianceEntitySourceStrategy": "PayeeProvided" }'

PayeeProvided needs the linked payee Account to have a ComplianceEntity, and PayerProvided needs payerSuppliedComplianceEntityId to be set. Otherwise the update returns 422 with detailCode: payments.PinnedComplianceEntityMissing. Reads never fail because of the setting: if the chosen side has nothing, the expanded read simply leaves complianceEntity out. Automatic tries both sides, and the other two try only their own.

Read it back with GET /v3/payments/payees/{payeeId}?expand=ComplianceEntity:

// 200 OK (trimmed)
{
  "id": "M9ISYbzJElXs4zIHv76rjT",
  "complianceEntitySourceStrategy": "PayeeProvided",
  "complianceEntitySource": "PayeeProvided",
  "complianceEntity": {
    "id": "TRX4OwJ0iZI280HtBM5V1I",
    "type": "Business",
    "legalName": "Shah Care Consulting LLC",
    "form": "LimitedLiability"
  }
}

The inlined summary carries names, legal form, business details, occupation, phone, and addresses. It never includes tax IDs, dates of birth, government IDs, verification state, or due-diligence answers. Read those through the ComplianceEntity itself.

The payee has the same setting on their side. On their Payer record, complianceEntitySourceStrategy chooses between the payer's own ComplianceEntity and one the payee supplied, and GET /v3/payments/payers/{payerId}?expand=ComplianceEntity shows the result.

How a W-9 is produced

Wingspan produces the W-9 a payer holds as a Form, generated from a Tax-verified ComplianceEntity. These forms have:

  • type: W9 and source.type: ComplianceEntity, with source.complianceEntityId naming the record it came from.
  • status: Effective as soon as they're created. They're read-only to you; they change only when their source changes.
  • validFrom and validTo, matching the source record's startDate and endDate.
  • subject: { type: Payee, id } when the form is about one of your Payees.

At most one current projected W-9 exists for each pair of payer Account and source ComplianceEntity.

What happens to the sourceWhat happens to the W-9
A payee shares, and their record is Tax-verifiedA W-9 is created in the payer's account.
The payee's record is advanced to a successorThe W-9 becomes Superseded. A new one is created from the successor once it's Tax-verified.
The record is no longer Tax-verified, or the share is revokedThe W-9 becomes Expired. It isn't deleted.

List them with GET /v3/compliance/forms:

curl -G https://api.wingspan.app/v3/compliance/forms \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  --data-urlencode "filter[type][eq]=W9" \
  --data-urlencode "filter[sourceType][eq]=ComplianceEntity" \
  --data-urlencode "filter[isLatest][eq]=true"

Creating a W-9 directly (source.type: Direct) is part of the Form model, but direct form creation isn't available in the V3 API yet. See Tax filing for how W-9 data feeds 1099s.

Share tax information with a payer

A payee controls whether a payer can use their tax identity. They grant it on their Payer record for that payer with POST /v3/payments/payers/{payerId}/share-tax-information.

The share is standing: it covers the payee's current ComplianceEntity and every future one, until revoked. Each covered record that is Tax-verified produces a W-9 in the payer's account.

Only the payee can do this. The call must come from the payee's own session. A request with X-Wingspan-Account is rejected with 403 before anything happens, so a payer can't grant itself access.

curl -X POST https://api.wingspan.app/v3/payments/payers/KJn4Kzb9lKAwczssFEvqhI/share-tax-information \
  -H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "consent": {
      "attestationStatement": "<the exact statement shown to the payee>",
      "attestedAt": "2026-09-24T17:05:00Z",
      "signature": "Priya Shah",
      "consentVersion": "2026.1",
      "ipAddress": "203.0.113.24"
    }
  }'
FieldRequiredWhat it is
consent.attestationStatementYesThe statement the payee agreed to, exactly as shown to them.
consent.attestedAtYesWhen they agreed.
consent.signatureYesTheir electronic signature (typed name).
consent.consentVersionYesYour version identifier for the statement.
consent.ipAddressNoWhere they agreed from, if you captured it.
previousComplianceEntityIdsNoOlder, no-longer-current records to share as well, for earlier tax periods. They must belong to the payee.
// 200 OK (trimmed)
{
  "id": "KJn4Kzb9lKAwczssFEvqhI",
  "taxInformationShare": {
    "id": "juWeGJ1qIw9m8ZDHTfNAs0",
    "status": "Active",
    "complianceEntities": [
      { "id": "TRX4OwJ0iZI280HtBM5V1I", "projectionStatus": "Projected", "formId": "yu1CZ2X7E7z4psGGOMWqfU" }
    ]
  },
  "events": { "taxInformationSharedAt": "2026-09-24T17:05:01Z" }
}

projectionStatus is Projected (a W-9 exists; see formId), PendingVerification (waiting for Tax verification), or Expired.

Each projected W-9 records the share it came from in source.shareId, and its events.certifiedAt is set from the share's attestation and signature. W-9s from payer-supplied records have no shareId and no certifiedAt.

Calling share-tax-information again replaces the share's settings. Removing an entry from previousComplianceEntityIds expires only that record's W-9. A ComplianceEntity ID that doesn't belong to the payee returns 422 with detailCode: users.ComplianceEntityNotOwnedByGrantor, whether the record doesn't exist or belongs to someone else.

The payee may also need to accept agreements such as W9Certification and ElectronicTaxFormConsent. See Record agreement acceptance.

Revoke a share

POST /v3/payments/payers/{payerId}/revoke-tax-information, again from the payee's own session, sets taxInformationShare.status to Revoked and expires every W-9 produced from the share. Revoking is final for that share. It returns 409 if there's no active share.

Both sides can read the share on the relationship (Payer.taxInformationShare for the payee). Only the payee can change it.

Webhooks

You can subscribe to ComplianceEntity.Created and ComplianceEntity.Updated. Each routes to the Account or Person that owns the record, and the payload identifies the ComplianceEntity, so read it with GET /v3/platform/compliance-entities/{complianceEntityId} for details.

Payer.TaxInformationShared, Payer.TaxInformationRevoked, and the Form.* events aren't available for subscription yet. Poll GET /v3/compliance/forms or the Payer record until they are.

Common mistakes

  • Storing tax IDs on the Payee or in metadata. They belong on a ComplianceEntity, where they're tokenized.
  • Expecting to read back an SSN or EIN. Tax IDs are write-only.
  • PATCHing a legal name after verification. Use advance; the 409 tells you so.
  • Calling share-tax-information from the payer's backend. It must be the payee's own session. Build it into the contractor's onboarding screens.
  • Deleting an expired W-9 to "clean up". Expired and superseded forms are the history for earlier periods. Leave them.

Related


Did this page help you?