Files and documents
Upload files to the Wingspan V3 vault, attach them to other resources by ID, download PDFs through signed redirects, and share secure links.
This page shows you how to upload a file to Wingspan, attach it to other records, download documents such as invoice PDFs, and share a document with someone outside Wingspan.
Every file lives in one place, the vault at /v3/compliance/vault-files. Other resources never accept file bytes. They point at a vault file by its ID. That keeps one upload path, one set of access rules, and one record of each file, however many places use it.
Upload a file
Uploading takes three calls: reserve an upload, send the bytes, then register the file.
-
Reserve an upload. Tell Wingspan the file's name, type, and size in bytes. Files can be up to 25 MiB (26,214,400 bytes).
curl -X POST "https://api.wingspan.app/v3/compliance/vault-files/uploads" \ -H "Authorization: Bearer $WINGSPAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "filename": "certificate-of-insurance.pdf", "mimeType": "application/pdf", "contentLength": 48213 }'// (trimmed) { "uploadId": "e8GtY3pLq6Wz1KxN4vRcMd", "uploadUrl": "https://storage.example.com/signed-upload-url", "expiresAt": "2026-04-02T09:30:00Z", "requiredHeaders": { "Content-Type": "application/pdf", "x-goog-if-generation-match": "0", "x-goog-content-length-range": "48213,48213" } }The
uploadUrlis single use and expires 5 minutes after you reserve it. Upload beforeexpiresAt, and register the file (step 3) within 30 minutes. -
Send the bytes.
PUTthe file touploadUrl, with every header listed inrequiredHeaders, exactly as given. Copy them from the response rather than hard-coding them. Don't send your Wingspan token to this URL.Header Meaning Content-TypeThe mimeTypeyou reserved.x-goog-if-generation-match: 0The file can be written once. A second PUTto the same URL is rejected.x-goog-content-length-range: N,NThe body must be exactly contentLengthbytes.The two
x-goog-*headers are part of the URL's signature, so leaving one out or changing it makes the upload fail.curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/pdf" \ -H "x-goog-if-generation-match: 0" \ -H "x-goog-content-length-range: 48213,48213" \ --data-binary @certificate-of-insurance.pdf -
Register the file. Create a vault file with the
uploadIdand a title. The reservation's file name and type are used, so you don't repeat them.curl -X POST "https://api.wingspan.app/v3/compliance/vault-files" \ -H "Authorization: Bearer $WINGSPAN_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "uploadId": "e8GtY3pLq6Wz1KxN4vRcMd", "title": "Certificate of Insurance", "visibility": "Private", "payeeId": "Rw5Nc2Hk8LqZ4tVp9XmBsa", "externalId": "COI-2026-117" }'// (trimmed) { "id": "Pz7Xc4Nq1Lk9Rt2Vw5HmJb", "title": "Certificate of Insurance", "filename": "certificate-of-insurance.pdf", "mimeType": "application/pdf", "fileSize": 48213, "kind": "Generic", "status": "Uploaded", "visibility": "Private", "payeeId": "Rw5Nc2Hk8LqZ4tVp9XmBsa", "externalId": "COI-2026-117", "events": { "createdAt": "2026-04-02T09:21:44Z" } }
The id is the file's ID. Use it anywhere another resource asks for a file.
If the file is already at a public HTTP or HTTPS address, you can skip the first two steps: create the vault file with fileUrl instead of uploadId, and Wingspan imports it. Send one or the other, not both.
Vault file fields
| Field | Values |
|---|---|
kind | Generic (the default) or Document. Document marks a document artifact. The file's MIME type doesn't decide this. |
visibility | Public or Private. |
status | Created, Uploaded, or Deleted. |
Wingspan sends a VaultFile.Created webhook event when a file is registered and VaultFile.Updated when its details change.
Attach a file to something else
Other resources reference a vault file by its ID in a field named for its purpose. For example:
- To attach a document to a payable or an invoice, call attach a payable document (
POST /v3/payments/payables/{payableId}/attachments) or attach an invoice document (POST /v3/payments/invoices/{invoiceId}/attachments) with the vault file's ID inmemberFileId. The attachment returns the ID inmemberFileIdtoo. Attaching a private file gives the other party on the payable ongoing read access to it, which isn't removed if you detach it later. - A payable import batch item takes the file's ID in
attachmentFileId. See Batches and bulk operations.
No endpoint outside the vault accepts file uploads or multipart bodies.
Download a file
To get a file's contents, request its content path. Wingspan answers with 302 Found and a Location header pointing at a short-lived signed URL:
curl -L "https://api.wingspan.app/v3/compliance/vault-files/Pz7Xc4Nq1Lk9Rt2Vw5HmJb/file" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-o certificate-of-insurance.pdf-L tells curl to follow the redirect. Generated documents work the same way, with the format in the path:
| Document | Endpoint |
|---|---|
| A vault file | GET /v3/compliance/vault-files/{fileId}/file |
| An invoice PDF | GET /v3/payments/invoices/{invoiceId}/pdf |
| A payable PDF | GET /v3/payments/payables/{payableId}/pdf |
| A Wingspan account statement PDF | GET /v3/finance/internal-accounts/{internalAccountId}/statements/{statementId}/pdf |
Tax form PDFs and secure links, pay statement PDFs, and report files aren't available in the V3 API yet.
Signed URLs expire within minutes. Don't store them or put them in emails. Request the content path again whenever you need the file, and use a secure link (below) to share it.
Share a document outside Wingspan
A secure link gives someone without Wingspan access a time-limited URL to a document. Invoices and payables support them:
curl -X POST "https://api.wingspan.app/v3/payments/invoices/Xa2Lk8Pq5Rt9Vm3Wz7NcYd/secure-links" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"format": "Pdf",
"expiresIn": 300,
"sentTo": ["[email protected]"]
}'// (trimmed)
{
"id": "Vd3Kq8Wz1Nx6Lp4Rt9HcMa",
"url": "https://storage.example.com/signed-invoice-pdf",
"format": "Pdf",
"expiresAt": "2026-04-02T09:40:00Z",
"sentTo": ["[email protected]"],
"createdAt": "2026-04-02T09:35:00Z"
}formatisPdf, the only accepted value, and it's the default. Any other value returns422 ValidationError.expiresInis in seconds, from 1 to 300. A value outside that range returns422 ValidationError.sentTomust be email addresses. It records who you're sharing with and is returned with the link. Wingspan doesn't email the link for you.notesisn't supported. Sending it returns422 ValidationError.- The link's
idis the invoice's or payable's own ID, not a separate link record.
The link works until expiresAt. It can't be revoked early and isn't kept as a record you can list later, so create a new one each time you share. See create an invoice secure link and create a payable secure link. Secure links for tax forms aren't available yet.
Manage vault files
| Task | Endpoint |
|---|---|
List files, optionally with filter[payeeId][eq], filter[kind][eq], filter[visibility][eq], or filter[externalId][eq] | GET /v3/compliance/vault-files |
| Read a file's details | GET /v3/compliance/vault-files/{fileId} |
Change title, description, visibility, or metadata | PATCH /v3/compliance/vault-files/{fileId} |
| Read extracted summary data | GET /v3/compliance/vault-files/{fileId}/summary |
| Delete a file | DELETE /v3/compliance/vault-files/{fileId} |
A file's contents and kind can't change after it's created. Upload a new file instead. Deleting a file that's still referenced, for example by an attachment or a tax form, returns 409 ResourceConflict.
Get a document signed
E-signature works from a document template, not from a single vault file:
- Upload the document as a vault file with
kind: Document. - Turn it into a template with
POST /v3/compliance/document-templates. - Open a signature request against the template with
POST /v3/compliance/signature-requests. Name the payer-payee relationship inpayerPayeeIdand the template intemplateId. The title and signer roles come from the template. - The payee starts it with
POST /v3/compliance/signature-requests/{requestId}/start, which creates the document and returns signing URLs.
Most signature requests are created for you by a Signature requirement. See Requirements and eligibility for when to create one yourself and the rules for each side.
Sending a vault file for signature directly, with your own signer list (POST /v3/compliance/vault-files/{fileId}/signature-requests), isn't available in the V3 API yet. It returns 501. Use a document template instead, or contact support if you need it.
What can go wrong
| Response | Cause | Fix |
|---|---|---|
422 ValidationError on contentLength | The file is larger than 25 MiB | Split or compress the file |
422 ValidationError on create | Both uploadId and fileUrl were sent, or neither | Send exactly one |
Upload PUT rejected by storage | A header from requiredHeaders is missing or changed, the reservation expired, or the URL was already used | Send every required header as given, or reserve a new upload |
Upload PUT rejected by storage on size | The body isn't exactly contentLength bytes | Reserve a new upload with the file's exact size |
409 ResourceConflict on create | The externalId is already used by another vault file | Look it up with filter[externalId][eq] |
409 ResourceConflict on delete | The file is still in use | Remove the references first |
Related pages
Updated 10 days ago