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 ComplianceEntity | When 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
Individualrecord without asubjectbelongs to the Person, so create it withoutX-Wingspan-Account. ABusinessrecord belongs to the Account. - One current record per subject. A second create returns
409 ResourceConflict. Useadvanceto replace it (below). legalAddress,address,stakeholders, and non-emptymetadataaren't supported on create yet and return422. UsephysicalAddress, and record owners as Stakeholders (see Identity verification).usTaxStatus(UsPerson,ForeignPerson, orUndetermined) is computed by Wingspan from the tax document on file andfatcaChapter4Status.
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
PATCHin 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, returns409withdetailCode: users.MaterialChangeRequiresAdvance. - For a material change, call
POST /v3/platform/compliance-entities/{complianceEntityId}/advancewith the complete new record. Wingspan end-dates the current record (it still covers the period it applied to), creates a successor linked throughpreviousIdandnextId, and starts the successor's verification fromNotStarted. The successor must have the sametypeas the record it replaces.effectiveDatedefaults 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).
complianceEntitySourceStrategy | What ?expand=ComplianceEntity shows |
|---|---|
Automatic (default) | The payee's own ComplianceEntity when their Account is linked and has one. Otherwise the one you supplied. |
PayerProvided | Only the one you supplied. |
PayeeProvided | Only 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: W9andsource.type: ComplianceEntity, withsource.complianceEntityIdnaming the record it came from.status: Effectiveas soon as they're created. They're read-only to you; they change only when their source changes.validFromandvalidTo, matching the source record'sstartDateandendDate.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 source | What happens to the W-9 |
|---|---|
| A payee shares, and their record is Tax-verified | A W-9 is created in the payer's account. |
| The payee's record is advanced to a successor | The 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 revoked | The 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"
}
}'| Field | Required | What it is |
|---|---|---|
consent.attestationStatement | Yes | The statement the payee agreed to, exactly as shown to them. |
consent.attestedAt | Yes | When they agreed. |
consent.signature | Yes | Their electronic signature (typed name). |
consent.consentVersion | Yes | Your version identifier for the statement. |
consent.ipAddress | No | Where they agreed from, if you captured it. |
previousComplianceEntityIds | No | Older, 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. Useadvance; the409tells you so.- Calling
share-tax-informationfrom 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
- Identity verification
- Requirements and eligibility: the
TaxVerificationrequirement. - 1099 overview
- Recipient information logic
Updated 10 days ago