Skip to content

Transfer API Reference

The Transfer API sends transfers from your Leapfile account over HTTPS. The base URL is your own site plus /v1 — for example https://your-company.leapfile.com/v1.

Before you start, read Leapfile APIs for how the feature is turned on, how to create a key, and how authentication, rate limiting and error responses work. For a worked integration — retries, cleanup, and reference code — see Sending files programmatically.

Endpoints

Method Path Purpose
POST /v1/transfers Create a draft
GET /v1/transfers List sent transfers
GET /v1/transfers/{transfer_id} Retrieve one transfer
POST /v1/transfers/{transfer_id}/files Attach a file to a draft
GET /v1/transfers/{transfer_id}/files List a transfer's files
GET /v1/transfers/{transfer_id}/files/{file_id} Retrieve one file record
DELETE /v1/transfers/{transfer_id}/files/{file_id} Remove a file from a draft
DELETE /v1/transfers/{transfer_id} Discard a draft
POST /v1/transfers/{transfer_id}/send Send the draft

A transfer is created as a draft, filled, then sent. Nothing is delivered, notified or billed until the send call. A draft's subject, message and recipients are fixed at creation: to change them, discard the draft and create another.

Objects

Transfer

Field Type Notes
id string The transfer id. Save it as soon as you create a draft.
status string See Transfer status.
subject string
message string or null
recipients array See below.
files array Present when you retrieve one transfer; absent from the list response.
expires_at string ISO 8601. On a draft, 30 days from creation. On a sent transfer, the account's delivery window counted from the send.
sent_at string or null null on a draft.

Recipient

Field Type Notes
email string
name string Falls back to the part of the email address before the @ when you don't supply one. This is what the pickup email greets them by.
picked_up_at string or null null until that recipient downloads the transfer.

A recipient's phone is accepted when you create a transfer but is never returned.

File

Field Type
id string
name string
size integer (bytes)

Transfer status

Status Meaning
draft Created but not sent. Not delivered, not billed, invisible to recipients.
pending Sent; nobody has collected it yet.
partial Some recipients have collected it.
complete Every recipient has collected it.
canceled Canceled in the web application.
expired Past expires_at without being fully collected.
unknown An old record whose stored status predates this vocabulary. Only from GET /v1/transfers/{transfer_id}, never from the listing, and not a value you can filter on. Handle it rather than assuming the list above is closed.

expired is both a stored status and one worked out on read: a pending or partial transfer past its expires_at reports expired on the next read with no action on your part. complete, canceled and draft are never overridden this way.


Create a transfer

POST /v1/transfers
Content-Type: application/json
Field Required Notes
subject yes Non-empty string.
recipients yes Non-empty array of objects with email, and optionally name and phone. Up to 250. Trial and free subscriptions are limited to one recipient. Duplicate addresses are not merged; each one counts.
message no Up to 2,000 characters.
security no See Recipient authentication. Omit to use the account's default.
security_question only with security: "question"
security_answer only with security: "question"
{
  "subject": "Q3 audit files",
  "message": "The signed statements are attached.",
  "recipients": [{"email": "auditor@example.com", "name": "Dana Reyes"}]
}

Returns 201 with the transfer object, and a Location header holding the new transfer's path — a path such as /v1/transfers/{transfer_id}, not an absolute URL.

{
  "id": "8f14e45fceea167a",
  "status": "draft",
  "subject": "Q3 audit files",
  "message": "The signed statements are attached.",
  "recipients": [
    {"email": "auditor@example.com", "name": "Dana Reyes", "picked_up_at": null}
  ],
  "files": [],
  "expires_at": "2026-09-20T14:02:11Z",
  "sent_at": null
}

Treat id as an opaque string; its length and format are not part of the contract.

Recipient addresses are checked for form here, so a malformed address fails at creation rather than at send. The recipient count is checked at send instead, so on a subscription limited to one recipient a five-recipient draft is created and then refused when you send it.

