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.

  1. 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 uploadUrl is single use and expires 5 minutes after you reserve it. Upload before expiresAt, and register the file (step 3) within 30 minutes.

  2. Send the bytes. PUT the file to uploadUrl, with every header listed in requiredHeaders, exactly as given. Copy them from the response rather than hard-coding them. Don't send your Wingspan token to this URL.

    HeaderMeaning
    Content-TypeThe mimeType you reserved.
    x-goog-if-generation-match: 0The file can be written once. A second PUT to the same URL is rejected.
    x-goog-content-length-range: N,NThe body must be exactly contentLength bytes.

    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
  3. Register the file. Create a vault file with the uploadId and 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

FieldValues
kindGeneric (the default) or Document. Document marks a document artifact. The file's MIME type doesn't decide this.
visibilityPublic or Private.
statusCreated, 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 in memberFileId. The attachment returns the ID in memberFileId too. 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:

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"
}
  • format is Pdf, the only accepted value, and it's the default. Any other value returns 422 ValidationError.
  • expiresIn is in seconds, from 1 to 300. A value outside that range returns 422 ValidationError.
  • sentTo must be email addresses. It records who you're sharing with and is returned with the link. Wingspan doesn't email the link for you.
  • notes isn't supported. Sending it returns 422 ValidationError.
  • The link's id is 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

TaskEndpoint
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 detailsGET /v3/compliance/vault-files/{fileId}
Change title, description, visibility, or metadataPATCH /v3/compliance/vault-files/{fileId}
Read extracted summary dataGET /v3/compliance/vault-files/{fileId}/summary
Delete a fileDELETE /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:

  1. Upload the document as a vault file with kind: Document.
  2. Turn it into a template with POST /v3/compliance/document-templates.
  3. Open a signature request against the template with POST /v3/compliance/signature-requests. Name the payer-payee relationship in payerPayeeId and the template in templateId. The title and signer roles come from the template.
  4. 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

ResponseCauseFix
422 ValidationError on contentLengthThe file is larger than 25 MiBSplit or compress the file
422 ValidationError on createBoth uploadId and fileUrl were sent, or neitherSend exactly one
Upload PUT rejected by storageA header from requiredHeaders is missing or changed, the reservation expired, or the URL was already usedSend every required header as given, or reserve a new upload
Upload PUT rejected by storage on sizeThe body isn't exactly contentLength bytesReserve a new upload with the file's exact size
409 ResourceConflict on createThe externalId is already used by another vault fileLook it up with filter[externalId][eq]
409 ResourceConflict on deleteThe file is still in useRemove the references first

Related pages


Did this page help you?