subject is only required to be a non-empty string; the API applies no length limit to it. Keep it to the length of an email subject.

List transfers

GET /v1/transfers
Parameter Default Notes
limit 25 Clamped into 1–100 rather than rejected, so limit=1000 returns 100. A value that is not a whole number is a 400.
status all all, pending, partial, complete, canceled or expired.
cursor The next_cursor from the previous page.
{
  "data": [ ... ],
  "next_cursor": "MjAyNi0wOC0yMVQxMjowMDowMFo6MjU"
}

Newest first. next_cursor is null on the last page.

Drafts are not in the listing

This endpoint returns sent transfers only, and status=draft is rejected. A draft is reachable only by its id, so persist the id the moment you create one.

The cursor is based on send time. A transfer sent while you're paging shows up on a later page, not one you've already fetched.

A status filter can make you skip a transfer

Each page re-checks the status filter. If a transfer stops matching after you've paged past where it would have been — say a pending one gets picked up — it disappears from the results, and everything below it moves up one spot. That row now lands on a page you already read, so you miss it.

To see every transfer with no gaps, page with status=all and filter in your own code instead. A transfer only vanishes from that unfiltered list if it's canceled or deleted while you're paging.

Retrieve a transfer

GET /v1/transfers/{transfer_id}

Returns 200 with the transfer object, including files and each recipient's picked_up_at. Works for drafts and for sent transfers.

Attach a file

POST /v1/transfers/{transfer_id}/files
Content-Length: 184320
Content-Disposition: attachment; filename="statements.pdf"

The request body is the raw bytes of the file — one call per file, streamed straight through to storage, so the size of the file never has to fit in memory. It is not a multipart upload.

  • Content-Length is required. Without it the request fails with 411. Some HTTP clients switch to chunked encoding for a streamed body and drop the header; set it explicitly from the file size. Take it from the actual size of the file: the API stores exactly the number of bytes you declare, so a Content-Length smaller than the file stores a truncated file and reports success.
  • The filename comes only from Content-Disposition. Any directory part is stripped. filename* (RFC 5987) is used in preference to filename when both are present.
  • The request's Content-Type is ignored. The stored type is worked out from the file extension, so give the file a correct one.
  • The size limit is your account's per-file limit, checked against Content-Length before any bytes are read. The file's size is rounded up to whole binary megabytes (1 MB = 1,048,576 bytes) for the comparison, so a file one byte over a whole megabyte counts as the next one up, and a 100.1 MB file is refused against a 100 MB limit. The 413 message reports that rounded figure, not the exact byte count.

Returns 201 with the file object, and a Location header holding the new file's path, when the file is attached.

Returns 200 with the existing file object when that filename is already attached to the draft. Nothing is stored twice and nothing is billed twice, so a timed-out upload is safe to repeat.

Give every file on a transfer a distinct name

The filename identifies the file within the transfer, and directory parts are stripped before it is used — 2026-08-21/report.csv and 2026-08-20/report.csv are both report.csv here, so they do not arrive as two files.

A job that collects files from a directory tree should build a distinct name for each one, folding in the date or the folder it came from.

An upload that is cut off partway attaches nothing: the partial bytes are discarded and the call fails with 500. There is no half-attached state to clean up — repeat the call and it sends the file again from the start.

Files can only be attached to a draft. Attaching to a sent transfer returns 409 invalid_state.

List files, retrieve a file

GET /v1/transfers/{transfer_id}/files
GET /v1/transfers/{transfer_id}/files/{file_id}

The list returns {"data": [ ... ]} with every file on the transfer in one response. Both work on sent transfers as well as drafts.

Remove a file

DELETE /v1/transfers/{transfer_id}/files/{file_id}

Returns 204 with an empty body. Draft only; on a sent transfer this returns 409 invalid_state.

Discard a draft

DELETE /v1/transfers/{transfer_id}

Returns 204 with an empty body.

409 invalid_state means the transfer has already been sent; cancel a sent transfer in the web application. 404 means the draft was already discarded.

Send

POST /v1/transfers/{transfer_id}/send

No request body. This is the call that bills the transfer, emails the recipients and starts the expiration clock.

Returns 200 with the sent transfer. status is pending and expires_at has been recalculated from the account's delivery window.

A draft can be sent once. A second call returns 409 invalid_state.

409 invalid_state on a send has two causes that share one code: the transfer is no longer a draft, and the draft has no files attached. Only the message separates them, so read the transfer back before recording a send as done — see Retries and sending exactly once.

The send also fails, with its own codes, if the draft would exceed the subscription's recipient limit (409 recipient_limit_exceeded) or the number of transfers the account allows to be outstanding at one time (409 transfer_limit_reached).

Every send masks the recipients from one another, includes your message in the notification, and emails each recipient. These follow the account and the sending user's own defaults rather than the request.


Recipient authentication

security decides what a recipient must do before downloading. Omit it and the account's configured default applies, which keeps API transfers consistent with what the same account sends from the web.

Value The recipient must Also required
none Nothing; the link is enough
email Confirm their email address
email_code Enter a code emailed to them
question Answer a question you set security_question and security_answer
sms Enter a code texted to them phone on every recipient
login Register as a guest and sign in

Two rules to build against:

  • An option the account does not have is a 403 auth_option_not_enabled. A transfer never goes out with less protection than you asked for.
  • Fields an option does not use are rejected. Sending security_answer with security: "none" is a 400.

For sms, the phone number must be digits once spaces, brackets, dashes and other punctuation are removed.

{
  "subject": "Contract",
  "recipients": [{"email": "legal@example.com", "phone": "+15125550123"}],
  "security": "sms"
}

Error codes

Every error returns {"error": "...", "message": "..."}. Branch on error; the wording of message can change.

Status Code Meaning
400 invalid_request The body or a parameter is wrong. message names the field.
401 invalid_api_key Missing, malformed, unknown or revoked key, or the owning user is deactivated.
403 api_not_enabled The Transfer API account feature is off, or the plan does not include outgoing transfers. An administrator fixes this.
403 api_not_permitted The user does not have Use the transfer API. An administrator fixes this.
403 sending_not_permitted The user is not allowed to send files at all.
403 auth_option_not_enabled The account does not have the security option you asked for.
403 no_credit The account bills per transfer and has no credit left.
404 not_found Unknown id, discarded draft, or a transfer sent by a different user.
409 invalid_state The transfer is not in a state that allows this: already sent, or a send with no files attached. The message says which.
409 transfer_limit_reached The account already has the maximum number of transfers outstanding.
409 recipient_limit_exceeded More recipients than the subscription allows.
411 length_required The upload has no usable Content-Length.
413 file_too_large The file is over the account's per-file limit. message gives the rounded size and the limit.
429 rate_limited 60 requests in the last 60 seconds on this key. Wait Retry-After seconds.
500 internal_error A fault on our side. Retry with backoff; contact support with the time of the request if it persists.
502 503 504 service_unavailable A dependency is down or slow. Retry with backoff.

404 also covers "not yours", so it does not distinguish a mistyped id from a colleague's transfer.

Limits

Several of these are account settings. Ask an administrator for your account's values.

Limit Value
Requests 60 per 60 seconds, per key
Recipients per transfer 250; 1 on trial and free subscriptions
Message 2,000 characters
File size Your plan's per-file limit
Files per transfer Unlimited, in count and in total size. Each file needs a unique name within the transfer
Draft lifetime 30 days from creation
Sent transfer lifetime Your account's delivery window, counted from the send
Outstanding transfers Your account's limit, checked at send. The count covers every sent transfer not yet collected, canceled or expired, and a scheduled send waiting in the web application or desktop client. Drafts do not count, so an abandoned draft costs you nothing but the clutter