SendFlo

API reference

Version 2026-08-07 · 227 operations · base URL https://api.sendflo.app/v1 · openapi.json

The SendFlo API moves files between people, teams and AI agents. This reference covers every operation an API key can call; it is built from the API's own routes, and a test fails when the two disagree.

  • Authentication: Authorization: Bearer sk_live_… with an API key from Settings → Developer. Each operation names the scope its key needs.
  • Versioning: by date. Every response carries sendflo-version; additive changes keep the date, breaking changes get a new one.
  • Errors: RFC 9457 problem documents (application/problem+json) with a machine code.
  • Rate limits: per key (set when you create it); over the limit you get 429 rate_limited.
  • Files: bytes never pass through this API. Uploads and downloads use presigned storage URLs that the API hands out.
  • Webhooks: signed the Standard Webhooks way, so any Standard Webhooks library verifies them: webhook-id (the event's id, the same on every retry — deduplicate on it), webhook-timestamp (unix seconds) and webhook-signature: v1,<base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw body>">, keyed with the secret after whsec_, base64-decoded. Reject a timestamp more than 5 minutes off. Until 28 April 2027 the old sendflo-signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> (keyed with the whole secret string) is sent too. Each event is tried 6 times with growing waits; every attempt is in the endpoint's delivery log for 30 days, where you can Resend it.

transfers

Upload a file: create a transfer, PUT the parts to storage, complete it.

POST/transfersStart an upload

Returns presigned multipart upload URLs: PUT each part_size slice of the file (the last one shorter) to part_urls[n], then complete the upload within 24 hours. Refused before any URL is issued when the file, or the whole send (transfer_total_bytes), is over the plan's size cap, or when the workspace is out of storage. A paid plan over its storage quota answers 413 overage_requires_ad, which a key can't satisfy.

Key scope: transfers.write

Request body (JSON)

namestringThe file name.
sealed_name?stringEnd-to-end files: the real name sealed on your side with the link key (`sfm1.<iv>.<ciphertext>`, like `sealed_meta`, with additional data `name`). Send `name` as "Encrypted file.<ext>" (its extension alone): SendFlo stores that, keeping only the extension of whatever `name` says, and the link page shows the real name only in the recipient's browser. 422 `sealed_name_invalid`; such a file's link must be `encrypted` (422 `sealed_name_requires_e2e`).
sizestring | integerThe exact size in bytes; the finished upload must match.
expires_at?stringThe expiry you'll give its link (clamped to the plan); it picks the storage tier.
overage_ad?booleanFor the signed-in app (an ad was watched to go over quota).
overage_ad_nonce?stringFor the signed-in app (the ad's credit).
overage_ad_batch?stringFor the signed-in app (one ad for a multi-file send).
transfer_total_bytes?string | integerThe whole send's size, so an oversize send is refused before its first byte.
intent?objectThe link settings you'll use (expiry, downloads, access, recipients, delivery): they decide the storage tier.
intent.expires_at?string
intent.max_downloads?string | integer | null
intent.access?"public" | "tracked" | "restricted"
intent.recipient_count?string | integer
intent.delivery?"preview_download" | "download_only" | "preview_only" | "preview_watermark"
intent.preview_kind?"image" | "pdf" | "video" | "audio" | null
intent.available_at?string | null
intent.e2e?boolean
origin?string`watch` (a desktop watched folder), `agent` (files for an agent task) or `device` (the signed-in app: a device send kept for a device that's off, sealed); leave it out otherwise.
watch?objectFor `origin: watch`: the folder `{ id, label, device_id?, destination? }`.
watch.idstring
watch.labelstring
watch.device_id?string | null
watch.destination?string | null
watch.delivery_id?string | null
approval?objectFor the signed-in app (an upload for an approval request).
approval.idstring
approval.entry_idstring
file_keys?array of objectFor the signed-in app (identity-sealed files: a watched folder to your own computers, or a device send kept for a device that's off): the file's SFE2 key sealed to each recipient's current identity key, `{ kind: "user", id, version, sealed }`; needs `sealed_name`. 422 `file_keys_*`, 409 `recipient_key_changed`.
file_keys.kind"user" | "team_key"
file_keys.idstring
file_keys.versionstring | integer
file_keys.sealedstring

A name ending in ? is optional.

Response 200

idstring
namestring
sizestring | integerThe size you declared (a number here; byte counts are strings elsewhere).
state"open"
storage_classstring
placementobject
placement.tier"pool" | "flat" | "fallback"
placement.reasonstring
placement.pool_multiplenumber | null
placement.eligibleboolean | null
placement.eligibility_reasonstring | null
uploadobject
upload.upload_idstring
upload.part_sizestring | integer
upload.part_urlsarray of string

A name ending in ? is optional.

An error: file_size_cap, transfer_size_cap, approval_upload_refused, overage_requires_ad, storage_quota_exceeded, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/transfers" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…","size":"…"}'
POST/transfers/{id}/completeComplete an upload

Finishes the upload (the storage provider's own part list decides; send parts: []), stores the file and queues its malware scan. Use the returned file_id with POST /v1/links. A stored size that differs from the declared one aborts the upload (413 size_mismatch).

Key scope: transfers.write

Path parameters

idstringThe upload's id from POST /v1/transfers.

Request body (JSON)

partsarray of objectRequired but not read: send `[]`.
parts.part_numberstring | integer
parts.etagstring
blake3?stringThe file's BLAKE3 hash, if you have it.
client_ref?stringYour own reference for this upload (64 characters at most).

A name ending in ? is optional.

Response 200

idstring
state"complete"
file_idstring
sizestring
approval?object
approval.idstring
approval.statusstring

A name ending in ? is optional.

An error: not_found, not_open, not_staged, size_mismatch, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/transfers/{id}/complete" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"parts":[{"part_number":"…","etag":"…"}]}'

activity

What happened to a send: who opened and downloaded what, the audit report, per-person access.

GET/links/{id}/eventsList one link's latest events

The newest 100 events of one link. For a whole send, with paging, use GET /v1/links/{id}/transfer/activity.

Key scope: links.write

Path parameters

idstringThe link's id.

Response 200

dataarray of LinkEvent
data.kindstringopen, view, download, restore, forward, comment, signup, access_off, access_on.
data.bytesstring
data.recipientstring | null
data.verifiedboolean
data.stamp_check?string | null
data.created_atdate-time

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/events" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/transfer/activitySee who opened and downloaded a send

Every file's events, newest first, 50 per page (up to 200 with limit); pass next_cursor back as cursor. The first page also lists the people (up to 200, people_total counts them all) with what they downloaded and the delivery proof; later pages answer people: null. On Team Pro and Team Enterprise each event also carries the IP address and its network, and each person their addresses; other plans see country and device only.

Key scope: links.write

Path parameters

idstringAny link of the send (its first link or any other); a single-file send is a send of one.

Query parameters

cursor?string`next_cursor` from the previous page.
limit?string | integerEvents per page, 1–200 (50 by default).

Response 200

transfer_idstring
slugstring
filesarray of object
files.link_idstring
files.namestring
peoplearray of ActivityPerson | null
people.keystring
people.emailstring | null
people.labelstringThe email, or "Visitor N" for someone who never gave one.
people.verifiedboolean
people.opened_atdate-time | null
people.last_atdate-time
people.files_totalstring | integer
people.downloadedarray of object
people.downloaded.link_idstring
people.downloaded.namestring
people.downloaded.atdate-time
people.downloaded.proof"receipts" | "handed_off" | null
people.countriesarray of string
people.devicesarray of string
people.proof"receipts" | "handed_off" | null
people.eventsstring | integer
people.ips?array of stringTeam Pro and Team Enterprise only: the addresses they came from.
people_totalstring | integer | null
eventsarray of ActivityEvent
events.idstring
events.link_idstring
events.filestring
events.kindstringopen, view, download, restore, forward, comment, signup, access_off, access_on.
events.bytesstring
events.recipientstring | null
events.verifiedboolean
events.countrystring | null
events.transportstring | null
events.devicestring | null
events.proof"receipts" | "handed_off" | null
events.stamp_check?"found" | "missing"
events.ip?string | null
events.network?object | null
events.network.orgstring | null
events.network.classstring | null
events.created_atdate-time
next_cursorstring | null

A name ending in ? is optional.

An error: invalid_cursor, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/transfer/activity" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/transfer/auditDownload a send's audit report

A download (sendflo-audit-<slug>-<date>.csv or .json) with the send, its files, the people, your actions and every event, oldest first (100,000 at most). CSV by default (UTF-8 with a byte-order mark, formula-safe cells; columns section,time_utc,file,link_id,person,verified,action,country,device,transport,proof,detail,ip,network, the last two filled on Team Pro and Team Enterprise); JSON with format=json (events carry ip and network on those plans). Proof older than 90 days is left empty, and a note says why.

Key scope: links.write

Path parameters

idstringAny link of the send (its first link or any other); a single-file send is a send of one.

Query parameters

format?"csv" | "json"`csv` (the default) or `json`.

Response 200 (text/csv): string

An error: invalid_format, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/transfer/audit" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/transfer/peopleList who can open which file of a send

Everyone listed, verified or emailed, with each file's state for them. Public sends answer manageable: false: per-person control needs Verified or Only-them access.

Key scope: links.write

Path parameters

idstringAny link of the send (its first link or any other); a single-file send is a send of one.

Response 200

access"public" | "tracked" | "restricted"
manageableboolean
filesarray of object
files.link_idstring
files.namestring
files.liveboolean
peoplearray of TransferPerson
people.emailstring
people.sourcesarray of "listed" | "verified" | "emailed"
people.filesarray of object
people.files.link_idstring
people.files.namestring
people.files.access"open" | "removed"
people.openstring | integer

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/transfer/people" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/links/{id}/transfer/people/accessSet which files one person may open

Sets exactly the files email may open: "all", or link ids of this send (the others are removed). It never widens access: a restore only gives back what you removed. Their emailed links stop working when they can open nothing. 409 public_link on a public send.

Key scope: links.write

Path parameters

idstringAny link of the send (its first link or any other); a single-file send is a send of one.

Request body (JSON)

emailstringThe person's email.
files"all" | array of string`"all"`, or link ids of this send.

A name ending in ? is optional.

Response 200

emailstring
openstring | integer
changesarray of object
changes.link_idstring
changes.change"removed" | "restored" | "none" | "not_listed"
personTransferPerson | null
person.emailstring
person.sourcesarray of "listed" | "verified" | "emailed"
person.filesarray of object
person.files.link_idstring
person.files.namestring
person.files.access"open" | "removed"
person.openstring | integer

A name ending in ? is optional.

An error: not_found, public_link, invalid_email, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/links/{id}/transfer/people/access" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"email":"…","files":"all"}'
GET/reports/transfersReport link activity over a period

Links created in the last days (1–90, 30 by default), the newest 200, with opens, views, downloads, sign-ups, bytes served and feedback over the same period.

Key scope: links.write

Query parameters

days?string | integerThe period in days, 1–90 (30 by default).

Response 200

periodobject
period.fromdate-time
period.todate-time
period.daysnumber
totalsobject
totals.linksstring | integer
totals.opensstring | integer
totals.downloadsstring | integer
totals.viewsstring | integer
totals.signupsstring | integer
totals.egress_bytesstring
dataarray of TransferReportRow
data.idstring
data.slugstring
data.namestring
data.sizestring
data.created_atdate-time
data.expires_atdate-time | null
data.revokedboolean
data.opensstring | integer
data.downloadsstring | integer
data.viewsstring | integer
data.signupsstring | integer
data.egress_bytesstring
data.commentsstring | integer
data.favoritesstring | integer
data.rating_countstring | integer
data.rating_avgnumber | null
data.rating_maxstring | integer

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/reports/transfers" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/reports/transfers

files

Your stored files, and sending a stored file again.

GET/filesList your stored files

The workspace's newest 100 files, newest first.

Key scope: files.read

Response 200

dataarray of File
data.idstring
data.namestring
data.sizestring
data.content_typestring | null
data.created_atdate-time

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/files" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/files
GET/files/{id}/send-againCheck how a file can be sent again

Whether the file can be sent again (and if not, why), its live links, whether a new link reuses the stored bytes (mode: reuse, free) or needs a copy billed like an upload (mode: copy), and the quota it would use.

Key scope: files.read

Path parameters

idstringThe stored file's id.

Response 200

fileobject
file.idstring
file.namestring
file.sizestring
file.expires_atdate-time | null
file.tier"pool" | "flat" | null
file.downloadedboolean
file.capabilitiesLinkCapabilities | null
file.capabilities.tier"flat" | "pool"
file.capabilities.readbooleanThe file has been read (downloaded, previewed from the original, or copied).
file.capabilities.download_limitobject
file.capabilities.accessobject
file.capabilities.recipientsobject
file.capabilities.bonusobject | null
file.capabilities.send_againobject
file.capabilities.permissionsobject
targetsobjectWhere a copy of a file on included storage may go. Both unavailable on a file on unlimited-download storage (a new link reuses it).
targets.sameobjectA copy on the same provider (a file on included storage): a new file under the same rules, fixed download limit.
targets.same.availableboolean
targets.same.counts_as_readboolean
targets.flexibleobjectA copy to unlimited-download storage within the monthly copy allowance: its download limit is editable afterwards.
targets.flexible.availableboolean
targets.flexible.needs_bytesstring
targets.flexible.remaining_bytesstring | null
targets.flexible.size_bytesstring | null
targets.flexible.period_enddate-time | null
targets.flexible.reason"no_flat_target" | "copy_quota_exhausted" | null
eligibleboolean
reasonsarray of object
reasons.code"staging" | "archived" | "relocating" | "scan" | "expired" | "encrypted" | "paused" | "abuse" | "suspended"
reasons.messagestring
live_linksarray of object
live_links.idstring
live_links.slugstring
live_links.urlstring
live_links.expires_atdate-time | null
live_links.max_downloadsstring | integer | null
live_links.download_countstring | integer
live_links.downloads_leftstring | integer | null
live_links.delivery"preview_download" | "download_only" | "preview_only" | "preview_watermark"
live_links.has_passwordboolean
live_links.access"public" | "tracked" | "restricted"
planobject
plan.mode"reuse" | "copy"
plan.representative_file_idstring
plan.representative_expires_atdate-time | null
plan.copyingboolean
plan.pool_headroomnumber | null
plan.charge_bytesstring
plan.repoints_original_linksboolean
extendobject
extend.near_expiryboolean
extend.current_untildate-time | null
extend.max_untildate-time
quotaobject
quota.usedstring
quota.limitstring
quota.afterstring
quota.allowedboolean
quota.code?"storage_quota_exceeded" | "overage_requires_ad"
ad_requiredboolean

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/files/{id}/send-again" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/files/{id}/send-againSend a stored file again with a new link

A new link without uploading again, under the same rules as POST /v1/links (team approval included). mode: reuse links the stored bytes (free); mode: copy makes a copy billed like an upload (charged_bytes), finished in the background (file.copying). One at a time per file (409 busy). On Free with ads on, it needs an ad credit a key can't get (402 ad_required).

Key scope: links.write

Path parameters

idstringThe stored file's id.

Request body (JSON)

linkobjectThe link's options: any POST /v1/links field except `file_id` (`{}` for the defaults).
link.slug?string
link.password?string
link.expires_at?string
link.max_downloads?string | integer
link.note?string
link.subject?string
link.sealed_meta?string
link.require_email?boolean
link.access?"public" | "tracked" | "restricted"
link.allowed_emails?array of string
link.encrypted?boolean
link.e2e_half?string
link.e2e_half_derivation?1
link.e2e_recovery?object
link.e2e_recovery.team_key_idstring
link.e2e_recovery.sealedstring
link.kind?"file" | "message"
link.open_once?boolean
link.e2e_stamp?boolean
link.e2e_each?boolean
link.delivery?"preview_download" | "download_only" | "preview_only" | "preview_watermark"
link.watermark_label?string
link.viewer_mark?boolean
link.allow_guests?boolean
link.guest_downloads?boolean
link.viewers_invite?boolean
link.download_mark?boolean
link.mark_unsupported?"block" | "send"
link.preview_variant?"720p" | "480p" | "1080p"
link.bundle?boolean
link.bundle_id?string
link.available_at?string
link.notify_on_download?boolean
link.brand_id?string
link.comments_mode?"off" | "open" | "private"
link.ratings_enabled?boolean
link.favorites_enabled?boolean
link.approvals_enabled?boolean
link.restore?object
link.restore.enabledboolean
link.restore.duration_days?string | integer
target?"same" | "flexible"A file on included storage: `same` (default) copies it on the same provider with a fixed download limit; `flexible` copies it to unlimited-download storage within the monthly copy allowance (409 `copy_quota_exhausted` past it), editable afterwards. Ignored on other files.
ad_nonce?stringFor the signed-in app (a Free plan's ad credit).
overage_ad?booleanFor the signed-in app (an ad was watched to go over quota).
overage_ad_nonce?stringFor the signed-in app (that ad's credit).

A name ending in ? is optional.

Response 200

mode"reuse" | "copy"
target?"same" | "flexible"
linkLinkCreatedA new link (or, for a retried create, the earlier one with `replayed: true`).
link.idstring
link.slugstring
link.urlstringThe download page, https://sendflo.app/l/{slug}.
link.file_idstring
link.has_passwordboolean
link.expires_atdate-time | null
link.max_downloadsstring | integer | null
link.download_countstring | integer
link.notestring | null
link.subjectstring | null
link.require_emailboolean
link.access"public" | "tracked" | "restricted"
link.allowed_emailsarray of string
link.encryptedboolean
link.e2e_stampbooleanEnd-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it.
link.e2e_eachbooleanMark each recipient: on a `restricted` end-to-end send of up to 50 people, the sender's device also stamped one copy per listed person with the label and their email; each person downloads their own once they confirm their email.
link.delivery"preview_download" | "download_only" | "preview_only" | "preview_watermark"
link.watermark_labelstring | null
link.viewer_markbooleanEach viewer sees their own copy of every image and PDF page, marked with the label and their confirmed email; on a video the player draws that email over the picture.
link.allow_guestsbooleanGuests may also view without an email: their copy is marked with their IP address, the time and a view id instead. They download only with `guest_downloads`.
link.guest_downloadsbooleanGuests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`.
link.viewers_invitebooleanViewers may invite others: a recipient who confirmed their email can invite people by email from the link's page; each invited person confirms their own email and gets their own copies and marks.
link.download_markbooleanDownload with watermark: recipients download a marked copy (and see marked previews), never the original.
link.mark_unsupported"block" | "send"With `download_mark`: files that can't be marked are blocked (`block`) or downloaded as their originals (`send`).
link.bundle_idstring | null
link.available_atdate-time | null
link.notify_on_downloadboolean
link.feedbackFeedbackSettingsThe transfer's feedback settings.
link.feedback.comments"off" | "open" | "private"
link.feedback.ratingsboolean
link.feedback.favoritesboolean
link.feedback.approvalsboolean
link.feedback.rating_scale"stars5" | "score10"
link.feedback.encryptedbooleanAn end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>).
link.created_atdate-time
link.restore_untildate-time | null
link.revoked_atdate-time | null
link.paused_by"owner" | "trust" | null
link.capabilities?LinkCapabilities | null
link.capabilities.tier"flat" | "pool"
link.capabilities.readbooleanThe file has been read (downloaded, previewed from the original, or copied).
link.capabilities.download_limitobject
link.capabilities.accessobject
link.capabilities.recipientsobject
link.capabilities.bonusobject | null
link.capabilities.send_againobject
link.capabilities.permissionsobject
link.placementLinkPlacement
link.placement.tierstring | null
link.placement.relocatingboolean
link.placement.eligibleboolean | null
link.restore?RestoreState
link.restore.activetrue
link.preview?LinkPreview | null
link.preview.statestringqueued, ready, failed, quota (or unsupported on create).
link.preview.kindstring | null
link.preview.variantstring | null
link.preview.pagesstring | integer | null
link.expiry_capped?ExpiryCapped | null
link.expiry_capped.reason"billing_cycle"
link.expiry_capped.billing_cycle"monthly" | "annual"
link.expiry_capped.max_daysstring | integer
link.expiry_capped.expires_atdate-time
link.expiry_capped.messagestring
link.replayed?true
fileobject
file.idstring
file.copyingboolean
charged_bytesstring
allowance_bytes?stringWhat the copy used of the monthly copy allowance (0 for `same`).
counts_as_read?booleanThe copy counted as a read of the original on its included storage.

A name ending in ? is optional.

An error: not_found, send_again_unavailable, ad_required, busy, overage_requires_ad, storage_quota_exceeded, no_flat_target, copy_quota_exhausted, approval_required, awaiting_approval, content_blocked, policy_forbidden, policy_password_required, policy_ttl_exceeded, free_tier_size, file_size_cap, transfer_size_cap, paid_required, delivery_conflict, e2e_view_unsupported, e2e_view_too_large, viewer_mark_not_on_e2e, not_found, plan_required, invalid_schedule, slug_invalid, slug_taken, allowed_emails_required, brand_not_found, brand_locked, file_budget_exceeded, viewer_mark_plan, viewer_mark_requires_watermark, viewer_mark_requires_verified_access, guests_require_viewer_mark, guests_require_tracked, guests_not_on_e2e, stamp_requires_encryption, stamp_unsupported, policy_locked, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/files/{id}/send-again" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"link":{}}'

received

Files people uploaded to you through upload pages and requests.

GET/inbox-itemsList files people uploaded to you

Uploads through upload pages, file requests and inboxes, newest first. Rejected uploads stay listed (scan_state: "rejected"). Includes the uploader's answers and contact details. Without parameters: the newest 200. Narrow with file_request_id, inbox_id, since / until (ISO 8601) and scan_state (comma-separated: pending, clean, flagged, released, not_scannable, rejected); page with limit (1–200) and cursor (the previous answer's next_cursor). Where it came from: source (a helpdesk, like zendesk), connection_id and ticket; q searches the uploader's name and email, file names, the request and the reference, best first, 25 a page (page 1–8; see search); an API credential searches the reference only. A malformed value answers 422 invalid_query.

Key scope: inboxes.read

Query parameters

file_request_id?stringOnly files that arrived through this file request.
inbox_id?stringOnly files delivered into this inbox.
source?stringOnly files requested from this platform (its name: `zendesk`, `testdesk` …).
connection_id?stringOnly files requested through this helpdesk connection.
ticket?stringOnly files requested from this ticket (its number, with or without #).
q?stringA search: the uploader's name and email, file names, the request and the reference (2–100 characters).
page?stringWith `q`: the page of results, 1–8.
since?date-timeOnly files received at or after this moment.
until?date-timeOnly files received before this moment.
scan_state?stringComma-separated scan states.
limit?string | integerHow many, 1–200 (default 200).
cursor?string`next_cursor` from the previous page.

Response 200

dataarray of ReceivedFile
data.idstring
data.file_idstring
data.namestring
data.sizestring
data.fromstring | null
data.from_emailstring | null
data.answersarray of object
data.answers.idstring
data.answers.labelstring
data.answers.typestring
data.answers.valuestring
data.contactobject | null
data.contact.namestring
data.contact.emailstring
data.contact.email_verifiedboolean
data.submission_idstring | null
data.scan_statestringrejected, or the file's scan state (pending, clean, flagged, failed, released, not_scannable).
data.preview_kind"image" | "video" | "audio" | "pdf" | null
data.scan_labelsarray of string
data.rejected_atdate-time | null
data.reject_reasonstring | null
data.received_atdate-time
data.expires_atdate-time | null
data.ad_supportedboolean
data.file_request_idstring | null
data.inbox_idstring | null
data.linkobject | null
data.link.idstring
data.link.namestring | null
data.link.kindstring | null
data.link.slugstring | null
data.view_onlybooleanA view-only PDF or image (its page's setting or the team's rule): preview it; the download is refused.
data.member_markbooleanIts previews are your own copies with your email on them.
data.can_open_originalbooleanYou're the team's Owner or an Admin: the app's View original… opens it, with a reason.
next_cursorstring | null
search?SearchMetaPresent when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first.
search.engine"meilisearch" | "postgres"
search.pagestring | integerThis page (1–8).
search.totalstring | integerAbout how many match (the search engine's estimate).
search.next_pagestring | integer | null

A name ending in ? is optional.

An error: invalid_query, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/inbox-items" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/inbox-items
POST/inbox-items/{id}/downloadGet a download URL for a received file

A storage URL valid for 1 hour (the bytes never pass through the API). Files still being scanned are allowed; blocked ones are refused. Counts against the monthly download allowance (429 when it's used up).

Key scope: files.read

Path parameters

idstringThe received item's id (from GET /v1/inbox-items).

Response 200

urlstring
namestring
sizestring
ticketstring

A name ending in ? is optional.

An error: not_found, gone, content_blocked, egress_cap_exceeded, egress_anomaly, view_only, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/inbox-items/{id}/download" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/inbox-items/{id}/previewGet a preview URL for a received file

Images, video, audio and PDFs within a size limit (415 otherwise). Waits for the malware scan (425 scan_pending). HEIC and TIFF come back as a rendered copy from your preview allowance (425 preparing while it renders). Counts as a download. A view_only file comes from the protected plane, never the original (425 preparing while its copy is made): a PDF answers its first 5 pages in page_urls (the rest: POST …/preview/pages), and with member_mark each page is your own marked copy (preview_paused when the team's marked views are used up).

Key scope: files.read

Path parameters

idstringThe received item's id.

Response 200

urlstring
kind"image" | "video" | "audio" | "pdf"
content_typestring
plane"stored" | "direct"
namestring
sizestring
view_only?boolean
marked?boolean
pages?string | integerA view-only PDF's page count.
page_urls?array of string | nullA view-only PDF's first pages; null = paused.
marked_pages?boolean
preview_paused?boolean

A name ending in ? is optional.

An error: not_found, rejected, gone, content_blocked, not_previewable, scan_pending, restoring, relocating, egress_cap_exceeded, egress_anomaly, preview_quota, render_failed, preparing, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/inbox-items/{id}/preview" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/inbox-items/{id}/preview/pagesGet more pages of a view-only received PDF

After POST …/preview answered a view-only PDF's first pages: the next count (10 at most) from from, each from the protected plane (your own marked copy with member_mark; null once the team's marked views are used up, with preview_paused).

Key scope: files.read

Path parameters

idstringThe received item's id.

Request body (JSON)

fromstring | integerThe first page to return, counting from 0 (5 after the first screen).
count?string | integerHow many pages (10 at most).

A name ending in ? is optional.

Response 200

fromstring | integer
pagesstring | integer
page_urlsarray of string | null
preview_paused?boolean

A name ending in ? is optional.

An error: not_found, not_view_only, gone, preparing, render_failed, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/inbox-items/{id}/preview/pages" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"from":"…"}'

file requests

Ask a person for files: a request link with its own rules.

GET/file-requestsList your file requests and upload pages

Newest first, 200 at most, every state, the personal upload link included. limits gives the plan's allowance of reception links (the personal link doesn't count).

Key scope: file_requests.write

Response 200

dataarray of FileRequest
data.idstring
data.slugstring
data.urlstringThe upload page, https://sendflo.app/u/{slug}.
data.state"open" | "fulfilled" | "cancelled" | "expired"
data.kind"request" | "reception"
data.personalbooleanThe account's personal upload link.
data.namestring | null
data.promptstring | null
data.max_filesstring | integer | null
data.max_bytes_per_filestring | null
data.max_bytes_per_uploadstring | null
data.max_files_per_uploadstring | integer | null
data.max_total_bytesstring | null
data.max_total_uploadsstring | integer | null
data.max_bytes_per_personstring | null
data.max_uploads_per_personstring | integer | null
data.show_space_leftbooleanThe page shows uploaders how much room is left.
data.usageobject | null
data.usage.received_bytesstringBytes, as a decimal string.
data.usage.received_filesstring | integer
data.usage.uploadsstring | integer
data.usage.reserved_bytesstringBytes held for uploads still landing.
data.files_receivedstring | integer
data.has_passwordboolean
data.notifyobject | null
data.notify.emails?array of string
data.notify.webhook_url?string
data.access"public" | "tracked" | "restricted"
data.allowed_emailsarray of string
data.retention_daysstring | integer | null
data.effective_retention_daysstring | integer
data.allowed_typesarray of string | null
data.reject_flaggedboolean
data.brand_idstring | null
data.questionsarray of UploadQuestion
data.questions.idstring
data.questions.labelstring
data.questions.type"short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent"
data.questions.requiredboolean
data.questions.help?string
data.questions.options?array of string
data.questions.settings?objectThe type's own settings (DZ4).
data.require_contactboolean
data.view_onlybooleanReceived PDFs and images are view-only (team seats).
data.member_markbooleanEach preview is the member's own copy with their email on it.
data.view_only_other"download" | "block"
data.team_visiblebooleanThe team's Owner and Admins see what arrives through this page (team seats).
data.embed_originsarray of stringThe sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site.
data.widget_share_answersbooleanThe host page's `all_complete` widget event carries the uploader's answers.
data.widgetobjectThe script widget for this page.
data.widget.allowedbooleanThe plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan.
data.widget.needsstringThe plan the script widget needs.
data.widget.secret_issuedbooleanA widget secret was issued (GET /v1/file-requests/:id/widget-secret).
data.e2ebooleanUploads are encrypted end to end in the uploader's browser; SendFlo stores ciphertext and each file's key sealed to the page's recipients.
data.e2e_availablebooleanThe plan has encrypted uploads (Pro, Team Pro, Team Enterprise).
data.expires_atdate-time | null
data.created_atdate-time
data.updated_atdate-time
limitsFileRequestLimits
limits.plan_class"free" | "starter" | "plus" | "pro" | "enterprise"
limits.reception_linksobject
limits.reception_links.usedstring | integer
limits.reception_links.maxstring | integer0 = unlimited.
limits.retention_default_daysstring | integer
limits.retention_max_daysstring | integer
limits.request_expiry_free_daysstring | integer
limits.paidboolean
limits.max_transfer_bytesstringThe plan's size per transfer, in bytes as a decimal string: the most one file or one upload can be.

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/file-requests" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/file-requests
POST/file-requestsCreate a file request or upload page

Creates an upload page at /u/<slug>. A key of your API & Agents workspace pays 1 credit per request, charged only once the request is created (402 insufficient_credits without a balance), and gets the paid features on any plan. Any other key follows its workspace's plan, as in the app (Verified and Only-them access, a custom retention, notification routing, a brand kit and a custom expiry are paid features), and costs no credits. Send an Idempotency-Key header (1 to 128 visible ASCII characters) to retry safely: within 24 hours the same key returns the request it created, marked idempotent-replayed: true, and is never charged twice. Returns notify_webhook_secret once when you set notify.webhook_url.

Key scope: file_requests.write

Request body (JSON)

name?string | nullYour own label, never shown to uploaders (120 characters at most).
prompt?string | nullThe message shown to the uploader (2000 characters at most).
kind?"request" | "reception"`request` (the default; closes after `max_files`) or `reception` (stays open).
max_files?string | integer1–50 (1 by default); ignored for `reception`.
max_bytes_per_file?string | integer | nullA size limit per file, in bytes; null = none.
max_bytes_per_upload?string | integer | nullA size limit for the files someone sends together, in bytes; null = none. Every plan.
max_files_per_upload?string | integer | nullHow many files someone can send together (1–1000); null = the plan's files per transfer.
max_total_bytes?string | integer | nullA size limit for everything the page receives, in bytes; null = none. Files the malware scan rejects don't count. When it is reached the page answers 410 `page_full`. Every plan.
max_total_uploads?string | integer | nullHow many uploads the page accepts in all (1–1,000,000); null = no limit. Every plan.
max_bytes_per_person?string | integer | nullA size limit per verified uploader, in bytes; null = none. Paid plans (403 `paid_required`). Turns `access` to `tracked` on a public page (422 `verified_uploaders_required` if you also ask for `public`): a typed email proves nothing.
max_uploads_per_person?string | integer | nullHow many uploads one verified uploader can send (1–100,000); null = no limit. Paid plans; like `max_bytes_per_person`.
show_space_left?booleanShow uploaders how much room the page has left.
password?string | nullAn upload-page password; null = none.
notify?object | nullWho's told about each upload: `{ emails?: up to 5 addresses, webhook_url?: an https URL that receives signed upload events }`.
notify.emails?array of string
notify.webhook_url?string
access?"public" | "tracked" | "restricted"`public`, `tracked` (uploaders verify their email with a code) or `restricted` (verified and on `allowed_emails`).
allowed_emails?array of string | nullRequired for `restricted` (100 at most).
retention_days?string | integer | nullHow long received files are kept; null = the plan's default.
allowed_types?array of string | nullFile categories (image, video, document…) or extensions; empty = any type.
reject_flagged?booleanDelete uploads the malware scan blocks (true by default).
brand_id?string | nullA brand kit of this workspace; null = the default kit.
deliver_to_inbox_id?stringAn inbox that also receives the uploads.
expires_at?string | nullWhen the page stops accepting uploads; null = never.
questions?array of object | nullUp to 10 questions asked with each upload: short_text, long_text (up to 5,000 characters), email, number, date, multiple_choice (one or several, with Other), opinion_scale, phone, yes_no or consent (always required), each with its `settings`. Questions are paid, except consent, which every plan may ask (403 `paid_required` names the other types on Free). The old names `text` and `select` are read as short_text and multiple_choice; the ids `v`, `name`, `email`, `from`, `from_email`, `password`, `slug`, `embed` and `sf_*` are refused (422 `invalid_questions`).
questions.id?string
questions.labelstring
questions.typestring
questions.required?boolean
questions.options?array of string
questions.help?string
questions.settings?map of string | number | boolean | null
require_contact?booleanUploaders must give their name and email.
view_only?booleanTeam seats only (403 `team_required`): received PDFs and images open as previews and the download is refused (403 `view_only`). A team protection policy can require it (403 `policy_locked` when turned off).
member_mark?booleanTeam Pro and Team Enterprise (403 `viewer_mark_plan`): each preview is the member's own copy with their email on it, one marked view each from the team's pool. Implies `view_only`.
view_only_other?"download" | "block" | nullWhile `view_only`: `download` (the default) keeps other file types downloadable; `block` makes the page accept PDFs and images only (422 `no_viewable_types` when nothing viewable is left).
team_visible?booleanTeam seats only (403 `team_required`): the team's Owner and Admins see the files received through this page in the team's Received view (opening an original still takes a reason and is audited). Off by default.
embed_origins?array of string | nullThe sites allowed to frame the script widget: up to 20 origins (`https://example.com`, `https://*.example.com` for subdomains, `http://localhost:3000` while developing). Every widget response then carries `frame-ancestors` with this list, and the widget talks only to these sites. `null` or `[]` = any site. Pro, Team Pro and Team Enterprise (403 `plan_required`); clearing it never needs a plan.
widget_share_answers?booleanThe script widget's `all_complete` event, sent to the host page, carries the uploader's answers. Off by default. Pro and up.
e2e?booleanEncrypt uploads end to end: the upload page seals each file and its name in the uploader's browser, and seals the file's key to the page's owner (or its team inbox) and the team's recovery key. SendFlo can't scan, preview or make view-only copies of what arrives. Pro, Team Pro and Team Enterprise (403 `plan_required`); turning it on needs those keys (409 `owner_no_keys`, `inbox_no_key`), and not with `view_only` (422 `e2e_view_only`).
deliver_to?stringA team inbox's id: the page is made in that inbox and what arrives is the team's (signed-in members of the inbox only; a key gets 403 `user_credential_required`).

A name ending in ? is optional.

Response 201

idstring
slugstring
urlstringThe upload page, https://sendflo.app/u/{slug}.
state"open" | "fulfilled" | "cancelled" | "expired"
kind"request" | "reception"
personalbooleanThe account's personal upload link.
namestring | null
promptstring | null
max_filesstring | integer | null
max_bytes_per_filestring | null
max_bytes_per_uploadstring | null
max_files_per_uploadstring | integer | null
max_total_bytesstring | null
max_total_uploadsstring | integer | null
max_bytes_per_personstring | null
max_uploads_per_personstring | integer | null
show_space_leftbooleanThe page shows uploaders how much room is left.
usageobject | null
usage.received_bytesstringBytes, as a decimal string.
usage.received_filesstring | integer
usage.uploadsstring | integer
usage.reserved_bytesstringBytes held for uploads still landing.
files_receivedstring | integer
has_passwordboolean
notifyobject | null
notify.emails?array of string
notify.webhook_url?string
access"public" | "tracked" | "restricted"
allowed_emailsarray of string
retention_daysstring | integer | null
effective_retention_daysstring | integer
allowed_typesarray of string | null
reject_flaggedboolean
brand_idstring | null
questionsarray of UploadQuestion
questions.idstring
questions.labelstring
questions.type"short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent"
questions.requiredboolean
questions.help?string
questions.options?array of string
questions.settings?objectThe type's own settings (DZ4).
questions.settings.placeholder?string
questions.settings.max_length?string | integer
questions.settings.min?number
questions.settings.max?number
questions.settings.decimals?string | integer
questions.settings.unit?string
questions.settings.range?"any" | "past" | "future" | "between"
questions.settings.from?string
questions.settings.to?string
questions.settings.week_start?string | integer
questions.settings.dob?boolean
questions.settings.multiple?boolean
questions.settings.other?boolean
questions.settings.display?"list" | "dropdown"
questions.settings.scale?"0-10" | "1-5" | "1-10"
questions.settings.low_label?string
questions.settings.high_label?string
questions.settings.default_country?string
questions.settings.statement?string
questions.settings.link_url?string
questions.settings.link_label?string
require_contactboolean
view_onlybooleanReceived PDFs and images are view-only (team seats).
member_markbooleanEach preview is the member's own copy with their email on it.
view_only_other"download" | "block"
team_visiblebooleanThe team's Owner and Admins see what arrives through this page (team seats).
embed_originsarray of stringThe sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site.
widget_share_answersbooleanThe host page's `all_complete` widget event carries the uploader's answers.
widgetobjectThe script widget for this page.
widget.allowedbooleanThe plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan.
widget.needsstringThe plan the script widget needs.
widget.secret_issuedbooleanA widget secret was issued (GET /v1/file-requests/:id/widget-secret).
e2ebooleanUploads are encrypted end to end in the uploader's browser; SendFlo stores ciphertext and each file's key sealed to the page's recipients.
e2e_availablebooleanThe plan has encrypted uploads (Pro, Team Pro, Team Enterprise).
expires_atdate-time | null
created_atdate-time
updated_atdate-time
notify_webhook_secret?stringThe webhook's signing secret, shown once when `notify.webhook_url` is new.

A name ending in ? is optional.

An error: insufficient_credits, paid_required, reception_link_limit, invalid_webhook, allowed_emails_required, retention_too_long, brand_not_found, brand_locked, invalid_questions, invalid_expiry, invalid_idempotency_key, idempotency_in_progress, team_required, viewer_mark_plan, policy_locked, no_viewable_types, invalid_embed_origins, plan_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/file-requests" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/file-requests/{id}Get a file request

One file request or upload page of this workspace, with how many files it received.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Response 200

idstring
slugstring
urlstringThe upload page, https://sendflo.app/u/{slug}.
state"open" | "fulfilled" | "cancelled" | "expired"
kind"request" | "reception"
personalbooleanThe account's personal upload link.
namestring | null
promptstring | null
max_filesstring | integer | null
max_bytes_per_filestring | null
max_bytes_per_uploadstring | null
max_files_per_uploadstring | integer | null
max_total_bytesstring | null
max_total_uploadsstring | integer | null
max_bytes_per_personstring | null
max_uploads_per_personstring | integer | null
show_space_leftbooleanThe page shows uploaders how much room is left.
usageobject | null
usage.received_bytesstringBytes, as a decimal string.
usage.received_filesstring | integer
usage.uploadsstring | integer
usage.reserved_bytesstringBytes held for uploads still landing.
files_receivedstring | integer
has_passwordboolean
notifyobject | null
notify.emails?array of string
notify.webhook_url?string
access"public" | "tracked" | "restricted"
allowed_emailsarray of string
retention_daysstring | integer | null
effective_retention_daysstring | integer
allowed_typesarray of string | null
reject_flaggedboolean
brand_idstring | null
questionsarray of UploadQuestion
questions.idstring
questions.labelstring
questions.type"short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent"
questions.requiredboolean
questions.help?string
questions.options?array of string
questions.settings?objectThe type's own settings (DZ4).
questions.settings.placeholder?string
questions.settings.max_length?string | integer
questions.settings.min?number
questions.settings.max?number
questions.settings.decimals?string | integer
questions.settings.unit?string
questions.settings.range?"any" | "past" | "future" | "between"
questions.settings.from?string
questions.settings.to?string
questions.settings.week_start?string | integer
questions.settings.dob?boolean
questions.settings.multiple?boolean
questions.settings.other?boolean
questions.settings.display?"list" | "dropdown"
questions.settings.scale?"0-10" | "1-5" | "1-10"
questions.settings.low_label?string
questions.settings.high_label?string
questions.settings.default_country?string
questions.settings.statement?string
questions.settings.link_url?string
questions.settings.link_label?string
require_contactboolean
view_onlybooleanReceived PDFs and images are view-only (team seats).
member_markbooleanEach preview is the member's own copy with their email on it.
view_only_other"download" | "block"
team_visiblebooleanThe team's Owner and Admins see what arrives through this page (team seats).
embed_originsarray of stringThe sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site.
widget_share_answersbooleanThe host page's `all_complete` widget event carries the uploader's answers.
widgetobjectThe script widget for this page.
widget.allowedbooleanThe plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan.
widget.needsstringThe plan the script widget needs.
widget.secret_issuedbooleanA widget secret was issued (GET /v1/file-requests/:id/widget-secret).
e2ebooleanUploads are encrypted end to end in the uploader's browser; SendFlo stores ciphertext and each file's key sealed to the page's recipients.
e2e_availablebooleanThe plan has encrypted uploads (Pro, Team Pro, Team Enterprise).
expires_atdate-time | null
created_atdate-time
updated_atdate-time

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/file-requests/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PATCH/file-requests/{id}Change a file request's settings

Changes only the fields you send; the address never changes. Only open requests can be changed (409 not_open). Paid features follow your plan, as when creating one (403 paid_required). notify replaces the whole routing; a new webhook_url returns a new notify_webhook_secret once.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Request body (JSON)

name?string | nullYour own label, never shown to uploaders (120 characters at most).
prompt?string | nullThe message shown to the uploader (2000 characters at most).
kind?"request" | "reception"`request` (the default; closes after `max_files`) or `reception` (stays open).
max_files?string | integer1–50 (1 by default); ignored for `reception`.
max_bytes_per_file?string | integer | nullA size limit per file, in bytes; null = none.
max_bytes_per_upload?string | integer | nullA size limit for the files someone sends together, in bytes; null = none. Every plan.
max_files_per_upload?string | integer | nullHow many files someone can send together (1–1000); null = the plan's files per transfer.
max_total_bytes?string | integer | nullA size limit for everything the page receives, in bytes; null = none. Files the malware scan rejects don't count. When it is reached the page answers 410 `page_full`. Every plan.
max_total_uploads?string | integer | nullHow many uploads the page accepts in all (1–1,000,000); null = no limit. Every plan.
max_bytes_per_person?string | integer | nullA size limit per verified uploader, in bytes; null = none. Paid plans (403 `paid_required`). Turns `access` to `tracked` on a public page (422 `verified_uploaders_required` if you also ask for `public`): a typed email proves nothing.
max_uploads_per_person?string | integer | nullHow many uploads one verified uploader can send (1–100,000); null = no limit. Paid plans; like `max_bytes_per_person`.
show_space_left?booleanShow uploaders how much room the page has left.
password?string | nullAn upload-page password; null = none.
notify?object | nullWho's told about each upload: `{ emails?: up to 5 addresses, webhook_url?: an https URL that receives signed upload events }`.
notify.emails?array of string
notify.webhook_url?string
access?"public" | "tracked" | "restricted"`public`, `tracked` (uploaders verify their email with a code) or `restricted` (verified and on `allowed_emails`).
allowed_emails?array of string | nullRequired for `restricted` (100 at most).
retention_days?string | integer | nullHow long received files are kept; null = the plan's default.
allowed_types?array of string | nullFile categories (image, video, document…) or extensions; empty = any type.
reject_flagged?booleanDelete uploads the malware scan blocks (true by default).
brand_id?string | nullA brand kit of this workspace; null = the default kit.
deliver_to_inbox_id?stringAn inbox that also receives the uploads.
expires_at?string | nullWhen the page stops accepting uploads; null = never.
questions?array of object | nullUp to 10 questions asked with each upload: short_text, long_text (up to 5,000 characters), email, number, date, multiple_choice (one or several, with Other), opinion_scale, phone, yes_no or consent (always required), each with its `settings`. Questions are paid, except consent, which every plan may ask (403 `paid_required` names the other types on Free). The old names `text` and `select` are read as short_text and multiple_choice; the ids `v`, `name`, `email`, `from`, `from_email`, `password`, `slug`, `embed` and `sf_*` are refused (422 `invalid_questions`).
questions.id?string
questions.labelstring
questions.typestring
questions.required?boolean
questions.options?array of string
questions.help?string
questions.settings?map of string | number | boolean | null
require_contact?booleanUploaders must give their name and email.
view_only?booleanTeam seats only (403 `team_required`): received PDFs and images open as previews and the download is refused (403 `view_only`). A team protection policy can require it (403 `policy_locked` when turned off).
member_mark?booleanTeam Pro and Team Enterprise (403 `viewer_mark_plan`): each preview is the member's own copy with their email on it, one marked view each from the team's pool. Implies `view_only`.
view_only_other?"download" | "block" | nullWhile `view_only`: `download` (the default) keeps other file types downloadable; `block` makes the page accept PDFs and images only (422 `no_viewable_types` when nothing viewable is left).
team_visible?booleanTeam seats only (403 `team_required`): the team's Owner and Admins see the files received through this page in the team's Received view (opening an original still takes a reason and is audited). Off by default.
embed_origins?array of string | nullThe sites allowed to frame the script widget: up to 20 origins (`https://example.com`, `https://*.example.com` for subdomains, `http://localhost:3000` while developing). Every widget response then carries `frame-ancestors` with this list, and the widget talks only to these sites. `null` or `[]` = any site. Pro, Team Pro and Team Enterprise (403 `plan_required`); clearing it never needs a plan.
widget_share_answers?booleanThe script widget's `all_complete` event, sent to the host page, carries the uploader's answers. Off by default. Pro and up.
e2e?booleanEncrypt uploads end to end: the upload page seals each file and its name in the uploader's browser, and seals the file's key to the page's owner (or its team inbox) and the team's recovery key. SendFlo can't scan, preview or make view-only copies of what arrives. Pro, Team Pro and Team Enterprise (403 `plan_required`); turning it on needs those keys (409 `owner_no_keys`, `inbox_no_key`), and not with `view_only` (422 `e2e_view_only`).

A name ending in ? is optional.

Response 200

idstring
slugstring
urlstringThe upload page, https://sendflo.app/u/{slug}.
state"open" | "fulfilled" | "cancelled" | "expired"
kind"request" | "reception"
personalbooleanThe account's personal upload link.
namestring | null
promptstring | null
max_filesstring | integer | null
max_bytes_per_filestring | null
max_bytes_per_uploadstring | null
max_files_per_uploadstring | integer | null
max_total_bytesstring | null
max_total_uploadsstring | integer | null
max_bytes_per_personstring | null
max_uploads_per_personstring | integer | null
show_space_leftbooleanThe page shows uploaders how much room is left.
usageobject | null
usage.received_bytesstringBytes, as a decimal string.
usage.received_filesstring | integer
usage.uploadsstring | integer
usage.reserved_bytesstringBytes held for uploads still landing.
files_receivedstring | integer
has_passwordboolean
notifyobject | null
notify.emails?array of string
notify.webhook_url?string
access"public" | "tracked" | "restricted"
allowed_emailsarray of string
retention_daysstring | integer | null
effective_retention_daysstring | integer
allowed_typesarray of string | null
reject_flaggedboolean
brand_idstring | null
questionsarray of UploadQuestion
questions.idstring
questions.labelstring
questions.type"short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent"
questions.requiredboolean
questions.help?string
questions.options?array of string
questions.settings?objectThe type's own settings (DZ4).
questions.settings.placeholder?string
questions.settings.max_length?string | integer
questions.settings.min?number
questions.settings.max?number
questions.settings.decimals?string | integer
questions.settings.unit?string
questions.settings.range?"any" | "past" | "future" | "between"
questions.settings.from?string
questions.settings.to?string
questions.settings.week_start?string | integer
questions.settings.dob?boolean
questions.settings.multiple?boolean
questions.settings.other?boolean
questions.settings.display?"list" | "dropdown"
questions.settings.scale?"0-10" | "1-5" | "1-10"
questions.settings.low_label?string
questions.settings.high_label?string
questions.settings.default_country?string
questions.settings.statement?string
questions.settings.link_url?string
questions.settings.link_label?string
require_contactboolean
view_onlybooleanReceived PDFs and images are view-only (team seats).
member_markbooleanEach preview is the member's own copy with their email on it.
view_only_other"download" | "block"
team_visiblebooleanThe team's Owner and Admins see what arrives through this page (team seats).
embed_originsarray of stringThe sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site.
widget_share_answersbooleanThe host page's `all_complete` widget event carries the uploader's answers.
widgetobjectThe script widget for this page.
widget.allowedbooleanThe plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan.
widget.needsstringThe plan the script widget needs.
widget.secret_issuedbooleanA widget secret was issued (GET /v1/file-requests/:id/widget-secret).
e2ebooleanUploads are encrypted end to end in the uploader's browser; SendFlo stores ciphertext and each file's key sealed to the page's recipients.
e2e_availablebooleanThe plan has encrypted uploads (Pro, Team Pro, Team Enterprise).
expires_atdate-time | null
created_atdate-time
updated_atdate-time
notify_webhook_secret?stringThe webhook's signing secret, shown once when `notify.webhook_url` is new.

A name ending in ? is optional.

An error: not_found, not_open, paid_required, invalid_webhook, allowed_emails_required, retention_too_long, brand_not_found, brand_locked, invalid_questions, invalid_expiry, team_required, viewer_mark_plan, policy_locked, no_viewable_types, invalid_embed_origins, plan_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/file-requests/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
POST/file-requests/{id}/cancelStop a file request accepting uploads

Closes it (cancelled). Safe to repeat.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Response 200

oktrue

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/file-requests/{id}/cancel" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/file-requests/{id}/webhook/deliveriesList a link's webhook deliveries

The link's own webhook (notify.webhook_url): every attempt of the last 30 days, newest first — the status your server answered, how long it took, the start of its answer. attempt 0 is a Resend. 404 no_webhook when the link has none.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Query parameters

limit?string | integerHow many (1–200, 50 by default).

Response 200

dataarray of WebhookDelivery
data.idstring
data.event_idstringThe event's id — the `webhook-id` header; the same on every attempt and on Resend.
data.event_typestring
data.attemptstring | integer1–6; 0 for a Resend.
data.okbooleanYour server answered 2xx.
data.status_codestring | integer | null
data.errorstring | null
data.duration_msstring | integer | null
data.response_excerptstring | null
data.created_atdate-time

A name ending in ? is optional.

An error: not_found, no_webhook, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/file-requests/{id}/webhook/deliveries" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/file-requests/{id}/webhook/deliveries/{deliveryId}/resendResend a link's webhook delivery

Sends the same body again, with the same webhook-id (so your server can tell it's the same event), to the link's current webhook address. Queued: answers 202.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.
deliveryIdstringA delivery from the log.

Response 202

queuedtrue
event_idstring

A name ending in ? is optional.

An error: not_found, no_webhook, body_not_kept, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/file-requests/{id}/webhook/deliveries/{deliveryId}/resend" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/file-requests/{id}/webhook/resumeResume a link's paused webhook

A link's webhook pauses after 5 events in a row whose every attempt failed (notify.webhook_paused_at); you're told in the app and by email. Fix your server, then resume. Saving the link's notify resumes it too.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Response 200

idstring
webhook_paused_atnull
webhook_failed_eventsstring | integer

A name ending in ? is optional.

An error: not_found, no_webhook, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/file-requests/{id}/webhook/resume" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/file-requests/{id}/widget-secretShow an upload page's widget secret

The secret your server signs "verified by your site" tokens with (the script widget, Pro, Team Pro and Team Enterprise; see /docs/widget). null until you issue one with POST.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Response 200

secretstring | null
versionstring | integerHow many times one was issued.

A name ending in ? is optional.

An error: not_found, plan_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/file-requests/{id}/widget-secret" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/file-requests/{id}/widget-secretIssue a new widget secret

Issues a new secret for "verified by your site" tokens; the previous one stops working at once. Pro, Team Pro and Team Enterprise.

Key scope: file_requests.write

Path parameters

idstringThe file request's id.

Response 200

secretstring
versionstring | integer

A name ending in ? is optional.

An error: not_found, plan_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/file-requests/{id}/widget-secret" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

inboxes

Addressable inboxes that receive files for an agent.

GET/inboxesList your inboxes

The workspace's inboxes, newest first (200 at most).

Key scope: inboxes.read

Response 200

dataarray of Inbox
data.idstring
data.namestring | null
data.addressstring
data.default_ttl_secondsstring | integerHow long received items are kept.
data.created_atdate-time

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/inboxes" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/inboxes
POST/inboxesCreate an inbox

An inbox with a 12-character address. Items in it expire after default_ttl_seconds (7 days by default, 30 at most). Files reach it through a file request's deliver_to_inbox_id.

Key scope: inboxes.write

Request body (JSON)

name?stringYour label (120 characters at most).
default_ttl_seconds?string | integerHow long items are kept: 60 seconds to 30 days (7 days by default).

A name ending in ? is optional.

Response 201

idstring
addressstring
urlstring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/inboxes" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/inboxes/{id}/itemsList an inbox's files

The 50 newest items. An unknown inbox gives an empty list. Check file.scan_state before you use a file.

Key scope: inboxes.read

Path parameters

idstringThe inbox's id.

Response 200

dataarray of InboxItem
data.idstring
data.fileobject
data.file.idstring
data.file.namestring
data.file.sizestring
data.file.content_typestring | null
data.file.scan_state"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable"
data.fromstring | null
data.metadatamap of any | null
data.received_atdate-time
data.expires_atdate-time | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/inboxes/{id}/items" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

webhooks

Signed HTTPS callbacks for events in your workspace.

GET/eventsList events

Every event of the last 30 days, newest first, whether or not an endpoint received it: for polling (Zapier, Make, n8n), catching up after downtime, or a SIEM. Filter by types (comma-separated) and since; page with cursor (the previous page's next).

Key scope: webhooks.read

Query parameters

types?stringEvent types, comma-separated.
since?stringISO time.
cursor?stringThe previous page's `next`.
limit?string | integer1–100 (50).

Response 200

dataarray of StoredEvent
data.idstring
data.typestring
data.versionstring | integer
data.created_atdate-time
data.dataany
nextstring | null

A name ending in ? is optional.

An error: invalid_since, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/events" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/events
GET/events/{id}Read one event

One event of the last 30 days, as it was sent (its data is the webhook body's data).

Key scope: webhooks.read

Path parameters

idstringAn event's id (the webhook body's `id`).

Response 200: any

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/events/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/webhook-deliveries/{id}Read one delivery

One attempt from the delivery log: the request body SendFlo sent and your server's answer (its headers and first 2 KB).

Key scope: webhooks.read

Path parameters

idstringA delivery's id.

Response 200: any

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/webhook-deliveries/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/webhooksList your webhook endpoints

Every registered endpoint, newest first, without its secret, with its last 24 hours of deliveries (deliveries_24h, success_rate_24h). A key needs webhooks.read (or webhooks.write). When a workspace's plan stops including webhooks (below Plus), its endpoints keep running for 30 days (plan_pause_at), with a notice and a reminder, then pause (plan_paused) until an upgrade. Events during a pause are kept 30 days: replay them after upgrading. An endpoint pauses (disabled: true, paused_at set) after 5 events in a row whose every attempt failed — one bad event never pauses it — or at once when its address stops being public; you're told in the app and by email. Resume it with POST /v1/webhooks/:id/resume.

Key scope: webhooks.read

Response 200

dataarray of WebhookEndpoint
data.idstring
data.urlstring
data.eventsarray of string
data.disabledboolean
data.failure_countstring | integerFailed attempts since the last success.
data.failed_eventsstring | integerEvents in a row whose every attempt failed; the endpoint pauses at 5.
data.paused_atdate-time | null
data.teambooleanA team-wide endpoint: every member's events.
data.created_atdate-time
data.descriptionstring | null
data.filtersany | null
data.payload_versionstring | integerThe payload version this endpoint receives (2).
data.appstring | null
data.plan_pausedbooleanPaused because the plan has no webhooks; an upgrade resumes it.
data.plan_pause_atdate-time | null
data.paused_reason"failing" | "address" | "manual" | "staff" | null
data.secret_rotated_atdate-time | null
data.deliveries_24h?string | integer
data.success_rate_24h?number | null
allowedbooleanThis workspace's plan has webhooks (Plus, Pro, Enterprise, the API & Agents workspace).

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/webhooks" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/webhooks
POST/webhooksRegister a webhook endpoint

The URL must be https and public (its name is resolved now; a private answer is refused). events are names from the event list below, a family such as file.*, or * for everything; an unknown name is refused (422 unknown_event). Deliveries are signed the Standard Webhooks way with secret (whsec_…), which is shown only now; see Webhooks above. The access events (access.*: opened, previewed, downloaded) are on Team Pro and Team Enterprise; with team: true the team's owner or an admin makes an endpoint that receives every member's.

Key scope: webhooks.write

Request body (JSON)

urlstringAn https URL (2048 characters at most).
eventsarray of stringThe events to receive.
team?booleanTeam Pro and Team Enterprise, the key's maker the team's owner or an admin: receive every member's events, not only this workspace's.
description?string | nullWhat it's for (300 characters).
filters?object | null`{ file_request_ids?, link_ids?, destination_ids? }`: only events about these upload pages, links or destinations.
filters.file_request_ids?array of string
filters.link_ids?array of string
filters.destination_ids?array of string
app?"zapier" | "make" | "n8n"The automation app subscribing it: zapier, make or n8n (shown on the Webhooks page).

A name ending in ? is optional.

Response 201

idstring
urlstring
eventsarray of string
teamboolean
secretstringShown only now.
descriptionstring | null
filtersany | null
payload_versionstring | integer
appstring | null
disabledboolean
plan_pausedboolean
plan_pause_atdate-time | null
failure_countstring | integer
failed_eventsstring | integer
paused_atdate-time | null
paused_reasonstring | null
secret_rotated_atdate-time | null
created_atdate-time

A name ending in ? is optional.

An error: invalid_url, unknown_event, plan_required, forbidden, webhooks_plan, webhook_limit, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"…","events":["…"]}'
GET/webhooks/event-typesList the event types

Every event SendFlo sends: its family, payload version, what it means and when it is sent, and a sample of its data (exactly its schema; the same sample POST /v1/webhooks/:id/test sends). team_only: the access stream, Team Pro and Team Enterprise.

Key scope: webhooks.read

Response 200

dataarray of object
data.typestring
data.familystring
data.versionstring | integer
data.summarystring
data.whenstring
data.team_onlyboolean
data.sampleanyA sample of the event's data.

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/webhooks/event-types" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/webhooks/event-types
PATCH/webhooks/{id}Change a webhook endpoint

Change its address (checked like a new one), its events, a description, filters ({ file_request_ids?, link_ids?, destination_ids? }: only events about these), or enabled (true resumes a paused endpoint and clears its counts; false pauses it).

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Request body (JSON)

url?stringAn https, public URL.
events?array of stringThe events to receive.
description?string | nullWhat it's for (300 characters).
filters?object | nullOnly events about these upload pages, links or destinations; null clears them.
filters.file_request_ids?array of string
filters.link_ids?array of string
filters.destination_ids?array of string
enabled?booleanOn or off.

A name ending in ? is optional.

Response 200: any

An error: not_found, invalid_url, unknown_event, nothing_to_change, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/webhooks/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/webhooks/{id}Delete a webhook endpoint

Deliveries stop at once. Answers 204, also for an unknown id.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Response 204: no body.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/webhooks/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/webhooks/{id}/deliveriesList an endpoint's deliveries

Every attempt of the last 30 days, newest first: the event, the status your server answered (or why none came: timeout, dns, private_address…), how long it took and the start of its answer. Each event is tried 6 times with growing waits; attempt 0 is a Resend.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Query parameters

limit?string | integerHow many (1–200, 50 by default).

Response 200

dataarray of WebhookDelivery
data.idstring
data.event_idstringThe event's id — the `webhook-id` header; the same on every attempt and on Resend.
data.event_typestring
data.attemptstring | integer1–6; 0 for a Resend.
data.okbooleanYour server answered 2xx.
data.status_codestring | integer | null
data.errorstring | null
data.duration_msstring | integer | null
data.response_excerptstring | null
data.created_atdate-time

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/webhooks/{id}/deliveries" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/webhooks/{id}/deliveries/{deliveryId}/resendResend a delivery

Sends the same body again with the same webhook-id (deduplicate on it), freshly signed. Queued: answers 202. 409 body_not_kept for a body too large to keep (over 64 KiB).

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.
deliveryIdstringA delivery from the log.

Response 202

queuedtrue
event_idstring

A name ending in ? is optional.

An error: not_found, body_not_kept, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks/{id}/deliveries/{deliveryId}/resend" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/webhooks/{id}/replayReplay a window of events

Sends the endpoint again every event of a window (kept 30 days) it subscribes to, with the same event id and body, freshly signed: after an outage, or to catch up. only_failed skips events it already received. At most 1,000 events (422 replay_too_large). Queued: answers 202.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Request body (JSON)

sincestringISO time the window starts.
until?stringISO time it ends (now by default).
types?array of stringOnly these event types.
only_failed?booleanOnly events this endpoint never received.

A name ending in ? is optional.

Response 202

queuedstring | integer

A name ending in ? is optional.

An error: not_found, invalid_window, replay_too_large, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks/{id}/replay" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"since":"…"}'
POST/webhooks/{id}/resumeResume a paused endpoint

Turns a paused endpoint back on and clears its failure counts. Events from while it was paused aren't sent; Resend the ones you need from the delivery log.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Response 200

idstring
disabledboolean
paused_atnull
failed_eventsstring | integer

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks/{id}/resume" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/webhooks/{id}/rotate-secretRotate an endpoint's secret

A new whsec_… secret, shown only now. For 24 hours deliveries carry two signatures in webhook-signature (the new and the previous secret's), so you can switch your receiver without missing an event.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Response 200

idstring
secretstring
previous_valid_untildate-time

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks/{id}/rotate-secret" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/webhooks/{id}/testSend a test event

Sends a sample of any event type (the documented example, exactly its schema; its id starts evt_test_) to the endpoint now, signed like any delivery and kept in its log. Queued: answers 202.

Key scope: webhooks.write

Path parameters

idstringThe endpoint's id.

Request body (JSON)

type?stringAn event type (file_request.fulfilled by default).

A name ending in ? is optional.

Response 202

queuedboolean
event_idstring
typestring

A name ending in ? is optional.

An error: not_found, unknown_event, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/webhooks/{id}/test" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'

notifications

Where SendFlo notifies you: Slack, Discord, Teams, webhooks, email.

GET/notification-channelsList your notification channels

The channels of the person who made the key (not your account's own email), oldest first, with masked targets, and this month's additions per kind. A key made before keys belonged to a person gets 403 key_needs_reissue.

Key scope: notifications.write

Response 200

dataarray of NotificationChannel
data.idstring
data.channel"email" | "slack" | "discord" | "teams" | "webhook"
data.namestring
data.targetstringThe address or URL, partly masked.
data.activeboolean
data.pending?true
data.detail?string
limitsNotificationChannelLimits
limits.emailobject
limits.email.usedstring | integer
limits.email.allowancestring | integer
limits.slackobject
limits.slack.usedstring | integer
limits.slack.allowancestring | integer
limits.discordobject
limits.discord.usedstring | integer
limits.discord.allowancestring | integer
limits.teamsobject
limits.teams.usedstring | integer
limits.teams.allowancestring | integer
limits.webhookobject
limits.webhook.usedstring | integer
limits.webhook.allowancestring | integer

A name ending in ? is optional.

An error: key_needs_reissue, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/notification-channels" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/notification-channels
POST/notification-channelsAdd a notification channel

An email channel starts pending and gets a confirmation link. A webhook channel returns its signing secret once. Slack URLs must be on hooks.slack.com and Discord on discord.com; every URL must be https and public. Limited per month and by plan.

Key scope: notifications.write

Request body (JSON)

channelstring`email`, `slack`, `discord`, `teams` or `webhook`.
namestringYour label (120 characters at most).
targetstringAn email address or an https webhook URL.

A name ending in ? is optional.

Response 200

idstring
secret?string
pending?true

A name ending in ? is optional.

An error: key_needs_reissue, invalid_request, monthly_limit, plan_limit, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/notification-channels" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"channel":"…","name":"…","target":"…"}'
PATCH/notification-channels/{id}Rename, retarget or pause a channel

Changes the fields you send. A new email address gets a confirmation link and receives nothing until it's confirmed (pending: true). active: true turns a paused channel back on.

Key scope: notifications.write

Path parameters

idstringThe channel's id.

Request body (JSON)

name?stringYour label.
target?stringA new address or URL.
active?booleanOn or off.

A name ending in ? is optional.

Response 200

oktrue
pending?true

A name ending in ? is optional.

An error: key_needs_reissue, not_found, invalid_request, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/notification-channels/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/notification-channels/{id}Remove a notification channel

Removes it for good. It still counts toward this month's additions.

Key scope: notifications.write

Path parameters

idstringThe channel's id.

Response 200

oktrue

A name ending in ? is optional.

An error: key_needs_reissue, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/notification-channels/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/notification-channels/{id}/testSend a test notification

Sends a real test message. A pending email channel gets its confirmation link again instead (409 unconfirmed). A failed delivery answers 424 delivery_failed.

Key scope: notifications.write

Path parameters

idstringThe channel's id.

Response 200

oktrue

A name ending in ? is optional.

An error: key_needs_reissue, unconfirmed, delivery_failed, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/notification-channels/{id}/test" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

feedback

Recipients' comments, ratings and approvals on a send.

GET/links/{id}/feedbackRead a transfer's feedback summary

The whole transfer: its feedback settings, per-file totals (revoked and expired files too, with live: false) and the people who gave feedback, with their email and how they were identified (the 500 most recently active).

Key scope: links.write

Path parameters

idstringThe id of any file (link) in the transfer.

Response 200

settingsFeedbackSettingsThe transfer's feedback settings.
settings.comments"off" | "open" | "private"
settings.ratingsboolean
settings.favoritesboolean
settings.approvalsboolean
settings.rating_scale"stars5" | "score10"
settings.encryptedbooleanAn end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>).
root_idstringThe transfer (its first link's id).
ended_atdate-time | null
kept_untildate-time | null
all_approvedbooleanApprovals are on, every live file is approved, and nobody asked for changes.
filesarray of FeedbackFileSummary
files.link_idstring
files.slugstring
files.namestring
files.livebooleanNot revoked, not expired, already available.
files.commentsstring | integer
files.hiddenstring | integer
files.favoritesstring | integer
files.ratingobject
files.rating.countstring | integer
files.rating.averagenumber | null
files.rating.distributionmap of string | integerRating value → how many gave it.
files.approvalsobject
files.approvals.approvedstring | integer
files.approvals.changesstring | integer
reviewersarray of FeedbackReviewerSummary
reviewers.idstring
reviewers.namestring
reviewers.emailstring
reviewers.proof"account" | "access" | "email"
reviewers.senderboolean
reviewers.blockedboolean
reviewers.created_atdate-time
reviewers.last_active_atdate-time
reviewers.commentsstring | integerTheir comments (deleted ones with replies included).
reviewers.ratingsstring | integer
reviewers.favoritesstring | integer
reviewers.approvalsstring | integer

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/feedback" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/feedback/commentsList one file's comments

One file's comments as the owner sees them: hidden comments, blocked people and emails included. Oldest first, 50 per page, replies carry parent_id. Pass the previous page's next as after.

Key scope: links.write

Path parameters

idstringThe id of any file (link) in the transfer.

Query parameters

filestringThe file's link id or slug (it must be in this transfer).
after?stringThe `next` of the previous page.

Response 200

commentsarray of FeedbackComment
comments.idstring
comments.filestringThe file's link slug.
comments.parent_idstring | null
comments.bodystring | null
comments.encrypted?true
comments.anchorFeedbackAnchor | null
comments.created_atdate-time
comments.editedboolean
comments.resolvedboolean
comments.mineboolean
comments.hiddenbooleanHidden from recipients (a block, or Trust & Safety).
comments.authorobject
comments.author.namestring
comments.author.verifiedtrue
comments.author.proof"account" | "access" | "email"
comments.author.senderboolean
comments.author.emailstring
comments.author.idstringThe person's reviewer id (for block and unblock).
comments.author.blockedboolean
nextstring | null

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/feedback/comments" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/links/{id}/feedback/comments/{cid}Delete a comment

Deletes any comment on the transfer. One that still has replies stays as a placeholder (body: null) so the replies keep their place.

Key scope: feedback.write

Path parameters

idstringThe id of any file (link) in the transfer.
cidstringThe comment's id.

Response 204: no body.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/links/{id}/feedback/comments/{cid}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/links/{id}/feedback/comments/{cid}/resolveResolve a comment thread

Marks a top-level comment resolved (replies answer 404). Safe to repeat.

Key scope: feedback.write

Path parameters

idstringThe id of any file (link) in the transfer.
cidstringThe top-level comment's id.

Response 200

commentobject
comment.idstring
comment.resolvedboolean

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/links/{id}/feedback/comments/{cid}/resolve" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/links/{id}/feedback/comments/{cid}/resolveReopen a resolved comment thread

Clears the resolved mark of a top-level comment. Safe to repeat.

Key scope: feedback.write

Path parameters

idstringThe id of any file (link) in the transfer.
cidstringThe top-level comment's id.

Response 200

commentobject
comment.idstring
comment.resolvedboolean

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/links/{id}/feedback/comments/{cid}/resolve" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/feedback/exportExport a transfer's feedback

Every comment, rating, favourite and approval, with names and emails, as a download. CSV by default (UTF-8 with a byte-order mark, formula-safe cells; columns type,file,person,email,verified_by,value,comment,at,where,reply_to,resolved,hidden,date), or JSON with format=json.

Key scope: links.write

Path parameters

idstringThe id of any file (link) in the transfer.

Query parameters

format?"csv" | "json"`csv` (the default) or `json`.

Response 200 (text/csv): string

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/feedback/export" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/links/{id}/feedback/reviewers/{rid}/blockBlock a person on this transfer

Hides all their comments from everyone else, signs them out and refuses anything more from them on this transfer. The sender can't be blocked (409).

Key scope: feedback.write

Path parameters

idstringThe id of any file (link) in the transfer.
ridstringThe person's reviewer id (`author.id`).

Response 200

reviewerFeedbackReviewerA person giving feedback on one transfer.
reviewer.idstring
reviewer.namestring
reviewer.emailstring
reviewer.proof"account" | "access" | "email"
reviewer.senderboolean
reviewer.blockedboolean
reviewer.created_atdate-time
reviewer.last_active_atdate-time

A name ending in ? is optional.

An error: not_found, cannot_block_sender, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/links/{id}/feedback/reviewers/{rid}/block" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/links/{id}/feedback/reviewers/{rid}/blockUnblock a person on this transfer

Lifts the block and shows again the comments the block hid (a Trust & Safety hold stays). Safe to repeat.

Key scope: feedback.write

Path parameters

idstringThe id of any file (link) in the transfer.
ridstringThe person's reviewer id.

Response 200

reviewerFeedbackReviewerA person giving feedback on one transfer.
reviewer.idstring
reviewer.namestring
reviewer.emailstring
reviewer.proof"account" | "access" | "email"
reviewer.senderboolean
reviewer.blockedboolean
reviewer.created_atdate-time
reviewer.last_active_atdate-time

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/links/{id}/feedback/reviewers/{rid}/block" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/links/{id}/repliesList a transfer's secure replies

Replies people sent back on the transfer's page (CA6c, docs/SECURE-REPLIES.md), newest first: who (a verified address), the message and the files that arrived (also in Received, scanned like any upload). On an end-to-end transfer the message (sfm1.…) and each file's name (sealed_name) are sealed with the link key: open them with the full link (packages/transfer openName; the message with additional data reply). Replies are on for Tracked and Restricted transfers unless the sender turned them off (replies_off). A reply to a secure message (CA16) is sealed with a key from the link's client half and its own key_half, given only to the signed-in sender (never to an API key): an open-once message's own half is wiped when it's read.

Key scope: links.write

Path parameters

idstringThe id of any file (link) in the transfer.

Response 200

repliesTransferReplies
replies.idstring
replies.from_emailstringThe replier's verified address.
replies.bodystring | null
replies.encryptedboolean
replies.created_atdate-time
replies.files_expectedstring | integerHow many files the replier said they'd send.
replies.filesarray of object
replies.files.item_idstring
replies.files.file_idstring
replies.files.namestring
replies.files.sealed_name?string
replies.files.sizestring
replies.files.scanstring
replies.key_half?stringA reply to a secure message: the half its key is derived from with the link's client half (packages/transfer `deriveSplitLinkKey`). Only for a signed-in sender, never an API key.
replies_offbooleanThe sender turned replies off for this transfer.
replies_onbooleanRecipients can reply now (Tracked or Restricted, not turned off).

A name ending in ? is optional.

An error: not_found, feedback_unavailable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/links/{id}/replies" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

branding

Logos, colours and brand kits shown on your pages and emails.

GET/brandingRead the default brand kit

The workspace's default brand kit; every field is null when there is none. logo_url and wallpaper_url are previews valid for 1 hour.

Key scope: links.write

Response 200

brand_idstring | null
logo_keystring | null
wallpaper_keystring | null
accentstring | null
heading_colorstring | null
page_textstring | null
link_urlstring | null
link_labelstring | null
logo_urlstring | null
wallpaper_urlstring | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/branding" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/branding
PUT/brandingReplace the default brand kit

Paid plans. Replaces the whole default kit: a field you leave out is cleared. Creates the kit when there is none (then name names it). Images are checked when you save (PNG, JPEG, WebP, GIF or an SVG without scripts).

Key scope: links.write

Request body (JSON)

name?stringThe kit's name, used only when this creates it.
logo_key?string | nullA key from POST /v1/branding/assets; null or "" clears it.
wallpaper_key?string | nullLike `logo_key`.
accent?string | nullA colour, `#RRGGBB`.
heading_color?string | nullA colour, `#RRGGBB`.
page_text?string | nullText shown on your pages (500 characters at most).
link_url?string | nullAn `https:` link shown on your pages.
link_label?string | nullThe link's label (80 characters at most).
thank_you_text?string | nullShown to uploaders when their files have arrived (300 characters). Every kit.
appearance?string | null`auto` (the visitor's system), `light` or `dark`; null follows the visitor's own theme. Every kit.
receipt?booleanMail the uploader a receipt when their files arrive (when the page knows their address). Every kit.
sender_name?string | null"<name> via SendFlo" in the From line of emails to uploaders (letters, numbers, spaces, `.,&'-`). Every kit.
reply_to?string | nullWhere replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit.
redirect_url?string | nullAn `https:` address uploaders are sent to after their files arrive. Plus and up.
favicon_key?string | nullA key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up.
tab_title?string | nullThe browser tab's title (70 characters). Plus and up.
og_image_key?string | nullA key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up.
og_title?string | nullThe link preview's title (90 characters). Plus and up.
og_description?string | nullThe link preview's description (200 characters). Plus and up.
font?string | null`inter`, `dm-sans`, `montserrat`, `poppins`, `space-grotesk`, `lora`, `merriweather` or `playfair`. Plus and up.
extra_wallpaper_keys?array of string | nullUp to 4 more wallpaper keys: the page rotates through all of them. Pro and up.
video_key?string | nullA key (`kind: video`, MP4 or WebM, 20 MB): a muted, looping background (a still wallpaper for visitors who prefer less motion). Pro and up.
language?string | nullThe upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up.
texts?map of string | nullYour own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up.

A name ending in ? is optional.

Response 200

oktrue
logo_urlstring | null
wallpaper_urlstring | null

A name ending in ? is optional.

An error: paid_required, invalid_color, invalid_asset_key, invalid_link, unsafe_image, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/branding" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
POST/branding/assetsGet an upload URL for a brand image

Paid plans. Returns a storage key and a presigned put_url (valid for 1 hour). Upload the file with PUT put_url, sending content-type equal to the content_type you gave and exactly size bytes (both are part of the signature), then use the key in the kit (logo_key, wallpaper_key, extra_wallpaper_keys, favicon_key, og_image_key or video_key). Sizes: a logo 2 MB, a wallpaper 10 MB, a favicon 512 KB, a link-preview image 5 MB, a video 20 MB; 60 upload URLs an hour per workspace. A favicon and a link-preview image need Plus, a video Pro. An uploaded file no kit uses is deleted after a day.

Key scope: links.write

Request body (JSON)

kind"logo" | "wallpaper" | "favicon" | "og" | "video"`logo`, `wallpaper`, `favicon`, `og` (link preview) or `video`.
content_typestringAn image type (`image/…`), or `video/mp4` / `video/webm` for a video.
sizestring | integerThe file's exact size in bytes.

A name ending in ? is optional.

Response 200

keystring
put_urlstring

A name ending in ? is optional.

An error: paid_required, plan_required, invalid_content_type, too_large, rate_limited, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/branding/assets" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"kind":"logo","content_type":"…","size":"…"}'
GET/brandsList brand kits

Every kit (the default first), with 1-hour preview URLs, how many the plan allows, which options the plan carries, and — on Free with kits left from a paid plan — when they'll be deleted.

Key scope: links.write

Response 200

dataarray of Brand
data.idstring
data.namestring
data.is_defaultboolean
data.logo_keystring | null
data.wallpaper_keystring | null
data.accentstring | null
data.heading_colorstring | null
data.page_textstring | null
data.link_urlstring | null
data.link_labelstring | null
data.thank_you_textstring | null
data.appearance"auto" | "light" | "dark" | null
data.receiptboolean
data.sender_namestring | null
data.reply_tostring | null
data.reply_to_verifiedbooleanReplies go to `reply_to` only once it was confirmed with the code mailed to it.
data.redirect_urlstring | null
data.favicon_keystring | null
data.tab_titlestring | null
data.og_image_keystring | null
data.og_titlestring | null
data.og_descriptionstring | null
data.fontstring | null
data.extra_wallpaper_keysarray of string
data.video_keystring | null
data.languagestring | null
data.textsobject | null
data.texts.heading?string
data.texts.intro?string
data.texts.button?string
data.logo_urlstring | null
data.wallpaper_urlstring | null
data.previewsobject
data.previews.favicon_urlstring | null
data.previews.og_image_urlstring | null
data.previews.video_urlstring | null
data.previews.extra_wallpaper_urlsarray of string | null
data.created_atdate-time
data.updated_atdate-time
limitobject
limit.usedstring | integer
limit.maxstring | integerKits the plan allows (0 on Free).
limit.plan_class"free" | "starter" | "plus" | "pro" | "enterprise"
optionsmap of objectEach option (thank_you, appearance, receipt, sender_name, reply_to, email_kit, redirect, favicon, tab_title, link_preview, font, wallpapers, video, language, texts) and whether this plan has it.
dormant_untildate-time | null
teamobject | null
team.plan_idstringThe team (its subscription id).
team.allowedbooleanTeam Plus and up: the team's kits and its lock apply.
team.needsstringThe plan team kits need (Team Plus).
team.kitsarray of BrandThe team's kits, read-only here: a page or link may name any of them.
team.kits.idstring
team.kits.namestring
team.kits.is_defaultboolean
team.kits.logo_keystring | null
team.kits.wallpaper_keystring | null
team.kits.accentstring | null
team.kits.heading_colorstring | null
team.kits.page_textstring | null
team.kits.link_urlstring | null
team.kits.link_labelstring | null
team.kits.thank_you_textstring | null
team.kits.appearance"auto" | "light" | "dark" | null
team.kits.receiptboolean
team.kits.sender_namestring | null
team.kits.reply_tostring | null
team.kits.reply_to_verifiedbooleanReplies go to `reply_to` only once it was confirmed with the code mailed to it.
team.kits.redirect_urlstring | null
team.kits.favicon_keystring | null
team.kits.tab_titlestring | null
team.kits.og_image_keystring | null
team.kits.og_titlestring | null
team.kits.og_descriptionstring | null
team.kits.fontstring | null
team.kits.extra_wallpaper_keysarray of string
team.kits.video_keystring | null
team.kits.languagestring | null
team.kits.textsobject | null
team.kits.logo_urlstring | null
team.kits.wallpaper_urlstring | null
team.kits.previewsobject
team.kits.created_atdate-time
team.kits.updated_atdate-time
team.policyobject
team.policy.lockedbooleanMembers are locked to the team's kits: their own kits don't show, and new ones (or pages naming one) answer 409 brand_locked.
team.policy.default_kit_idstring | null
team.can_managebooleanThe caller is the team's Owner or an Admin (they manage team kits in the team console).

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/brands" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/brands
POST/brandsCreate a brand kit

Paid plans, up to the plan's number of kits. The workspace's first kit becomes the default. On a team that locks members to its kits: 409 brand_locked.

Key scope: links.write

Request body (JSON)

name?stringThe kit's name ("Brand N" when blank).
logo_key?string | nullA key from POST /v1/branding/assets; null or "" clears it.
wallpaper_key?string | nullLike `logo_key`.
accent?string | nullA colour, `#RRGGBB`.
heading_color?string | nullA colour, `#RRGGBB`.
page_text?string | nullText shown on your pages (500 characters at most).
link_url?string | nullAn `https:` link shown on your pages.
link_label?string | nullThe link's label (80 characters at most).
thank_you_text?string | nullShown to uploaders when their files have arrived (300 characters). Every kit.
appearance?string | null`auto` (the visitor's system), `light` or `dark`; null follows the visitor's own theme. Every kit.
receipt?booleanMail the uploader a receipt when their files arrive (when the page knows their address). Every kit.
sender_name?string | null"<name> via SendFlo" in the From line of emails to uploaders (letters, numbers, spaces, `.,&'-`). Every kit.
reply_to?string | nullWhere replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit.
redirect_url?string | nullAn `https:` address uploaders are sent to after their files arrive. Plus and up.
favicon_key?string | nullA key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up.
tab_title?string | nullThe browser tab's title (70 characters). Plus and up.
og_image_key?string | nullA key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up.
og_title?string | nullThe link preview's title (90 characters). Plus and up.
og_description?string | nullThe link preview's description (200 characters). Plus and up.
font?string | null`inter`, `dm-sans`, `montserrat`, `poppins`, `space-grotesk`, `lora`, `merriweather` or `playfair`. Plus and up.
extra_wallpaper_keys?array of string | nullUp to 4 more wallpaper keys: the page rotates through all of them. Pro and up.
video_key?string | nullA key (`kind: video`, MP4 or WebM, 20 MB): a muted, looping background (a still wallpaper for visitors who prefer less motion). Pro and up.
language?string | nullThe upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up.
texts?map of string | nullYour own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up.

A name ending in ? is optional.

Response 201

idstring
namestring
is_defaultboolean
logo_keystring | null
wallpaper_keystring | null
accentstring | null
heading_colorstring | null
page_textstring | null
link_urlstring | null
link_labelstring | null
thank_you_textstring | null
appearance"auto" | "light" | "dark" | null
receiptboolean
sender_namestring | null
reply_tostring | null
reply_to_verifiedbooleanReplies go to `reply_to` only once it was confirmed with the code mailed to it.
redirect_urlstring | null
favicon_keystring | null
tab_titlestring | null
og_image_keystring | null
og_titlestring | null
og_descriptionstring | null
fontstring | null
extra_wallpaper_keysarray of string
video_keystring | null
languagestring | null
textsobject | null
texts.heading?string
texts.intro?string
texts.button?string
logo_urlstring | null
wallpaper_urlstring | null
previewsobject
previews.favicon_urlstring | null
previews.og_image_urlstring | null
previews.video_urlstring | null
previews.extra_wallpaper_urlsarray of string | null
created_atdate-time
updated_atdate-time

A name ending in ? is optional.

An error: paid_required, plan_required, brand_limit, brand_locked, invalid_color, invalid_asset_key, invalid_link, invalid_redirect, invalid_appearance, invalid_font, invalid_language, invalid_reply_to, invalid_sender_name, unsafe_image, unsafe_video, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/brands" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
PATCH/brands/{id}Update a brand kit

Paid plans. Changes only the fields you send; null or "" clears one. An option above the plan answers 403 plan_required with the option and the plan it needs; clearing one never does. Changing reply_to needs it confirmed again.

Key scope: links.write

Path parameters

idstringThe kit's id.

Request body (JSON)

name?stringThe kit's name.
logo_key?string | nullA key from POST /v1/branding/assets; null or "" clears it.
wallpaper_key?string | nullLike `logo_key`.
accent?string | nullA colour, `#RRGGBB`.
heading_color?string | nullA colour, `#RRGGBB`.
page_text?string | nullText shown on your pages (500 characters at most).
link_url?string | nullAn `https:` link shown on your pages.
link_label?string | nullThe link's label (80 characters at most).
thank_you_text?string | nullShown to uploaders when their files have arrived (300 characters). Every kit.
appearance?string | null`auto` (the visitor's system), `light` or `dark`; null follows the visitor's own theme. Every kit.
receipt?booleanMail the uploader a receipt when their files arrive (when the page knows their address). Every kit.
sender_name?string | null"<name> via SendFlo" in the From line of emails to uploaders (letters, numbers, spaces, `.,&'-`). Every kit.
reply_to?string | nullWhere replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit.
redirect_url?string | nullAn `https:` address uploaders are sent to after their files arrive. Plus and up.
favicon_key?string | nullA key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up.
tab_title?string | nullThe browser tab's title (70 characters). Plus and up.
og_image_key?string | nullA key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up.
og_title?string | nullThe link preview's title (90 characters). Plus and up.
og_description?string | nullThe link preview's description (200 characters). Plus and up.
font?string | null`inter`, `dm-sans`, `montserrat`, `poppins`, `space-grotesk`, `lora`, `merriweather` or `playfair`. Plus and up.
extra_wallpaper_keys?array of string | nullUp to 4 more wallpaper keys: the page rotates through all of them. Pro and up.
video_key?string | nullA key (`kind: video`, MP4 or WebM, 20 MB): a muted, looping background (a still wallpaper for visitors who prefer less motion). Pro and up.
language?string | nullThe upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up.
texts?map of string | nullYour own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up.

A name ending in ? is optional.

Response 200

idstring
namestring
is_defaultboolean
logo_keystring | null
wallpaper_keystring | null
accentstring | null
heading_colorstring | null
page_textstring | null
link_urlstring | null
link_labelstring | null
thank_you_textstring | null
appearance"auto" | "light" | "dark" | null
receiptboolean
sender_namestring | null
reply_tostring | null
reply_to_verifiedbooleanReplies go to `reply_to` only once it was confirmed with the code mailed to it.
redirect_urlstring | null
favicon_keystring | null
tab_titlestring | null
og_image_keystring | null
og_titlestring | null
og_descriptionstring | null
fontstring | null
extra_wallpaper_keysarray of string
video_keystring | null
languagestring | null
textsobject | null
texts.heading?string
texts.intro?string
texts.button?string
logo_urlstring | null
wallpaper_urlstring | null
previewsobject
previews.favicon_urlstring | null
previews.og_image_urlstring | null
previews.video_urlstring | null
previews.extra_wallpaper_urlsarray of string | null
created_atdate-time
updated_atdate-time

A name ending in ? is optional.

An error: paid_required, plan_required, not_found, invalid_color, invalid_asset_key, invalid_link, invalid_redirect, invalid_appearance, invalid_font, invalid_language, invalid_reply_to, invalid_sender_name, unsafe_image, unsafe_video, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/brands/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/brands/{id}Delete a brand kit

Links and upload pages that used it fall back to the default kit; deleting the default makes the oldest remaining kit the default.

Key scope: links.write

Path parameters

idstringThe kit's id.

Response 200

oktrue

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/brands/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/brands/{id}/defaultMake a brand kit the default

The default kit is what links and upload pages without their own kit show.

Key scope: links.write

Path parameters

idstringThe kit's id.

Response 200

oktrue

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/brands/{id}/default" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/brands/{id}/reply-to/confirmConfirm a kit's Reply-To with its code

From then on, replies to emails sent for this kit go to its reply_to. Five wrong codes end the code.

Key scope: links.write

Path parameters

idstringThe kit's id.

Request body (JSON)

codestringThe 6-digit code from the email.

A name ending in ? is optional.

Response 200

oktrue
verifiedtrue

A name ending in ? is optional.

An error: not_found, code_expired, wrong_code, too_many_attempts, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/brands/{id}/reply-to/confirm" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"code":"…"}'
POST/brands/{id}/reply-to/sendMail a code to confirm a kit's Reply-To

Every kit (Lite and up). Mails a 6-digit code to the kit's saved reply_to, valid for 15 minutes; 5 codes an hour per workspace. Answers verified: true at once when the address is already confirmed.

Key scope: links.write

Path parameters

idstringThe kit's id.

Response 200

oktrue
verifiedboolean
expires_in?string | integer

A name ending in ? is optional.

An error: plan_required, not_found, no_reply_to, rate_limited, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/brands/{id}/reply-to/send" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

agent tasks

Hand work to AI agents and follow it to the result.

GET/agent-rostersList the agents on your computers

What the SendFlo connector on each of the key creator's computers offers: its agents, presets, sessions and public key, and whether the computer is online.

Key scope: agent_tasks.read

Response 200

rostersarray of AgentRoster
rosters.device_idstring
rosters.device_namestring
rosters.platformstring
rosters.onlineboolean
rosters.agentsarray of RosterAgent
rosters.agents.idstring
rosters.agents.namestring
rosters.agents.runner"folder" | "command" | "acp" | "channel" | "session"
rosters.agents.product?string
rosters.agents.enabledboolean
rosters.agents.status?"ready" | "busy" | "paused"
rosters.agents.presets?array of AgentPreset
rosters.agents.accept?AgentAccept
rosters.agents.sessions?array of object
rosters.agents.policy?object
rosters.e2ebooleanThe computer published a key, so instructions can be sealed to it.
rosters.public_keyAgentPublicKey | null
rosters.public_key.kidstring
rosters.public_key.jwkobject
rosters.connectorobject | null
rosters.connector.version?string
rosters.connector.platform?string
rosters.updated_atdate-time

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/agent-rosters" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/agent-rosters
GET/agent-targetsList where tasks can be sent

Everything this workspace can hand a task to: project folders bound to an agent on a computer (100 at most) and its cloud agents. available: false with unavailable says why one can't take a task now.

Key scope: agent_tasks.read

Response 200

targetsarray of AgentTarget
targets.kind"project"
targets.idstringproject:<id>
targets.project_idstring
targets.namestring
targets.destinationstring
targets.deviceobject
targets.device.idstring
targets.device.namestring | null
targets.device.platformstring | null
targets.device.onlineboolean
targets.agentobject
targets.agent.refstring
targets.agent.namestring | null
targets.agent.runnerstring | null
targets.agent.productstring | null
targets.agent.status"ready" | "busy" | "paused" | null
targets.agent.presetsarray of AgentPreset
targets.agent.sessionsarray of object
targets.agent.acceptAgentAccept | null
targets.agent.policyobject | null
targets.bindingobject
targets.binding.agent_refstring
targets.binding.session?object | null
targets.binding.ask_first?boolean
targets.binding.direct_only?boolean
targets.binding.results?"send" | "keep"
targets.binding.default_preset?string | null
targets.availableboolean
targets.unavailable"device_gone" | "no_connector" | "agent_missing" | "agent_paused" | null
targets.public_keyAgentPublicKey | null
targets.public_key.kidstring
targets.public_key.jwkobject

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/agent-targets" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/agent-targets
GET/agent-tasksList agent tasks

box=out (the default): tasks the key's creator sent from this workspace, newest first; page with before (the last id you got). box=in: tasks for the creator's own computers in this workspace. box=pull: queued tasks for this workspace's pull agents, oldest first, plus the ones this key claimed and hasn't finished. limit 1–100 (50 by default). With q (box=out or in): the tasks whose agent, instruction, files or sender match, best first, 25 a page (page 1–8; see search). A sealed instruction is never searched.

Key scope: agent_tasks.read

Query parameters

q?stringSearch words (`box=out` or `in`).
page?stringWith `q`: the page of results, 1–8.
box?"out" | "in" | "device" | "pull"`out`, `in` or `pull` (`device` is for the SendFlo connector).
agent?stringFor `box=pull`: only this cloud agent's tasks.
state?stringFor `box=out`: only tasks in this stored state.
before?stringFor `box=out`: tasks older than this id.
limit?stringTasks per page, 1–100 (50 by default).
active?stringFor the SendFlo connector.

Response 200

tasksarray of AgentTask
tasks.idstring
tasks.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
tasks.labelstringThe state in plain words ("Done", "Needs your answer"…).
tasks.target_kind"device" | "cloud"
tasks.agentobject
tasks.agent.refstring
tasks.agent.namestring
tasks.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
tasks.deviceobject | null
tasks.device.idstring
tasks.device.namestring | null
tasks.project_idstring | null
tasks.cloud_agent_idstring | null
tasks.session_refstring | null
tasks.fromobject
tasks.from.namestring
tasks.from.user_idstring
tasks.from.device_idstring | null
tasks.instructionstring | null
tasks.instruction_encryptedboolean
tasks.presetstring | null
tasks.voice_filestring | null
tasks.filesarray of object
tasks.files.namestring
tasks.files.sizestring | integer
tasks.files.sha256?string
tasks.files.mime?string
tasks.total_bytesstringBytes, as a decimal string.
tasks.reasonstring | null
tasks.detailstring | null
tasks.questionAgentTaskQuestion | null
tasks.question.idstring
tasks.question.kind"question" | "permission" | "approval"
tasks.question.textstring
tasks.question.options?array of object
tasks.question.free_text?boolean
tasks.question.asked_atdate-time
tasks.question.answer?object
tasks.resultAgentTaskResult | null
tasks.result.summarystring | null
tasks.result.filesarray of object
tasks.result.kept_atstring | null
tasks.result.expires_atdate-time | null
tasks.result.data?anyFree-form JSON the agent returned.
tasks.policyobject | null
tasks.policy.ask_first?boolean
tasks.policy.direct_only?boolean
tasks.policy.results?"send" | "keep"
tasks.device_transfer_idstring | null
tasks.transfer_statestring | null
tasks.cancel_requestedboolean
tasks.retry_ofstring | null
tasks.role"sender" | "owner" | "both" | null
tasks.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
tasks.attemptsstring | integer
tasks.created_atdate-time
tasks.updated_atdate-time
tasks.delivered_atdate-time | null
tasks.started_atdate-time | null
tasks.finished_atdate-time | null
tasks.expires_atdate-time
search?SearchMetaPresent when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first.
search.engine"meilisearch" | "postgres"
search.pagestring | integerThis page (1–8).
search.totalstring | integerAbout how many match (the search engine's estimate).
search.next_pagestring | integer | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/agent-tasks" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/agent-tasks
POST/agent-tasksSend a task to a cloud agent

Hands 1–50 of this workspace's stored files to one of its cloud agents with an instruction, a preset or a voice note (at least one). Webhook, A2A and routine agents get it in the background; a pull agent's task waits (queued) until claimed. Refused for flagged or expired files, files over the agent's size limit, a paused agent, or an agent with 50 unfinished tasks. Sends the agent_task.created webhook.

Key scope: agent_tasks.write

Request body (JSON)

cloud_agent_idstringOne of this workspace's cloud agents.
file_idsarray of string1–50 stored file ids.
instruction?stringWhat the agent should do (8000 characters at most).
preset?stringA preset id of the agent.
voice_file?stringThe name of one of the files that holds a spoken instruction.
instruction_enc?anyNot for cloud agents (422 `e2e_not_for_cloud`).
from_name?stringThe sender name the agent sees (the key creator's name by default).

A name ending in ? is optional.

Response 200

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time

A name ending in ? is optional.

An error: no_user, agent_not_found, agent_paused, file_not_found, file_expired, file_flagged, invalid_task, instruction_required, e2e_not_for_cloud, agent_file_too_large, rate_limited, agent_queue_full, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/agent-tasks" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"cloud_agent_id":"…","file_ids":["…"]}'
GET/agent-tasks/{id}Get a task with its events

The task (with 1-hour download URLs for its results), its events (oldest first, 500 at most) and the tasks that re-ran it. Visible to its sender and to the owner of the computer it went to.

Key scope: agent_tasks.read

Path parameters

idstringThe task's id.

Response 200

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
eventsarray of AgentTaskEvent
events.idstring
events.atdate-time
events.kind"state" | "progress" | "log" | "question" | "answer" | "approval" | "result" | "error" | "delivery" | "control"
events.actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"
events.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | null
events.messagestring | null
events.dataanyFree-form JSON, or null.
retries?array of AgentTaskRetryRef
retries.idstring
retries.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
retries.created_atdate-time

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/agent-tasks/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/agent-tasks/{id}/answerAnswer a task's question

Answers the agent's open question, as the task's sender or the computer's owner: one of its option_ids, or free text when it allows it (stored up to 4000 characters). The answer is passed to the computer or cloud agent; reached says whether it got it now. For a task waiting for approval, approve or reject decides it (the computer's owner only).

Key scope: agent_tasks.write

Path parameters

idstringThe task's id.

Request body (JSON)

question_idstringThe open question's id.
option_id?string | nullOne of the question's options.
text?string | nullA free-text answer.

A name ending in ? is optional.

Response 200

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
reached?booleanWhether the agent or computer got it now (otherwise it picks it up on its next check).

A name ending in ? is optional.

An error: not_found, owner_decides, unknown_option, no_question, question_changed, already_answered, invalid_answer, option_required, text_required, terminal, not_awaiting_approval, illegal_transition, contended, rate_limited, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/agent-tasks/{id}/answer" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"question_id":"…"}'
POST/agent-tasks/{id}/cancelCancel a task

As the sender or the computer's owner. A cloud task, or one not yet handed to the agent, is canceled at once. A running task on a computer gets a stop request (cancel_requested) and reports canceled when it stops. A finished task comes back unchanged.

Key scope: agent_tasks.write

Path parameters

idstringThe task's id.

Response 200

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
reached?booleanWhether the agent or computer got it now (otherwise it picks it up on its next check).

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/agent-tasks/{id}/cancel" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/agent-tasks/{id}/claimClaim a pull agent's task

For a pull agent, with an API key: takes a queued task of this workspace (one claimer wins) and returns what a webhook agent would receive, with a per-task callback token (sfcb_…, shown once). Report progress with Authorization: Bearer <token> to the callback URLs. File URLs last 7 days; blocked files are left out.

Key scope: agent_tasks.write

Path parameters

idstringThe task's id (from GET /v1/agent-tasks?box=pull).

Response 200

type"agent_task"
api1
taskobject
task.idstring
task.instructionstring
task.presetstring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.mimestring | null
task.files.urlstringA download URL valid for 7 days.
task.fromobject
task.from.namestring
task.created_atdate-time
task.expires_atdate-time
callbackobject
callback.basestring
callback.tokenstring
callback.taskstring
callback.eventsstring
callback.resultsstring
callback.completestring
task_state"handed_off" | "queued"

A name ending in ? is optional.

An error: not_found, already_claimed, not_claimable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/agent-tasks/{id}/claim" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/agent-tasks/{id}/retryRun a task again

As its sender: a new task (retry_of = this one) from a finished task whose files were delivered. Cloud tasks reuse the same uploads; a computer's task re-checks the computer and its agent first.

Key scope: agent_tasks.write

Path parameters

idstringThe task's id.

Response 200

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time

A name ending in ? is optional.

An error: not_found, not_retryable, files_not_on_device, rate_limited, agent_unavailable, agent_paused, device_gone, agent_owner_only, agent_queue_full, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/agent-tasks/{id}/retry" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

cloud agents

The cloud agents (webhook, A2A, routine) your tasks can go to.

GET/cloud-agentsList cloud agents

This workspace's cloud agents, newest first (32 at most). Secrets are never returned (has_secret).

Key scope: agent_tasks.read

Response 200

agentsarray of CloudAgent
agents.idstring
agents.namestring
agents.kind"webhook" | "a2a" | "routine" | "pull"
agents.enabledboolean
agents.configobject
agents.config.urlstring | null
agents.config.endpointstring | null
agents.config.protocol_versionstring | null
agents.config.routine_idstring | null
agents.config.max_minutesstring | integer | null
agents.has_secretboolean
agents.presetsarray of AgentPreset
agents.presets.idstring
agents.presets.labelstring
agents.presets.instructionstring
agents.acceptAgentAccept | null
agents.accept.types?array of string
agents.accept.max_mb?number
agents.statusCloudAgentHealth | null
agents.status.okboolean
agents.status.atdate-time
agents.status.error?string
agents.status.http?string | integerWebhook agents: the HTTP status the test got.
agents.status.endpoint?stringA2A agents: the endpoint from the agent card.
agents.status.protocol_version?"1.0" | "0.3"
agents.status.name?string | null
agents.status.description?string | null
agents.status.push?boolean
agents.status.skills?array of object
agents.status.note?string
agents.created_atdate-time
agents.updated_atdate-time

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/cloud-agents" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/cloud-agents
POST/cloud-agentsAdd a cloud agent

A webhook agent (needs config.url; its signing secret is made here and shown once), an a2a agent (needs config.url; its agent card is fetched now), a routine (needs config.routine_id and secret), or a pull agent (nothing needed; it claims tasks with a key). URLs must be https and public. 32 per workspace.

Key scope: agent_tasks.write

Request body (JSON)

namestringThe agent's name.
kind"webhook" | "a2a" | "routine" | "pull"`webhook`, `a2a`, `routine` or `pull` (can't be changed later).
config?object | null`{ url?, routine_id?, max_minutes? }`: what the kind needs (see above).
config.url?stringWebhook agents: where tasks are posted. A2A agents: the agent card, site or endpoint. https and public.
config.routine_id?stringRoutine agents: the routine's id.
config.max_minutes?numberA2A agents: how long a task may run, 1–1440 minutes (120 by default).
secret?string | nullA2A: a bearer token sent to the agent. Routine: its trigger token. Webhook: ignored (SendFlo makes the signing secret).
enabled?booleanPause (false) or resume (true); a new agent is always on.
presets?array of objectUp to 12 ready-made instructions `{ id, label, instruction }` senders can pick.
presets.idstring
presets.labelstring
presets.instructionstring
accept?object | null`{ types?, max_mb? }`: tasks whose files total more than `max_mb` are refused.
accept.types?array of string
accept.max_mb?number

A name ending in ? is optional.

Response 200

agentCloudAgentA cloud agent (never its secret).
agent.idstring
agent.namestring
agent.kind"webhook" | "a2a" | "routine" | "pull"
agent.enabledboolean
agent.configobject
agent.config.urlstring | null
agent.config.endpointstring | null
agent.config.protocol_versionstring | null
agent.config.routine_idstring | null
agent.config.max_minutesstring | integer | null
agent.has_secretboolean
agent.presetsarray of AgentPreset
agent.presets.idstring
agent.presets.labelstring
agent.presets.instructionstring
agent.acceptAgentAccept | null
agent.accept.types?array of string
agent.accept.max_mb?number
agent.statusCloudAgentHealth | null
agent.status.okboolean
agent.status.atdate-time
agent.status.error?string
agent.status.http?string | integerWebhook agents: the HTTP status the test got.
agent.status.endpoint?stringA2A agents: the endpoint from the agent card.
agent.status.protocol_version?"1.0" | "0.3"
agent.status.name?string | null
agent.status.description?string | null
agent.status.push?boolean
agent.status.skills?array of object
agent.status.note?string
agent.created_atdate-time
agent.updated_atdate-time
secret?stringWebhook agents: the signing secret, shown once.

A name ending in ? is optional.

An error: invalid_agent, too_many_agents, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/cloud-agents" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…","kind":"webhook"}'
PATCH/cloud-agents/{id}Change a cloud agent

Changes the fields you send (kind can't change). config keys are merged over the stored ones; secret: null clears the secret; presets replaces the list. After changing an A2A agent's URL, run its test to refresh the endpoint.

Key scope: agent_tasks.write

Path parameters

idstringThe cloud agent's id.

Request body (JSON)

name?stringThe agent's name.
kind?"webhook" | "a2a" | "routine" | "pull"`webhook`, `a2a`, `routine` or `pull` (can't be changed later).
config?object | null`{ url?, routine_id?, max_minutes? }`: what the kind needs (see above).
config.url?stringWebhook agents: where tasks are posted. A2A agents: the agent card, site or endpoint. https and public.
config.routine_id?stringRoutine agents: the routine's id.
config.max_minutes?numberA2A agents: how long a task may run, 1–1440 minutes (120 by default).
secret?string | nullA2A: a bearer token sent to the agent. Routine: its trigger token. Webhook: ignored (SendFlo makes the signing secret).
enabled?booleanPause (false) or resume (true); a new agent is always on.
presets?array of objectUp to 12 ready-made instructions `{ id, label, instruction }` senders can pick.
presets.idstring
presets.labelstring
presets.instructionstring
accept?object | null`{ types?, max_mb? }`: tasks whose files total more than `max_mb` are refused.
accept.types?array of string
accept.max_mb?number

A name ending in ? is optional.

Response 200

agentCloudAgentA cloud agent (never its secret).
agent.idstring
agent.namestring
agent.kind"webhook" | "a2a" | "routine" | "pull"
agent.enabledboolean
agent.configobject
agent.config.urlstring | null
agent.config.endpointstring | null
agent.config.protocol_versionstring | null
agent.config.routine_idstring | null
agent.config.max_minutesstring | integer | null
agent.has_secretboolean
agent.presetsarray of AgentPreset
agent.presets.idstring
agent.presets.labelstring
agent.presets.instructionstring
agent.acceptAgentAccept | null
agent.accept.types?array of string
agent.accept.max_mb?number
agent.statusCloudAgentHealth | null
agent.status.okboolean
agent.status.atdate-time
agent.status.error?string
agent.status.http?string | integerWebhook agents: the HTTP status the test got.
agent.status.endpoint?stringA2A agents: the endpoint from the agent card.
agent.status.protocol_version?"1.0" | "0.3"
agent.status.name?string | null
agent.status.description?string | null
agent.status.push?boolean
agent.status.skills?array of object
agent.status.note?string
agent.created_atdate-time
agent.updated_atdate-time

A name ending in ? is optional.

An error: not_found, invalid_agent, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/cloud-agents/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/cloud-agents/{id}Delete a cloud agent

Deletes it. Its tasks not yet sent fail (agent_missing).

Key scope: agent_tasks.write

Path parameters

idstringThe cloud agent's id.

Response 200

oktrue

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/cloud-agents/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/cloud-agents/{id}/testHealth-check a cloud agent

A2A: reads the agent card again. Webhook: posts a signed { "type": "agent_task.ping" } (10-second timeout) and reports the HTTP status. Routine and pull: checks the configuration only. The result is stored as the agent's status.

Key scope: agent_tasks.write

Path parameters

idstringThe cloud agent's id.

Response 200

statusCloudAgentHealthThe agent's last health check.
status.okboolean
status.atdate-time
status.error?string
status.http?string | integerWebhook agents: the HTTP status the test got.
status.endpoint?stringA2A agents: the endpoint from the agent card.
status.protocol_version?"1.0" | "0.3"
status.name?string | null
status.description?string | null
status.push?boolean
status.skills?array of object
status.skills.idstring
status.skills.namestring
status.note?string

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/cloud-agents/{id}/test" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

account

Usage, credits and the requests made with your keys.

GET/creditsGet your credit balance and prices

The workspace's prepaid credit balance and the price list, as integer strings. The balance can go negative after metered use.

Key scope: usage.read

Response 200

balancestring
pricesobject
prices.egressPerGbstring
prices.storagePerGbMonthstring
prices.relayPerGbstring
prices.turnPerGbstring
prices.fileRequeststring
prices.webhookDeliverystring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/credits" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/credits
GET/usageGet this month's usage and plan limits

Usage for the current calendar month (UTC) and the plan's limits. Byte counts are decimal strings. Direct device-to-device transfers are free and not capped.

Key scope: usage.read

Response 200

period_startdate-time
period_enddate-time
direct_bytesstring
relay_bytesstring
turn_bytesstring
link_egress_bytesstring
storage_resident_bytesstring
storage_received_bytesstring
storage_own_bytesstring
staging_resident_bytesstring
storage_quota_bytesstring
egress_cap_bytesstring
egress_pooledboolean
egress_used_bytesstring
downloadsstring | integer
plan_class"free" | "starter" | "plus" | "pro" | "enterprise"
max_transfer_bytesstring
max_file_bytesstringThe same as max_transfer_bytes (kept for older clients).
max_claims_per_codenumber
max_files_per_transfernumber
max_downloads_per_filenumber
account_egress_multiplenumber
account_egress_cap_bytesstring
turn_projectsboolean
billing_cycle"monthly" | "annual" | null
direct_is_freetrue
previewobject
preview.imagesobject
preview.images.usedstring | integer
preview.images.limitstring | integer
preview.pdf_pagesobject
preview.pdf_pages.usedstring | integer
preview.pdf_pages.limitstring | integer
preview.video_bytesobject
preview.video_bytes.usedstring
preview.video_bytes.limitstring
preview.pooled?booleanA team seat: these are the team's pooled allowances and its usage, shared by every member.
marked_views?objectTeam Pro and Team Enterprise seats only: the team's pooled marked views this month (each viewer's email on their own copy of an image or PDF page). ok under 80%, warn under 100%, packs (packs pay), grace (a further 10%), paused (new viewers see "This preview is paused").
marked_views.state"ok" | "warn" | "packs" | "grace" | "paused"
marked_views.usedstring | integer
marked_views.includedstring | integer
marked_views.packs_leftstring | integer

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/usage" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/usage

audit

The audit log: who did what across your workspace or team, with filters and exports.

GET/auditRead the audit log

Who did what, newest first: sign-ins, links, every open, view and download, requests, collections, team changes, billing, the API, and SendFlo staff (shown as "SendFlo staff"). 50 events per page (up to 200 with limit); pass next_cursor back as cursor. Team Pro and Team Enterprise: a key reads its own workspace's log, or the whole team's with plan_id when the key's maker is the team's owner or an admin. Events are kept 365 days on Team Pro and 730 on Team Enterprise.

Key scope: audit.read

Query parameters

plan_id?stringA team's id: read the whole team's log (owners and admins). Without it: the key's own workspace.
since?date-timeOnly events at or after this time (ISO 8601).
until?date-timeOnly events before this time.
action?stringOne or more actions, comma-separated (`FILE_DOWNLOAD,FILE_PREVIEWED`) — the list is the catalogue in the docs.
category?stringOne or more of ACCOUNT, SECURITY, LINK, ACCESS, REQUEST, COLLECTION, TEAM, DEVICE, BILLING, API, STAFF.
actor?stringWho did it: an email address or a user id.
ip?stringOne IPv4 / IPv6 address, or a CIDR block (`203.0.113.0/24`).
link_id?stringOnly events on this link.
outcome?"success" | "failure" | "denied"`success`, `failure` or `denied`.
cursor?string`next_cursor` from the previous page.
limit?string | integerEvents per page, 1–200 (50 by default).

Response 200

dataarray of AuditEvent
data.event_idstringUUIDv7 — unique and time-ordered.
data.occurred_atdate-time
data.categorystring
data.actionstringe.g. FILE_DOWNLOAD — see the catalogue.
data.labelstringThe sentence a person reads after the actor's name: "downloaded a file".
data.outcome"success" | "failure" | "denied"
data.actorobject
data.actor.kind"user" | "recipient" | "guest" | "api_key" | "device" | "staff" | "system"
data.actor.user_idstring | null
data.actor.emailstring | null
data.actor.verifiedbooleanThe address was proved (a code by email, or the account's own).
data.actor.namestring | null
data.on_behalf_ofstring | null
data.ipstring | null
data.ip_source"cloudflare" | "claimed" | "direct" | "internal" | "none" | null
data.networkobject | null
data.network.asnstring | integer | null
data.network.orgstring | null
data.network.classstring | null
data.countrystring | null
data.targetobject
data.target.kindstring | null
data.target.idstring | null
data.link_idstring | null
data.file_idstring | null
data.file_namestring | null
data.owner_user_idstring | null
data.deviceobject
data.device.labelstring | null
data.device.idstring | null
data.user_agentstring | null
data.session_idstring | null
data.api_key_idstring | null
data.request_idstring | null
data.workspace_idstring
data.subscription_idstring | null
data.detailsmap of anyBytes, reasons, the fields an edit changed …
next_cursorstring | null
visibility"own" | "all" | "staff"

A name ending in ? is optional.

An error: invalid_query, plan_required, forbidden, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/audit" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/audit
GET/audit/exportDownload the audit log

The same events and filters as a file (sendflo-audit-<scope>-<date>.csv or .json), newest first, 100,000 at most. CSV by default (UTF-8 with a byte-order mark, formula-safe cells, details as JSON in its column); a JSON array of events with format=json. The export is itself recorded (AUDIT_EXPORTED).

Key scope: audit.read

Query parameters

plan_id?stringA team's id: read the whole team's log (owners and admins). Without it: the key's own workspace.
since?date-timeOnly events at or after this time (ISO 8601).
until?date-timeOnly events before this time.
action?stringOne or more actions, comma-separated (`FILE_DOWNLOAD,FILE_PREVIEWED`) — the list is the catalogue in the docs.
category?stringOne or more of ACCOUNT, SECURITY, LINK, ACCESS, REQUEST, COLLECTION, TEAM, DEVICE, BILLING, API, STAFF.
actor?stringWho did it: an email address or a user id.
ip?stringOne IPv4 / IPv6 address, or a CIDR block (`203.0.113.0/24`).
link_id?stringOnly events on this link.
outcome?"success" | "failure" | "denied"`success`, `failure` or `denied`.
format?"csv" | "json"`csv` (the default) or `json`.

Response 200 (text/csv): string

An error: invalid_query, plan_required, forbidden, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/audit/export" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/audit/export

team inboxes

Who uses a team's inboxes: keep access in sync from an HR or identity system.

GET/team-inboxes/{ws}/membersList a team inbox's members

Who is in a team inbox now (the people seated on its seats and groups, plus the team's owner and admins), and its access list: the seats and groups. The team's shared inbox has everyone.

Key scope: team_inboxes.write

Path parameters

wsstringThe inbox's id (GET /v1/plans/{id}/inboxes in the web app; the id in its address).

Response 200

idstring
workspace_idstring
subscription_idstring
namestring
restrictedboolean
seat_idsarray of string
group_idsarray of string
membersstring | integer
created_atdate-time
dataarray of TeamInboxMember
data.user_idstring
data.emailstring | null
data.namestring | null

A name ending in ? is optional.

An error: forbidden, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/team-inboxes/{ws}/members" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/team-inboxes/{ws}/membersReplace who uses a team inbox

Replace a restricted inbox's access list with these seats, groups and people (an email names the seat its holder sits in, so access follows the seat when it changes hands). A list left out is left as it is. Members join or leave at once; anyone who leaves loses the inbox's sessions, and the files assigned to them go back to Unassigned. The shared inbox can't be changed (everyone is in it): 409 shared_inbox.

Key scope: team_inboxes.write

Path parameters

wsstringThe inbox's id (GET /v1/plans/{id}/inboxes in the web app; the id in its address).

Request body (JSON)

seat_ids?array of stringSeats on the team.
group_ids?array of stringThe team's groups (Team settings → Groups): their seats as they are when someone opens the inbox.
emails?array of stringPeople seated on the team; one not seated is refused (422 `unknown_member`).
base?objectOptional: the access as your form showed it (`seat_ids`, `group_ids`). Then only who you added or removed since is applied over the access as it is now, so a seat or group someone else added meanwhile stays.
base.seat_ids?array of string
base.group_ids?array of string

A name ending in ? is optional.

Response 200

idstring
workspace_idstring
subscription_idstring
namestring
restrictedboolean
seat_idsarray of string
group_idsarray of string
membersstring | integer
created_atdate-time
addedstring | integer
removedstring | integer

A name ending in ? is optional.

An error: forbidden, not_found, shared_inbox, unknown_member, unknown_seat, unknown_group, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/team-inboxes/{ws}/members" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'

intakes

Secure intakes: a bot collects files it can't open, and hands them to people.

POST/intakesOpen a secure intake

A short-lived upload page for one person in one conversation, for a bot, an assistant or a helpdesk. It closes after one upload and expires in 24 hours by default (15 minutes to 7 days). With send_to SendFlo emails the person, and only they can upload; with for_email the page is bound to them and upload_url carries their one-time sign-in. client_token (single use, 15 minutes) opens the page in the widget's pop-up (SendFlo.requestUpload). Watch intake.ready (every file stored and scanned clean) or poll GET /v1/intakes/{id}. What an intake collects is content-blind to every API credential (API keys, apps' OAuth tokens, and so MCP and A2A): download, preview, links, send again and agent attachments answer 403 intake_content_blind, whatever the key's scopes, unless the intake was made with agent_access: "read" where the team allows it. An API & Agents workspace pays 1 credit, as for a file request.

Key scope: file_requests.write

Request body (JSON)

prompt?stringWhat to upload, shown on the page.
name?stringYour label for it (never shown to the uploader).
requested_by?stringWho is asking, as the page says it: "Requested by Acme support in your chat".
send_to?stringSendFlo emails this person the link; only they can upload.
for_email?stringBind the page to this person without emailing them.
expires_in_minutes?string | integer15 to 10,080 (7 days); default 1,440.
max_files?string | integerFiles per upload (1–50; default 10).
close_after_uploads?string | integerUploads before it closes (default 1).
allowed_types?array of stringAccepted file types: category ids or extensions.
max_bytes_per_file?string | integerA per-file size limit under the plan's.
questions?array of objectUp to 10 questions for the uploader (the upload page's question types).
questions.idstring
questions.labelstring
questions.typestring
questions.required?boolean
questions.options?array of string
locked_answers?map of stringAnswers you set, by question id: shown to the uploader, never editable.
agent_fields?objectWhat the bot sees beyond status, counts, types, sizes and the scan: `answers` (question ids) and `names` (file names; off by default).
agent_fields.answers?array of string
agent_fields.names?boolean
agent_access?"none" | "read"`none` (default) or `read`: whether API credentials may open what it collects, where the team allows it (403 `intake_read_not_allowed` otherwise).
external_refs?objectWhere it came from: {provider, kind, id, url?, label?}, carried on every item and event.
external_refs.providerstring
external_refs.kindstring
external_refs.idstring
external_refs.url?string
external_refs.label?string
handoff?objectWhen it's ready, hand the files to these people or groups of the team: {to: [{email | user_id | group_id, permission}], expires_days}.
handoff.toarray of object
handoff.to.email?string
handoff.to.user_id?string
handoff.to.group_id?string
handoff.to.permission?"view" | "manage"
handoff.expires_days?string | integer
brand_id?stringA brand kit for the page.
widget_origin?stringThe chat page's origin, for the widget's pop-up to answer to.

A name ending in ? is optional.

Response 201

idstring
status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
external_refsmap of any | null
countstring | integerHow many files arrived.
filesarray of object
files.item_idstring
files.typestring | null
files.sizestringBytes, as a decimal string.
files.scanstringpending, clean, flagged, released, not_scannable or rejected.
files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
uploader_verifiedbooleanThe uploader proved their email (never the address itself).
answersarray of objectOnly the answers the intake shares with the bot.
answers.idstring
answers.labelstring
answers.valuestring
handed_toarray of object
handed_to.idstring
handed_to.kind"user" | "group"
handed_to.grantee_idstring
handed_to.emailstring | null
handed_to.permission"view" | "manage"
handed_to.expires_atdate-time | null
expires_atdate-time | null
submitted_atdate-time | null
ready_atdate-time | null
urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).
upload_urlstringThe page the person uploads on.
emailedboolean
client_tokenstringFor SendFlo.requestUpload: single use, 15 minutes.
client_token_expires_atdate-time
agent_access"none" | "read"

A name ending in ? is optional.

An error: intake_read_not_allowed, unknown_question, handoff_needs_team, unknown_member, unknown_group, conflicting_person, insufficient_credits, plan_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/intakes" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/intakesList your secure intakes

The workspace's 50 newest intakes, metadata only (a key with file_requests.write may read too).

Key scope: inboxes.read

Response 200

dataarray of Intake
data.idstring
data.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
data.external_refsmap of any | null
data.countstring | integerHow many files arrived.
data.filesarray of object
data.files.item_idstring
data.files.typestring | null
data.files.sizestringBytes, as a decimal string.
data.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
data.files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
data.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
data.answersarray of objectOnly the answers the intake shares with the bot.
data.answers.idstring
data.answers.labelstring
data.answers.valuestring
data.handed_toarray of object
data.handed_to.idstring
data.handed_to.kind"user" | "group"
data.handed_to.grantee_idstring
data.handed_to.emailstring | null
data.handed_to.permission"view" | "manage"
data.handed_to.expires_atdate-time | null
data.expires_atdate-time | null
data.submitted_atdate-time | null
data.ready_atdate-time | null
data.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/intakes" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/intakes
GET/intakes/{id}Read a secure intake

Its status (open → opened → submitted → ready, or rejected / expired / closed), how many files, each one's type, size and scan, whether the uploader proved their email, the answers it shares, and who it was handed to. Never the content, never a link that serves bytes. What an intake collects is content-blind to every API credential (API keys, apps' OAuth tokens, and so MCP and A2A): download, preview, links, send again and agent attachments answer 403 intake_content_blind, whatever the key's scopes, unless the intake was made with agent_access: "read" where the team allows it.

Key scope: inboxes.read

Path parameters

idstringThe intake's id.

Response 200

idstring
status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
external_refsmap of any | null
countstring | integerHow many files arrived.
filesarray of object
files.item_idstring
files.typestring | null
files.sizestringBytes, as a decimal string.
files.scanstringpending, clean, flagged, released, not_scannable or rejected.
files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
uploader_verifiedbooleanThe uploader proved their email (never the address itself).
answersarray of objectOnly the answers the intake shares with the bot.
answers.idstring
answers.labelstring
answers.valuestring
handed_toarray of object
handed_to.idstring
handed_to.kind"user" | "group"
handed_to.grantee_idstring
handed_to.emailstring | null
handed_to.permission"view" | "manage"
handed_to.expires_atdate-time | null
expires_atdate-time | null
submitted_atdate-time | null
ready_atdate-time | null
urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/intakes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/intakes/{id}Close a secure intake

No more uploads; what it collected stays, with its hand-offs.

Key scope: file_requests.write

Path parameters

idstringThe intake's id.

Response 200

idstring
status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
external_refsmap of any | null
countstring | integerHow many files arrived.
filesarray of object
files.item_idstring
files.typestring | null
files.sizestringBytes, as a decimal string.
files.scanstringpending, clean, flagged, released, not_scannable or rejected.
files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
uploader_verifiedbooleanThe uploader proved their email (never the address itself).
answersarray of objectOnly the answers the intake shares with the bot.
answers.idstring
answers.labelstring
answers.valuestring
handed_toarray of object
handed_to.idstring
handed_to.kind"user" | "group"
handed_to.grantee_idstring
handed_to.emailstring | null
handed_to.permission"view" | "manage"
handed_to.expires_atdate-time | null
expires_atdate-time | null
submitted_atdate-time | null
ready_atdate-time | null
urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/intakes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/intakes/{id}/handoffHand an intake's files to someone

Give a team member (by email or id) or a group of the team view or manage access to every file the intake collected, and to any it collects later, with an optional end. They open them in SendFlo → Received → Handed to me; every open is in the workspace's audit log. A grant never gives the bot anything.

Key scope: file_requests.write

Path parameters

idstringThe intake's id.

Request body (JSON)

toobject{email | user_id | group_id, permission: view | manage}.
to.email?string
to.user_id?string
to.group_id?string
to.permission?"view" | "manage"
expires_at?stringWhen the access ends (ISO time).

A name ending in ? is optional.

Response 200

idstring
status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
external_refsmap of any | null
countstring | integerHow many files arrived.
filesarray of object
files.item_idstring
files.typestring | null
files.sizestringBytes, as a decimal string.
files.scanstringpending, clean, flagged, released, not_scannable or rejected.
files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
uploader_verifiedbooleanThe uploader proved their email (never the address itself).
answersarray of objectOnly the answers the intake shares with the bot.
answers.idstring
answers.labelstring
answers.valuestring
handed_toarray of object
handed_to.idstring
handed_to.kind"user" | "group"
handed_to.grantee_idstring
handed_to.emailstring | null
handed_to.permission"view" | "manage"
handed_to.expires_atdate-time | null
expires_atdate-time | null
submitted_atdate-time | null
ready_atdate-time | null
urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

An error: not_found, handoff_needs_team, unknown_member, unknown_group, invalid_expiry, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/intakes/{id}/handoff" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"to":{}}'
DELETE/intakes/{id}/handoffTake a hand-off back

End a person's or group's access to the intake's files at once.

Key scope: file_requests.write

Path parameters

idstringThe intake's id.

Request body (JSON)

kind"user" | "group"user or group.
idstringThe person's or group's id (as in `handed_to`).

A name ending in ? is optional.

Response 200

idstring
status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
external_refsmap of any | null
countstring | integerHow many files arrived.
filesarray of object
files.item_idstring
files.typestring | null
files.sizestringBytes, as a decimal string.
files.scanstringpending, clean, flagged, released, not_scannable or rejected.
files.namestring"Collected file.<ext>" unless the intake shares names (agent_fields.names).
uploader_verifiedbooleanThe uploader proved their email (never the address itself).
answersarray of objectOnly the answers the intake shares with the bot.
answers.idstring
answers.labelstring
answers.valuestring
handed_toarray of object
handed_to.idstring
handed_to.kind"user" | "group"
handed_to.grantee_idstring
handed_to.emailstring | null
handed_to.permission"view" | "manage"
handed_to.expires_atdate-time | null
expires_atdate-time | null
submitted_atdate-time | null
ready_atdate-time | null
urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/intakes/{id}/handoff" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"kind":"user","id":"…"}'

drive

Drive — My files: folders, uploads, versions, the trash, and the change feed a sync client reads.

GET/driveGet your Drive

Your My files space (its top folder, the room used) and your Drive plan. A key acts as the person who made it. no_drive when they have no Drive plan. An app the person allowed only some folders (DA3) gets those folders instead (space and plan null) — it starts from them; every other folder answers drive_folder_grant.

Key scope: drive.read

Response 200

spaceDriveSpace | null
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean
planDrivePlan | null
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.
folders?array of DriveNodeAn app allowed only some folders: those folders.
folders.idstring
folders.parent_idstring | null
folders.kind"folder" | "file"
folders.namestring
folders.sizestringThe current version's size (0 for a folder).
folders.mimestring | null
folders.content_hashstring | null
folders.versionsstring | integerVersions kept (the current one included).
folders.modified_atdate-timeWhen the content last changed.
folders.updated_atdate-timeWhen anything last changed (a rename, a move…).
folders.created_atdate-time
folders.trashed_atdate-time | null
folders.purge_afterdate-time | null
folders.starred?boolean
folders.seqstringThe change that last touched it (see GET /v1/drive/changes).
folders.thumb_url?string | null
folders.inside?string | integerTrash: how many items a trashed folder holds.
folders.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
folders.version_idstring | null
folders.lock?objectLocked for editing: others' saves are refused until then (P6).
folders.lock.user_idstring
folders.lock.namestring
folders.lock.untildate-time
folders.lock.mineboolean
folders.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
folders.conflict.idstring
folders.conflict.file_idstring

A name ending in ? is optional.

An error: no_drive, drive_folder_grant, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive
PATCH/drive/automations/{id}Change, pause or resume an automation

Any of its fields, or enabled.

Key scope: drive.write

Path parameters

idstringThe automation's id.

Request body (JSON)

name?stringIts name.
trigger?stringIts trigger.
subfolders?booleanFiles in subfolders too.
conditions?map of anyIts conditions.
actions?array of map of anyIts actions.
enabled?booleanfalse pauses it.

A name ending in ? is optional.

Response 200

automationDriveAutomation
automation.idstring
automation.folder_idstring
automation.namestring
automation.trigger"added" | "changed" | "labeled"
automation.subfoldersboolean
automation.conditionsmap of any
automation.actionsarray of map of any
automation.enabledboolean
automation.created_bystring
automation.runsstring | integer
automation.last_run_atdate-time | null
automation.created_atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_automation, automations_role, bad_rule, bad_folder, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/automations/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/automations/{id}Remove an automation

It stops; its run log goes with it.

Key scope: drive.write

Path parameters

idstringThe automation's id.

Response 200

removedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_automation, automations_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/automations/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/automations/{id}/runsWhat an automation did

Its last 50 runs, newest first: done (with each step), failed (why), skipped (which condition didn't hold) or loop_stopped. Kept 30 days.

Key scope: drive.read

Path parameters

idstringThe automation's id.

Response 200

automationDriveAutomation
automation.idstring
automation.folder_idstring
automation.namestring
automation.trigger"added" | "changed" | "labeled"
automation.subfoldersboolean
automation.conditionsmap of any
automation.actionsarray of map of any
automation.enabledboolean
automation.created_bystring
automation.runsstring | integer
automation.last_run_atdate-time | null
automation.created_atdate-time
runsarray of object
runs.idstring
runs.node_idstring
runs.namestring
runs.triggerstring
runs.status"done" | "failed" | "skipped" | "loop_stopped"
runs.stepsarray of string
runs.errorstring | null
runs.atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_automation, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/automations/{id}/runs" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/changesList changes since a point

Every change to My files after since (a seq), in order — what a sync client reads to stay current. cursor is the last change returned; more says there are more. Changes are numbered without gaps.

Key scope: drive.read

Query parameters

since?stringThe last `seq` you have (0: from the start).
limit?string1–5,000 (1,000 by default).

Response 200

space_idstring
cursorstring
moreboolean
changesarray of object
changes.seqstring
changes.op"create" | "update" | "rename" | "move" | "trash" | "restore" | "delete"
changes.node_idstring
changes.parent_idstring | null
changes.kind"folder" | "file"
changes.namestring | null
changes.version_idstring | null
changes.atdate-time
changes.metaany

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/changes" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/changes
GET/drive/clientsList your computers syncing Drive

Key scope: drive.read

Response 200

clientsarray of DriveSyncClient
clients.idstring
clients.namestring
clients.osstring | null
clients.client_versionstring | null
clients.root_kind"cfapi" | "fileprovider" | "fuse" | "folder"
clients.state"ok" | "syncing" | "paused" | "problems" | "offline"
clients.problemsstring | integer
clients.problem_reasonsmap of string | integer
clients.pendingstring | integer
clients.cache_bytesstring | null
clients.cache_budgetstring | null
clients.hiddenstring | integer
clients.spacesarray of object
clients.spaces.space_idstring
clients.spaces.cursorstring
clients.managedobject | null
clients.managed.keys?array of string
clients.managed.team?string | null
clients.peer_in_bytes?string
clients.peer_out_bytes?string
clients.last_seen_atdate-time | null
clients.created_atdate-time

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/clients" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/clients
POST/drive/clientsRegister a computer's sync client

Once per install: its name (shown in conflict copies and Settings), OS, client version and kind of drive. No local paths are sent.

Key scope: drive.write

Request body (JSON)

namestringThe computer's name.
os?string | nulle.g. `windows 10.0.19045`.
client_version?string | nullThe client's version.
root_kindstring`cfapi` (Windows), `fileprovider` (macOS), `fuse` (Linux) or `folder` (a synced folder).

A name ending in ? is optional.

Response 200

clientDriveSyncClient
client.idstring
client.namestring
client.osstring | null
client.client_versionstring | null
client.root_kind"cfapi" | "fileprovider" | "fuse" | "folder"
client.state"ok" | "syncing" | "paused" | "problems" | "offline"
client.problemsstring | integer
client.problem_reasonsmap of string | integer
client.pendingstring | integer
client.cache_bytesstring | null
client.cache_budgetstring | null
client.hiddenstring | integer
client.spacesarray of object
client.spaces.space_idstring
client.spaces.cursorstring
client.managedobject | null
client.managed.keys?array of string
client.managed.team?string | null
client.peer_in_bytes?string
client.peer_out_bytes?string
client.last_seen_atdate-time | null
client.created_atdate-time

A name ending in ? is optional.

An error: bad_root_kind, bad_name, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/clients" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…","root_kind":"…"}'
DELETE/drive/clients/{id}Remove a computer's sync client

Key scope: drive.write

Path parameters

idstringThe client's id.

Response 200

removedtrue

A name ending in ? is optional.

An error: no_client, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/clients/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/clients/{id}/heartbeatReport a sync client's state

Every few minutes and on a change of state: the client's state, its problems by reason, items waiting, its cache, folders hidden on this computer, and how far each space's feed was applied.

Key scope: drive.write

Path parameters

idstringThe client's id.

Request body (JSON)

state?string`ok`, `syncing`, `paused`, `problems` or `offline`.
problems?string | integerItems that failed.
problem_reasons?map of string | integer | null{reason: count}.
pending?string | integerItems waiting.
cache_bytes?string | string | integer | nullBytes kept locally.
cache_budget?string | string | integer | nullThe cache's limit.
hidden?string | integerFolders hidden on this computer.
spaces?array of object | null[{space_id, cursor}].
spaces.space_idstring
spaces.cursorstring
client_version?string | nullIf it changed.
managed?object | null{keys, team}: the settings an organisation's device management set on this computer (unknown keys dropped); null clears them, absent keeps them.
managed.keys?array of string
managed.team?string | null
peer?object | null{addr, port, token}: where this computer serves Drive files to nearby computers (a private address; the token is random per run and never shown to anyone). null: it stopped.
peer.addrstring
peer.portstring | integer
peer.tokenstring
peer_in?string | integerBytes fetched from nearby computers since the last heartbeat.
peer_out?string | integerBytes served to them since the last heartbeat.
shared?string | nullWhy several people use this computer at once (Windows Server, a multi-session edition): nearby devices and the hotspot are off there. null clears it, absent keeps it.

A name ending in ? is optional.

Response 200

clientDriveSyncClient
client.idstring
client.namestring
client.osstring | null
client.client_versionstring | null
client.root_kind"cfapi" | "fileprovider" | "fuse" | "folder"
client.state"ok" | "syncing" | "paused" | "problems" | "offline"
client.problemsstring | integer
client.problem_reasonsmap of string | integer
client.pendingstring | integer
client.cache_bytesstring | null
client.cache_budgetstring | null
client.hiddenstring | integer
client.spacesarray of object
client.spaces.space_idstring
client.spaces.cursorstring
client.managedobject | null
client.managed.keys?array of string
client.managed.team?string | null
client.peer_in_bytes?string
client.peer_out_bytes?string
client.last_seen_atdate-time | null
client.created_atdate-time

A name ending in ? is optional.

An error: no_client, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/clients/{id}/heartbeat" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
PATCH/drive/comments/{id}Edit or resolve a comment

body edits your own comment; resolved resolves or reopens a thread (its author or an editor).

Key scope: drive.write

Path parameters

idstringThe comment's id.

Request body (JSON)

body?stringThe new text.
resolved?booleantrue resolves, false reopens.

A name ending in ? is optional.

Response 200

updatedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_comment, not_yours, bad_body, deleted, reply, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/comments/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/comments/{id}Delete a comment

Your own (a co-owner any). A comment with replies stays as "Comment deleted".

Key scope: drive.write

Path parameters

idstringThe comment's id.

Response 200

deletedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_yours, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/comments/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/comments/{id}/taskTurn a comment into a task

For someone who can open the file, with an optional due date; they're notified. Editors. Again on the same comment reassigns it.

Key scope: drive.write

Path parameters

idstringThe comment's id.

Request body (JSON)

assignee_idstringTheir user id.
due_at?date-time | nullWhen it's due.

A name ending in ? is optional.

Response 200

taskobject
task.idstring
task.status"open" | "done"
task.due_atdate-time | null

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_comment, assignee_no_access, deleted, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/comments/{id}/task" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"assignee_id":"…"}'
GET/drive/conflictsList your open sync conflicts

Saves that arrived after the file had changed: each became a copy beside the file. Yours, in every space you sync (a team's admins see their spaces' all).

Key scope: drive.read

Response 200

conflictsarray of DriveConflict
conflicts.idstring
conflicts.space_idstring
conflicts.space_namestring
conflicts.fileany | null
conflicts.copyany | null
conflicts.patharray of object
conflicts.path.idstring
conflicts.path.namestring
conflicts.byobject
conflicts.by.idstring
conflicts.by.namestring
conflicts.client_namestring | null
conflicts.state"open" | "resolved"
conflicts.resolution"kept_file" | "kept_copy" | "kept_both" | null
conflicts.created_atdate-time
conflicts.resolved_atdate-time | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/conflicts" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/conflicts
POST/drive/conflicts/{id}/resolveResolve a sync conflict

file: keep the file (the copy goes to the trash); copy: the copy's content becomes the file's newest version (the copy goes to the trash); both: keep both as they are. Nothing is deleted for good. Someone else's conflict notifies them.

Key scope: drive.write

Path parameters

idstringThe conflict's id.

Request body (JSON)

keepstring`file`, `copy` or `both`.

A name ending in ? is optional.

Response 200

conflictDriveConflict
conflict.idstring
conflict.space_idstring
conflict.space_namestring
conflict.fileany | null
conflict.copyany | null
conflict.patharray of object
conflict.path.idstring
conflict.path.namestring
conflict.byobject
conflict.by.idstring
conflict.by.namestring
conflict.client_namestring | null
conflict.state"open" | "resolved"
conflict.resolution"kept_file" | "kept_copy" | "kept_both" | null
conflict.created_atdate-time
conflict.resolved_atdate-time | null

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_conflict, resolved, gone, bad_keep, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/conflicts/{id}/resolve" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"keep":"…"}'
POST/drive/foldersMake a folder

In parent_id (the top folder when absent). A name already there (compared case-insensitively) is refused with name_taken, or numbered — "Reports (2)" — with on_conflict: rename.

Key scope: drive.write

Request body (JSON)

parent_id?stringThe folder to make it in.
namestringIts name (no `/`, 255 bytes at most). In a private space, the name sealed on your device.
name_key?stringIn a private space (end-to-end): the name's index (64 hex) your device computed — required there, ignored elsewhere.
on_conflict?string`fail` (the default) or `rename`.

A name ending in ? is optional.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring

A name ending in ? is optional.

An error: no_drive, not_found, read_only, no_folder, name_taken, bad_name, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/folders" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…"}'
GET/drive/folders/{id}List a folder

A folder, its path from the top, and what is in it: folders first, then by sort (name, modified, size) and dir. root is the top folder.

Key scope: drive.read

Path parameters

idstringThe folder's id, or `root`.

Query parameters

sort?"name" | "modified" | "size"`name` (the default), `modified` or `size`.
dir?"asc" | "desc"`asc` (the default) or `desc`.

Response 200

folderDriveNode
folder.idstring
folder.parent_idstring | null
folder.kind"folder" | "file"
folder.namestring
folder.sizestringThe current version's size (0 for a folder).
folder.mimestring | null
folder.content_hashstring | null
folder.versionsstring | integerVersions kept (the current one included).
folder.modified_atdate-timeWhen the content last changed.
folder.updated_atdate-timeWhen anything last changed (a rename, a move…).
folder.created_atdate-time
folder.trashed_atdate-time | null
folder.purge_afterdate-time | null
folder.starred?boolean
folder.seqstringThe change that last touched it (see GET /v1/drive/changes).
folder.thumb_url?string | null
folder.inside?string | integerTrash: how many items a trashed folder holds.
folder.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
folder.version_idstring | null
folder.lock?objectLocked for editing: others' saves are refused until then (P6).
folder.lock.user_idstring
folder.lock.namestring
folder.lock.untildate-time
folder.lock.mineboolean
folder.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
folder.conflict.idstring
folder.conflict.file_idstring
patharray of objectFrom the top folder down to this one.
path.idstring
path.namestring
itemsarray of DriveNode
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean
planDrivePlan | null
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.
role"owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer"
sharedobject | null
shared.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
shared.top_idstring | null
shared.ownerDriveOwner | null
shared.owner.idstring
shared.owner.namestring
shared.owner.avatar_urlstring | null
accessobject | null
access.sharesstring | integerShares that reach it (its own and from folders above).
access.teamboolean
teamobject | null
team.idstring
team.namestring
team.pool_usedstringWhat every space of the team holds.
team.pool_bytesstringThe team's pool.
team.managesboolean

A name ending in ? is optional.

An error: no_drive, not_found, no_folder, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/folders/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PATCH/drive/links/{id}Change a link

Its end (expires_at; a public link keeps one), download_limit, delivery or password (null removes it).

Key scope: drive.write

Path parameters

idstringThe link's id.

Request body (JSON)

expires_at?date-time | nullWhen it ends (null: never, not for a public link).
download_limit?string | integer | null1–100,000, or null.
delivery?stringpreview_download, download_only or preview_only.
password?string | null4–128 characters, or null to remove it.

A name ending in ? is optional.

Response 200

linkDriveLink | null
link.idstring
link.node_idstringThe file or folder it opens.
link.slugstring
link.urlstringThe page anyone it lets in opens: https://sendflo.app/d/<slug>.
link.mode"latest" | "version"
link.version_idstring | null
link.audience"public" | "verified" | "restricted"
link.emailsarray of stringrestricted: the addresses that may open it (each confirms with a code).
link.passwordboolean
link.delivery"preview_download" | "download_only" | "preview_only"
link.expires_atdate-time | null
link.download_limitstring | integer | null
link.downloadsstring | integer
link.share_idstring | null
link.last_opened_atdate-time | null
link.created_atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_link, bad_expiry, bad_delivery, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/links/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/links/{id}Turn a link off

Its page then says the owner turned it off.

Key scope: drive.write

Path parameters

idstringThe link's id.

Response 200

revokedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/links/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}Get a file or folder

Key scope: drive.read

Path parameters

idstringThe file's or folder's id.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring
patharray of objectFrom the top folder down to this one.
path.idstring
path.namestring
role"owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer"
sharedobject | null
shared.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
shared.top_idstring | null
shared.ownerDriveOwner | null
shared.owner.idstring
shared.owner.namestring
shared.owner.avatar_urlstring | null
accessobject | null
access.sharesstring | integerShares that reach it (its own and from folders above).
access.teamboolean

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PATCH/drive/nodes/{id}Rename or move a file or folder

name renames; parent_id moves it (with everything in it) to another folder — a name already there is refused, or numbered with on_conflict: rename. A folder can't go inside itself.

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Request body (JSON)

name?stringThe new name (in a private space, sealed on your device).
name_key?stringIn a private space: the new name's index (64 hex), with the sealed `name`.
parent_id?stringThe folder to move it to.
on_conflict?stringWhen moving: `fail` (the default) or `rename`.

A name ending in ? is optional.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring

A name ending in ? is optional.

An error: no_drive, not_found, read_only, name_taken, bad_name, into_itself, root, bad_parent, agent_bulk_limit, drive_folder_grant, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/nodes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/nodes/{id}Move a file or folder to the trash

With everything in it. Kept the plan's version days, then deleted for good; restore it with POST …/restore. An app that signed in with OAuth needs drive.delete (an API key's drive.write covers it); an app or agent moves at most 25 files at once (agent_bulk_limit).

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Response 200

trashedtrue
idstring
countstring | integerItems moved (the folder and everything in it).
bytesstring
purge_afterdate-time

A name ending in ? is optional.

An error: no_drive, not_found, read_only, root, agent_bulk_limit, drive_folder_grant, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/nodes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}/automationsA folder's automations

Its rules (trigger, conditions, actions), and whether you may change them: edit access to the folder, and in a team space its policy (Policies › Drive: owner and admins, the default, or everyone who can edit the folder).

Key scope: drive.read

Path parameters

idstringThe folder's id.

Response 200

automationsarray of DriveAutomation
automations.idstring
automations.folder_idstring
automations.namestring
automations.trigger"added" | "changed" | "labeled"
automations.subfoldersboolean
automations.conditionsmap of any
automations.actionsarray of map of any
automations.enabledboolean
automations.created_bystring
automations.runsstring | integer
automations.last_run_atdate-time | null
automations.created_atdate-time
can_manageboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_folder, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/automations" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/automationsAdd an automation to a folder

A rule that runs on its own, as you: when a file is added to the folder, changed (a new version) or labeled; if every condition holds (extensions, name_contains, min_bytes, max_bytes, label_ids, people); do each action in order — move / copy {folder_id} (a folder in the same space), label {label_ids} (team spaces), notify {user_ids, message?}, review {user_ids, message?, due_days?} (a comment with a task for each reviewer). People who can't see the file aren't told about it. subfolders (default true): files in its subfolders too. A rule never fires on its own change; a chain of automations stops at its third step. 25 rules a space.

Key scope: drive.write

Path parameters

idstringThe folder's id.

Request body (JSON)

name?stringThe rule's name.
trigger?string`added`, `changed` or `labeled` (which needs conditions.label_ids).
subfolders?booleanFiles in subfolders too (default true).
conditions?map of anyAll must hold.
actions?array of map of any1 to 6, done in order.
enabled?booleanfalse: made paused.

A name ending in ? is optional.

Response 200

automationDriveAutomation
automation.idstring
automation.folder_idstring
automation.namestring
automation.trigger"added" | "changed" | "labeled"
automation.subfoldersboolean
automation.conditionsmap of any
automation.actionsarray of map of any
automation.enabledboolean
automation.created_bystring
automation.runsstring | integer
automation.last_run_atdate-time | null
automation.created_atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_folder, automations_role, bad_rule, bad_folder, too_many_rules, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/automations" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/drive/nodes/{id}/commentsList a file's comments

Its threads, oldest first: each comment with one level of replies, its author, an anchor (time, page, region of the version it was made on), whether it's resolved, and its task. can_task: may you turn comments into tasks (editors).

Key scope: drive.read

Path parameters

idstringThe file's or folder's id.

Response 200

commentsarray of any
can_taskboolean

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/comments" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/commentsComment on a file

Anyone the file is shared with may comment (previewers too). parent_id replies to a thread; anchor points at a time ({kind: "time", t} seconds), a page ({kind: "page", page}) or a region ({kind: "region", x, y, w, h} fractions). mentions: user ids of people who can open the file — they're notified.

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Request body (JSON)

bodystring1–2,000 characters.
parent_id?string | nullReply to this comment.
anchor?any | nullWhere it points.
mentions?array of stringUp to 20 user ids.

A name ending in ? is optional.

Response 200

commentobject
comment.idstring

A name ending in ? is optional.

An error: no_drive, not_found, bad_body, no_comment, nested_reply, mention_no_access, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/comments" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"body":"…"}'
POST/drive/nodes/{id}/copyCopy a file or folder

Into parent_id (its own folder when absent), as "name (2)" when the name is taken. Instant — the copies share the stored content — and they count against the room like any file. 10,000 items at most per copy.

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Request body (JSON)

parent_id?stringThe folder to copy it into.

A name ending in ? is optional.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring

A name ending in ? is optional.

An error: no_drive, not_found, read_only, into_itself, too_many, drive_full, agent_bulk_limit, drive_folder_grant, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/copy" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/drive/nodes/{id}/downloadDownload a file

A short-lived URL (an hour) for the current version, or an earlier one with version; inline=1 for viewing in a browser. Works on a plan that ended (until the files are deleted). Empty files answer a data: URL.

Key scope: drive.read

Path parameters

idstringThe file's id.

Query parameters

version?stringAn earlier version's id (GET …/versions).
inline?"1"`1`: for viewing, not saving.

Response 200

urlstring
namestring
sizestring
mimestring | null
version_idstring

A name ending in ? is optional.

An error: no_drive, not_found, no_version, gone, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/download" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/lockLock a file for editing

Others' saves are refused (locked) until it's unlocked or minutes pass (60 by default, up to a day). Locking it again extends your lock; someone else's lock answers locked.

Key scope: drive.write

Path parameters

idstringThe file's id.

Request body (JSON)

minutes?string | integer1–1,440.
client_id?string | nullYour sync client, if a computer locked it.

A name ending in ? is optional.

Response 200

lockobject
lock.user_idstring
lock.untildate-time
lock.minetrue

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, locked, not_a_file, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/lock" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/nodes/{id}/lockUnlock a file

Whoever locked it, or the space's owner (a team's admins).

Key scope: drive.write

Path parameters

idstringThe file's id.

Response 200

unlockedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/nodes/{id}/lock" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}/partsA version's part hashes

The BLAKE3 hash of each part of a file's version (its current one unless version_id), at the part size it was stored with: compare a new version's parts with these and upload only the changed ones (parts on POST /v1/drive/uploads). Known once SendFlo has checked the stored bytes, for files of two parts or more; parts_not_ready until then — send the whole file.

Key scope: drive.read

Path parameters

idstringThe file's id.

Query parameters

version_id?stringA version of the file (the current one by default).

Response 200

version_idstring
sizestring
part_sizestring | integer
hashesarray of string

A name ending in ? is optional.

An error: no_drive, not_found, no_version, parts_not_ready, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/parts" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/peersNearby computers that hold a file

For a sync client about to download a file: the computers on its network (the same public address) whose person may open it, each with where it listens and a key only that computer and this one can make. GET http://<addr>:<port>/dp/<hash>?a=<client_id>&e=<exp>&p=<HMAC-SHA256(key, "req")> answers the file in AES-256-GCM frames under the key; check the whole file's BLAKE3 against hash before using it. Empty when none is nearby, the version isn't checked yet, or the team turned it off (off).

Key scope: drive.read

Path parameters

idstringThe file's id.

Request body (JSON)

client_idstringThis computer's sync-client id (POST /v1/drive/clients): the key binds it.
version_id?string | nullThe version wanted (the current one by default).

A name ending in ? is optional.

Response 200

hash?stringThe version's BLAKE3.
size?string
version_id?string
off?true
peersarray of object
peers.client_idstring
peers.addrstring
peers.portstring | integer
peers.expstring | integerUnix seconds.
peers.keystringHex, 32 bytes.

A name ending in ? is optional.

An error: no_drive, not_found, no_client, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/peers" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"client_id":"…"}'
GET/drive/nodes/{id}/peopleList who can open a file

Its owner and everyone its shares reach (people, and the members of the groups and teams they name — 200 at most): whom you can @mention or give a task.

Key scope: drive.read

Path parameters

idstringThe file's or folder's id.

Response 200

peoplearray of object
people.idstring
people.namestring
people.emailstring
people.avatar_urlstring | null
people.meboolean

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/people" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/restoreRestore from the trash

To its folder — or the top folder if that is gone — with everything that was in it; numbered when the name is taken.

Key scope: drive.write

Path parameters

idstringThe trashed item's id (as listed by GET /v1/drive/trash).

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring
patharray of objectFrom the top folder down to this one.
path.idstring
path.namestring

A name ending in ? is optional.

An error: no_drive, not_found, read_only, not_in_trash, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/restore" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}/sharesList who has access

The owner and every share that reaches a file or folder: its own, and those on folders above (inherited_from). Someone it's shared with sees the shares from their shared folder down. roles lists the roles with their one-line descriptions.

Key scope: drive.read

Path parameters

idstringThe file's or folder's id.

Response 200

ownerDriveOwner | null
owner.idstring
owner.namestring
owner.avatar_urlstring | null
role"owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer"
can_shareboolean
sharesarray of DriveShare
shares.idstring
shares.node_idstringThe file or folder the share is on (a folder above, when inherited).
shares.kind"user" | "email" | "group" | "team"
shares.user_idstring | null
shares.emailstring | null
shares.group_idstring | null
shares.team_idstring | null
shares.namestringThe person, the address, the group or "Everyone at <team>".
shares.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
shares.expires_atdate-time | null
shares.acceptedbooleanFalse for an address that hasn't opened it signed in yet.
shares.created_atdate-time
shares.inherited_fromobject | null
shares.inherited_from.idstring
shares.inherited_from.namestring
rolesarray of object
roles.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
roles.labelstring
roles.textstring

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/shares" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/sharesShare a file or folder

Gives each of people the role (an existing share is changed to it): previewer sees and previews, no download; viewer also downloads; uploader also adds files and folders; editor also changes, renames, moves and deletes; co_owner also shares. A share on a folder reaches everything in it. A person is {kind: "email", email} (an address with no account gets an email and the share becomes theirs when they sign in with it) or {kind: "user", id}; a team group {kind: "group", id} and the whole team {kind: "team", id} must be your team's. The owner and co-owners share; the top of My files can't be shared.

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Request body (JSON)

peoplearray of object1–50 people, groups or teams.
people.kindstring
people.id?string
people.email?string
rolestringpreviewer, viewer, uploader, editor or co_owner.
expires_at?date-time | nullWhen it ends (optional; in the future).
message?string | nullA note for them (500 characters).

A name ending in ? is optional.

Response 200

sharesarray of DriveShare
shares.idstring
shares.node_idstringThe file or folder the share is on (a folder above, when inherited).
shares.kind"user" | "email" | "group" | "team"
shares.user_idstring | null
shares.emailstring | null
shares.group_idstring | null
shares.team_idstring | null
shares.namestringThe person, the address, the group or "Everyone at <team>".
shares.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
shares.expires_atdate-time | null
shares.acceptedbooleanFalse for an address that hasn't opened it signed in yet.
shares.created_atdate-time
shares.inherited_fromobject | null
shares.inherited_from.idstring
shares.inherited_from.namestring

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, read_only, share_root, bad_role, bad_email, bad_person, bad_group, bad_team, bad_expiry, bad_people, self, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/shares" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"people":[{"kind":"…"}],"role":"…"}'
PUT/drive/nodes/{id}/starStar a file or folder

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Response 200

starredtrue

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/drive/nodes/{id}/star" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/drive/nodes/{id}/starUnstar a file or folder

Key scope: drive.write

Path parameters

idstringThe file's or folder's id.

Response 200

starredfalse

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/nodes/{id}/star" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}/text-in-imageWhat text in images made of a file

The line a file's Details shows for a picture or a scanned PDF on a Drive with text in images: read, partly_read (some words may be missing), unreadable (lines of text no model reads yet, usually a language not supported yet), damaged (damaged, or too small or too large to read), no_text, not_read_yet or off (turned off in Preferences). null when it doesn't apply: not a picture or a scan, a private space, or a plan without text in images. Never the text itself.

Key scope: drive.read

Path parameters

idstringThe file's id.

Response 200

text_in_imageobject | null
text_in_image.state"read" | "partly_read" | "unreadable" | "damaged" | "no_text" | "not_read_yet" | "off"
text_in_image.labelstringThe words to show, e.g. "Partly read".
text_in_image.detailstring | null

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/text-in-image" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/nodes/{id}/versionsList a file's versions

Newest first; the current one is marked. Earlier versions are kept the plan's version days (purge_after).

Key scope: drive.read

Path parameters

idstringThe file's id.

Response 200

current_version_idstring | null
version_daysstring | integer
versionsarray of object
versions.idstring
versions.sizestring
versions.content_hashstring | null
versions.source"web" | "desktop" | "phone" | "api" | "restore" | "copy"
versions.created_atdate-time
versions.created_bystring | null
versions.purge_afterdate-time | null
versions.currentboolean
versions.editors?array of stringA note version (DW4): everyone who edited it together, by name.

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/nodes/{id}/versions" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/drive/nodes/{id}/versions/{vid}Delete an earlier version

For good. The current version can't be deleted (current_version): restore another first, or trash the file. Not for apps and agents (agent_no_purge).

Key scope: drive.write

Path parameters

idstringThe file's id.
vidstringThe version's id.

Response 200

deletedtrue

A name ending in ? is optional.

An error: no_drive, not_found, read_only, no_version, current_version, agent_no_purge, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/nodes/{id}/versions/{vid}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/nodes/{id}/versions/{vid}/restoreRestore an earlier version

Its content becomes the current version — as a new version, so nothing is lost.

Key scope: drive.write

Path parameters

idstringThe file's id.
vidstringThe version's id.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring

A name ending in ? is optional.

An error: no_drive, not_found, read_only, no_version, drive_full, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/nodes/{id}/versions/{vid}/restore" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/notesMake a note

A new, empty note in a folder: a Markdown file (Untitled note.md, or name with .md added; a number is added when the name is taken). It's a file like any other — versions, sharing, sync — that people edit together, live.

Key scope: drive.write

Request body (JSON)

parent_id?string | nullThe folder (your Drive's top when absent).
name?string | nullThe note's name.

A name ending in ? is optional.

Response 200

noteDriveNote
note.idstring
note.namestring
note.parent_idstring | null
note.sizestring
note.version_idstring | null
note.modified_atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, drive_full, read_only, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/notes" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/drive/notes/{id}Open a note

Its text, and for live editing its Yjs state (state, base64, one Y.Text named t), the epoch and the last update's seq; who else is in it (presence); whether you may edit it. Any .md file opens as a note.

Key scope: drive.read

Path parameters

idstringThe note's id.

Response 200

noteDriveNote
note.idstring
note.namestring
note.parent_idstring | null
note.sizestring
note.version_idstring | null
note.modified_atdate-time
epochstring
statestring
seqstring
savedstringThe last `seq` already saved as a version.
textstring
presencearray of DriveNotePresence
presence.clientstring
presence.user_idstring
presence.namestring
presence.colourstring
presence.anchorstring | null
presence.headstring | null
presence.atstring | integer
youstring
can_editboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_note, note_too_large, note_unreadable, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/notes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/drive/notes/{id}Set a note's text

The whole text, from an agent or a script: merged into the live note as one edit (people editing it see it at once) and saved as a new version now.

Key scope: drive.write

Path parameters

idstringThe note's id.

Request body (JSON)

textstringThe note's Markdown (up to 1 MB).

A name ending in ? is optional.

Response 200

noteDriveNote
note.idstring
note.namestring
note.parent_idstring | null
note.sizestring
note.version_idstring | null
note.modified_atdate-time

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_note, note_too_large, note_saving, read_only, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/drive/notes/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"text":"…"}'
POST/drive/notes/{id}/presenceSay where you are in a note

Your cursor (no edit), or leave: true when you close it. Presence lasts 30 seconds unless renewed.

Key scope: drive.read

Path parameters

idstringThe note's id.

Request body (JSON)

clientstringYour client's id.
anchor?string | nullYour selection's anchor.
head?string | nullYour selection's head.
leave?booleanYou closed the note.

A name ending in ? is optional.

Response 200

okboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_note, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/notes/{id}/presence" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"client":"…"}'
GET/drive/notes/{id}/updatesA note's edits since yours

Live editing: the Yjs updates after since (base64), and who's in the note — or, when a save already folded what you missed, the whole state. note_reloaded (409) when a version from elsewhere arrived: open the note again.

Key scope: drive.read

Path parameters

idstringThe note's id.

Query parameters

epochstringThe epoch the note opened with.
since?stringThe last `seq` you have.

Response 200

epochstring
seqstring
savedstringThe last `seq` already saved as a version.
updatesarray of object
updates.seqstring
updates.clientstring
updates.updatestring
presencearray of DriveNotePresence
presence.clientstring
presence.user_idstring
presence.namestring
presence.colourstring
presence.anchorstring | null
presence.headstring | null
presence.atstring | integer
state?stringWhen you're behind the last save: the note's whole state (apply it; it holds every edit you missed).

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_note, note_reloaded, bad_epoch, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/notes/{id}/updates" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/notes/{id}/updatesSend an edit to a note

One Yjs update (base64) from your client, and where your cursor is (Yjs relative positions, base64). It's saved as a new version once nobody has typed for a few seconds (at most a minute after the first unsaved edit), naming everyone who edited.

Key scope: drive.write

Path parameters

idstringThe note's id.

Request body (JSON)

epochstringThe note's epoch.
clientstringYour client's id (4–40 letters, digits, - or _).
updatestringThe Yjs update, base64.
anchor?string | nullYour selection's anchor.
head?string | nullYour selection's head.

A name ending in ? is optional.

Response 200

seqstring

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, not_a_note, note_reloaded, bad_update, update_too_large, note_too_large, read_only, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/notes/{id}/updates" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"epoch":"…","client":"…","update":"…"}'
GET/drive/recentList recently changed files

Key scope: drive.read

Query parameters

limit?string1–500 (200 by default).

Response 200

itemsarray of DriveNode
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
planDrivePlan
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.
spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/recent" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/recent
GET/drive/searchSearch your Drive

Files and folders by name across everything you can open in Drive — My files, the team spaces you're in and what is shared with you — best first (typos forgiven), 25 a page. Each result carries the folders above it from the top you can see (never above a folder shared with you), its space, and your role. An end-to-end space's files are found by type, size and date, never by name or contents. in limits it to a folder's subtree; trashed=1 searches your trash (and the team trash you manage). contents=1 also searches the words inside files where the Drive plan includes it (Drive 2 TB and up, every Team Drive plan): names and contents come back in one list, best first, each item saying how it matched, and a file found by its text carries the words around the match.

Key scope: drive.read

Query parameters

qstringSearch words (200 characters at most).
in?stringA folder: search only inside it.
trashed?"1"`1`: search the trash instead.
kind?"file" | "folder"`file` or `folder` only.
page?stringThe page of results, 1–8.
contents?"0" | "1"`1`: search inside files too (never the trash or folders). `0` (the default): names only.

Response 200

itemsarray of object
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
items.patharray of objectFrom the top folder down to this one.
items.path.idstring
items.path.namestring
items.spaceDriveSpace
items.space.idstring
items.space.kind"personal" | "team"
items.space.namestring
items.space.root_idstringThe top folder.
items.space.seqstringThe last change.
items.space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
items.space.filesstring | integer
items.space.e2eboolean
items.sharedobject | null
items.shared.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
items.shared.top_idstring | null
items.shared.ownerDriveOwner | null
items.match?"name" | "contents"With `contents=1`: how it was found — its name, or the words inside it (its best match).
items.snippet?stringThe words around the match inside the file, as plain text (with `contents=1`, when its text matched).
items.marks?array of array of anyWhere the matched words are in `snippet`: [start, end) string indices in UTF-16 code units, in order, never overlapping.
searchSearchMetaPresent when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first.
search.engine"meilisearch" | "postgres"
search.pagestring | integerThis page (1–8).
search.totalstring | integerAbout how many match (the search engine's estimate).
search.next_pagestring | integer | null
contents?"searched" | "not_in_plan" | "unavailable" | "blocked_by_team"With `contents=1` (not for the trash or folders): `searched`; `not_in_plan` — no Drive you can search includes search inside files; `unavailable` — it isn't answering right now, these are name matches; `blocked_by_team` — an API key or app whose person's team doesn't let apps read file contents: these are name matches only.

A name ending in ? is optional.

An error: invalid_query, no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/search" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/share-targetsList the teams and groups you can share with

Each team you're on (share with {kind: "team", id}) and its groups ({kind: "group", id}), with their sizes.

Key scope: drive.read

Response 200

teamsarray of object
teams.idstring
teams.namestring
teams.seatsstring | integer
teams.groupsarray of object
teams.groups.idstring
teams.groups.namestring
teams.groups.membersstring | integer

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/share-targets" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/share-targets
GET/drive/sharedList what's shared with you

Files and folders other people share with you — with you, your address, a group you're in or your team — newest first, each with its owner and your role. A share inside a folder that's also shared with you is reached from that folder. Needs no Drive plan. count=1 answers {count} only (how many shares name you).

Key scope: drive.read

Query parameters

count?"1"`1`: only how many.

Response 200

itemsarray of object
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
items.share_idstring
items.role"co_owner" | "editor" | "uploader" | "viewer" | "previewer"
items.shared_atdate-time
items.expires_atdate-time | null
items.via"user" | "email" | "group" | "team"
items.ownerDriveOwner | null
items.owner.idstring
items.owner.namestring
items.owner.avatar_urlstring | null
items.can_leavebooleanIt names you (not a group or team): you may leave it.

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/shared" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/shared
PATCH/drive/shares/{id}Change a share

Its role, or when it ends (expires_at; null: never). The owner and co-owners.

Key scope: drive.write

Path parameters

idstringThe share's id.

Request body (JSON)

role?stringpreviewer, viewer, uploader, editor or co_owner.
expires_at?date-time | nullWhen it ends, or null.

A name ending in ? is optional.

Response 200

updatedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_share, bad_role, bad_expiry, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/shares/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/drive/shares/{id}Remove a share

The owner and co-owners remove anyone's; the person a share names may leave it (left: true). Removing what's already gone answers removed: false.

Key scope: drive.write

Path parameters

idstringThe share's id.

Response 200

removedboolean
left?boolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/shares/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/spacesList your team spaces

The team spaces you reach — every space of a team you own or administer, and those shared with you — with your role, the team and how many shares it has; and the teams whose Drive plan has team spaces, with whether you may make one there.

Key scope: drive.read

Response 200

spacesarray of object
spaces.idstring
spaces.kind"personal" | "team"
spaces.namestring
spaces.root_idstringThe top folder.
spaces.seqstringThe last change.
spaces.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
spaces.filesstring | integer
spaces.e2eboolean
spaces.teamobject
spaces.team.idstring
spaces.team.namestring
spaces.role"owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer"
spaces.membersstring | integer
teamsarray of object
teams.idstring
teams.namestring
teams.can_createboolean
teams.managesboolean

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/spaces" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/spaces
POST/drive/spacesMake a team space

A space owned by the team: its files stay with the team when someone leaves, and count against the team's Drive pool. The team's policy says who may make one (its owner and admins by default); its maker gets a co-owner share on it.

Key scope: drive.write

Request body (JSON)

team_idstringThe team (subscription) id.
namestringIts name (unique in the team).
private?objectMake it a private space (end-to-end; Team Drive Enterprise): `{ public_key, seals }` — the space key's public half and its seed sealed to you. Names and files are then sealed on the devices of the people it's shared with; needs encryption set up (Settings › Encryption).
private.public_keystring
private.sealsarray of object
private.seals.user_idstring
private.seals.identity_versionstring | integer
private.seals.sealedstring

A name ending in ? is optional.

Response 200

spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean

A name ending in ? is optional.

An error: not_on_team, no_team_drive, space_policy, name_taken, bad_name, plan_required, encryption_setup_required, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/spaces" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"team_id":"…","name":"…"}'
PATCH/drive/spaces/{id}Rename a team space

Co-owners and the team's owner and admins.

Key scope: drive.write

Path parameters

idstringThe space's id.

Request body (JSON)

namestringIts new name.

A name ending in ? is optional.

Response 200

spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_space, bad_name, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/spaces/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…"}'
DELETE/drive/spaces/{id}Archive a team space

The team's owner and admins.

Key scope: drive.write

Path parameters

idstringThe space's id.

Response 200

archivedboolean

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_space, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/spaces/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/spaces/{id}/changesList a space's changes since a point

The space's change journal after since, each change with the node as it is now (node: null — trashed or deleted for good: remove it). Apply them in order; more says there are more.

Key scope: drive.read

Path parameters

idstringThe space's id.

Query parameters

since?stringThe last `seq` you applied.
limit?string1–5,000 (1,000 by default).

Response 200

space_idstring
cursorstring
moreboolean
changesarray of object
changes.seqstring
changes.op"create" | "update" | "rename" | "move" | "trash" | "restore" | "delete"
changes.node_idstring
changes.nodeDriveNode | null
changes.node.idstring
changes.node.parent_idstring | null
changes.node.kind"folder" | "file"
changes.node.namestring
changes.node.sizestringThe current version's size (0 for a folder).
changes.node.mimestring | null
changes.node.content_hashstring | null
changes.node.versionsstring | integerVersions kept (the current one included).
changes.node.modified_atdate-timeWhen the content last changed.
changes.node.updated_atdate-timeWhen anything last changed (a rename, a move…).
changes.node.created_atdate-time
changes.node.trashed_atdate-time | null
changes.node.purge_afterdate-time | null
changes.node.starred?boolean
changes.node.seqstringThe change that last touched it (see GET /v1/drive/changes).
changes.node.thumb_url?string | null
changes.node.inside?string | integerTrash: how many items a trashed folder holds.
changes.node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
changes.node.version_idstring | null
changes.node.lock?objectLocked for editing: others' saves are refused until then (P6).
changes.node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/spaces/{id}/changes" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/spaces/{id}/snapshotList a space for a first sync

Every live file and folder of the space, a page at a time in id order (next → after). Take seq from the first page and then read GET …/changes from it: changes made while you listed come again there.

Key scope: drive.read

Path parameters

idstringThe space's id.

Query parameters

after?stringThe last node id you have (0: from the start).
limit?string1–5,000 (1,000 by default).

Response 200

space_idstring
seqstring
nodesarray of DriveNode
nodes.idstring
nodes.parent_idstring | null
nodes.kind"folder" | "file"
nodes.namestring
nodes.sizestringThe current version's size (0 for a folder).
nodes.mimestring | null
nodes.content_hashstring | null
nodes.versionsstring | integerVersions kept (the current one included).
nodes.modified_atdate-timeWhen the content last changed.
nodes.updated_atdate-timeWhen anything last changed (a rename, a move…).
nodes.created_atdate-time
nodes.trashed_atdate-time | null
nodes.purge_afterdate-time | null
nodes.starred?boolean
nodes.seqstringThe change that last touched it (see GET /v1/drive/changes).
nodes.thumb_url?string | null
nodes.inside?string | integerTrash: how many items a trashed folder holds.
nodes.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
nodes.version_idstring | null
nodes.lock?objectLocked for editing: others' saves are refused until then (P6).
nodes.lock.user_idstring
nodes.lock.namestring
nodes.lock.untildate-time
nodes.lock.mineboolean
nodes.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
nodes.conflict.idstring
nodes.conflict.file_idstring
nextstring | null

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/spaces/{id}/snapshot" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/drive/starredList starred files and folders

Key scope: drive.read

Response 200

itemsarray of DriveNode
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
planDrivePlan
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.
spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/starred" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/starred
GET/drive/sync/rootsList the spaces a computer syncs

My files, and every team space whose top folder the caller can reach — with their role there (read_only: a viewer's placeholders can't be changed) and each space's change counter. A sync client lists each with GET …/snapshot, then follows GET …/changes from the snapshot's seq.

Key scope: drive.read

Response 200

rootsarray of object
roots.space_idstring
roots.kind"personal" | "team"
roots.namestring
roots.teamstring | null
roots.root_idstring
roots.role"owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer"
roots.read_onlyboolean
roots.seqstring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/sync/roots" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/sync/roots
GET/drive/tasksList your open tasks

Tasks given to you on Drive files, due first. count=1 answers {count} only.

Key scope: drive.read

Query parameters

count?"1"`1`: only how many.

Response 200

tasksarray of any

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/tasks" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/tasks
PATCH/drive/tasks/{id}Change a task

Mark it done (or open again) or change when it's due — its assignee, its maker or an editor.

Key scope: drive.write

Path parameters

idstringThe task's id.

Request body (JSON)

status?stringopen or done.
due_at?date-time | nullWhen it's due, or null.

A name ending in ? is optional.

Response 200

taskobject
task.idstring
task.status"open" | "done"
task.due_atdate-time | null

A name ending in ? is optional.

An error: no_drive, not_found, drive_role, no_task, not_yours, bad_status, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/drive/tasks/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/drive/teams/{id}/conflictsList a team's open sync conflicts

The conflict center: every open conflict in the team's spaces. The team's owner and admins.

Key scope: drive.read

Path parameters

idstringThe team plan's id.

Response 200

conflictsarray of DriveConflict
conflicts.idstring
conflicts.space_idstring
conflicts.space_namestring
conflicts.fileany | null
conflicts.copyany | null
conflicts.patharray of object
conflicts.path.idstring
conflicts.path.namestring
conflicts.byobject
conflicts.by.idstring
conflicts.by.namestring
conflicts.client_namestring | null
conflicts.state"open" | "resolved"
conflicts.resolution"kept_file" | "kept_copy" | "kept_both" | null
conflicts.created_atdate-time
conflicts.resolved_atdate-time | null

A name ending in ? is optional.

An error: team_admin, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/teams/{id}/conflicts" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/teams/{id}/conflicts/resolveResolve a team's conflicts by a rule

newest: whichever of the file and the copy was saved last; file: the file; both: keep both. Every open conflict of the team's spaces, or those in ids.

Key scope: drive.write

Path parameters

idstringThe team plan's id.

Request body (JSON)

rulestring`newest`, `file` or `both`.
ids?array of string | nullConflict ids (all when absent).

A name ending in ? is optional.

Response 200

resolvedstring | integer
skippedstring | integer

A name ending in ? is optional.

An error: team_admin, bad_rule, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/teams/{id}/conflicts/resolve" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"rule":"…"}'
GET/drive/teams/{id}/policyGet a team's Drive policy

Who may make team spaces (admins, managers, members) and whether spaces may be shared outside the team (allow, off — Team Drive Pro and up). The team's owner and admins.

Key scope: drive.read

Path parameters

idstringThe team (subscription) id.

Response 200

policyobject
policy.create"admins" | "managers" | "members"
policy.outside"allow" | "off"
policy.peers"on" | "off"
outside_controlsboolean

A name ending in ? is optional.

An error: team_admin, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/teams/{id}/policy" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/drive/teams/{id}/policyChange a team's Drive policy

Key scope: drive.write

Path parameters

idstringThe team (subscription) id.

Request body (JSON)

create?stringadmins, managers or members.
outside?stringallow or off.
peers?stringon (the default) or off: the team's computers on one network fetch its spaces' files from each other.
automations?stringWho sets up folder automations in the team's spaces: `admins` (the owner and admins, the default) or `members` (everyone who can edit the folder).

A name ending in ? is optional.

Response 200

policyobject
policy.create"admins" | "managers" | "members"
policy.outside"allow" | "off"
policy.peers"on" | "off"

A name ending in ? is optional.

An error: team_admin, bad_policy, plan_feature, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/drive/teams/{id}/policy" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
GET/drive/trashList the trash

One row per trashed item (a trashed folder is one row; inside says how much it holds), newest first.

Key scope: drive.read

Response 200

itemsarray of DriveNode
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
version_daysstring | integer
planDrivePlan
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.
spaceDriveSpace
space.idstring
space.kind"personal" | "team"
space.namestring
space.root_idstringThe top folder.
space.seqstringThe last change.
space.bytes_usedstringWhat the plan counts: current versions of files not in the trash.
space.filesstring | integer
space.e2eboolean

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/trash" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/trash
DELETE/drive/trashEmpty the trash

Key scope: drive.write

Response 200

deletedstring | integer

A name ending in ? is optional.

An error: no_drive, agent_no_purge, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/trash" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/drive/trash/{id}Delete a trashed item for good

With everything in it and every version. It can't be brought back. Not for apps and agents (agent_no_purge): deleting for good is the person's own act.

Key scope: drive.write

Path parameters

idstringThe trashed item's id.

Response 200

deletedstring | integerItems deleted.

A name ending in ? is optional.

An error: no_drive, not_found, not_in_trash, agent_no_purge, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/trash/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/uploadsStart an upload

Upload a file into parent_id (the top folder when absent): this answers part_urls; PUT each part's bytes (part_size each, the last one the rest) to its URL, then POST …/complete. The URLs live an hour — ask for more with POST …/parts. Send hash (BLAKE3 of the whole file, hex) and content already in My files needs no upload at all: the answer is state: done with the file. The hash is only a hint: SendFlo computes it from the stored bytes and uses its own. A name already there: rename (the default, "name (2)"), replace (a new version of that file) or fail. node_id adds a version to that file. Up to 5 TB a file; drive_full past the plan's room.

Key scope: drive.write

Request body (JSON)

parent_id?stringThe folder.
namestringThe file's name.
sizenumberBytes.
mime?string | nullIts media type.
hash?string | nullBLAKE3 of the content (64 hex characters), optional.
on_conflict?string`rename` (the default), `replace` or `fail`.
node_id?string | nullA file to add a version to.
source?string`api` (the default for keys), `desktop` or `phone`.
modified_at?date-time | nullWhen the content last changed on the device.
base_version_id?string | nullWith `node_id`: the version this save started from. If the file has a newer one by the time the upload finishes, nothing is overwritten: the content becomes a conflict copy beside it (`conflict_of` in the answer).
client_id?string | nullA sync client's id (POST /v1/drive/clients): a conflict copy is named after that computer.
parts?objectWith `node_id`: send only what changed. `part_size` is the base version's (GET /v1/drive/nodes/:id/parts), `hashes` the new file's BLAKE3 of each part at that size. Parts the base version's object holds unchanged are copied by SendFlo; the answer's `upload_parts` lists the ones to send.
parts.part_sizestring | integer
parts.hashesarray of string
name_key?stringIn a private space (end-to-end): the name's index (64 hex) your device computed, with the sealed `name`.
sealed?objectIn a private space: `{ space_key_id, sealed_key }` — the space key's current version and the file's key sealed to it. The bytes you upload are the sealed (SFE2) file; a name already there is refused (`fail`), never numbered.
sealed.space_key_idstring
sealed.sealed_keystring

A name ending in ? is optional.

Response 200

state"done"
deduplicatedtrue
nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring
conflict_of?string | null

A name ending in ? is optional.

An error: no_drive, not_found, read_only, no_folder, name_taken, bad_name, bad_size, bad_hash, bad_parts, too_large, drive_full, locked, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/uploads" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…","size":1}'
DELETE/drive/uploads/{id}Cancel an upload

Key scope: drive.write

Path parameters

idstringThe upload's id.

Response 200

abortedtrue

A name ending in ? is optional.

An error: no_drive, not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/drive/uploads/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/uploads/{id}/completeFinish an upload

SendFlo checks every part the storage holds against the file's size and makes the file. Safe to call again: a repeated call answers the same file. parts_missing until every part has arrived; size_mismatch when a part is the wrong size (the upload is cancelled — start again). A save from a version that's no longer current answers its conflict copy, with conflict_of; locked when someone else locked the file meanwhile.

Key scope: drive.write

Path parameters

idstringThe upload's id.

Response 200

nodeDriveNode
node.idstring
node.parent_idstring | null
node.kind"folder" | "file"
node.namestring
node.sizestringThe current version's size (0 for a folder).
node.mimestring | null
node.content_hashstring | null
node.versionsstring | integerVersions kept (the current one included).
node.modified_atdate-timeWhen the content last changed.
node.updated_atdate-timeWhen anything last changed (a rename, a move…).
node.created_atdate-time
node.trashed_atdate-time | null
node.purge_afterdate-time | null
node.starred?boolean
node.seqstringThe change that last touched it (see GET /v1/drive/changes).
node.thumb_url?string | null
node.inside?string | integerTrash: how many items a trashed folder holds.
node.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
node.version_idstring | null
node.lock?objectLocked for editing: others' saves are refused until then (P6).
node.lock.user_idstring
node.lock.namestring
node.lock.untildate-time
node.lock.mineboolean
node.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
node.conflict.idstring
node.conflict.file_idstring
conflict_of?string | null

A name ending in ? is optional.

An error: no_drive, not_found, read_only, no_upload, upload_closed, parts_missing, size_mismatch, name_taken, no_folder, locked, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/uploads/{id}/complete" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/drive/uploads/{id}/partsGet more part URLs

Fresh URLs for the parts named (1-based, 500 at most a call): for the parts past the first 64, a resumed upload, or URLs that expired.

Key scope: drive.write

Path parameters

idstringThe upload's id.

Request body (JSON)

partsarray of string | integerPart numbers.

A name ending in ? is optional.

Response 200

part_urlsarray of string

A name ending in ? is optional.

An error: no_drive, not_found, no_upload, upload_closed, bad_parts, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/drive/uploads/{id}/parts" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"parts":["…"]}'
GET/drive/usageGet your Drive usage

What the plan counts (current versions of files not in the trash), and what earlier versions take apart from it.

Key scope: drive.read

Response 200

bytes_usedstring
filesstring | integer
bytesstringThe plan's room.
history_bytesstringEarlier versions and the trash (not counted).
planDrivePlan
plan.planstring
plan.namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: files can be read and downloaded until deleted, nothing new added.
plan.bytesstringThe room.
plan.version_daysstring | integerHow long earlier versions and trashed items are kept.

A name ending in ? is optional.

An error: no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/drive/usage" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/drive/usage
GET/search/indexingSearch inside files: status and progress

For your own Drive and each team Drive you're on, when its plan searches inside files: the plan, a trial's cap, the documents searched inside now, and the latest backfill — the files already there when the plan started, read newest first. Backfills run first come, first served; started_at stays null while one waits its turn. A Drive without search inside files isn't listed.

Key scope: drive.read

Response 200

itemsarray of object
items.workspacestringThe Drive's workspace.
items.kind"personal" | "team"
items.teamobject | null
items.team.idstringThe team plan.
items.team.namestring | null
items.planstring | null
items.enabledbooleanThe plan searches inside files now.
items.trialboolean
items.capstring | integer | null
items.untildate-time | null
items.documentsstring | integerDocuments searched inside now.
items.backfillSearchBackfill | null
items.backfill.idstring
items.backfill.status"queued" | "running" | "paused" | "done" | "cancelled" | "failed"
items.backfill.triggerstringWhy it ran: `trial`, `upgrade`, `reupgrade`, `converted` (a trial became a paid plan), `ocr_opt_in` (text in images was turned on), `staff`.
items.backfill.files_seenstring | integerFiles it looked at (end-to-end files included: their contents are never read).
items.backfill.queuedstring | integerFiles it sent to be read.
items.backfill.donestring | integerFiles read.
items.backfill.failedstring | integerFiles that couldn't be read (a password-protected or broken document, or the reader gave up).
items.backfill.skippedmap of string | integerFiles left out, by reason: `e2e`, `type` (one we don't read), `empty`, `too_big`, `already` (read before), `cap` (past a trial's documents or OCR pages), `gone`, and for text in images `off`, `too_small`, `too_large`.
items.backfill.processedstring | integerFiles back from the reader: `done`, `failed` and those skipped there. The progress bar's numerator.
items.backfill.totalstring | integerFiles it expects to read: an estimate while it walks, exact once the walk is over.
items.backfill.started_atdate-time | null
items.backfill.finished_atdate-time | null
items.ocrobject | null
items.ocr.allowedbooleanThe plan offers text in images (Drive 5 TB, Team Drive Pro and Enterprise).
items.ocr.onbooleanTurned on for this Drive (PUT /v1/search/settings).
items.ocr.sincedate-time | null
items.ocr.untildate-time | null
items.ocr.cap_pagesstring | integer | null
items.ocr.pagesstring | integerPages read for text in images so far.
items.ocr.filesstring | integerFiles whose text in images was read and is searchable.
items.ocr.backfillSearchBackfill | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/search/indexing" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/search/indexing
GET/search/settingsSearch settings of the Drives you manage

Your own Drive, and each team Drive whose team you own or administer, with its plan's search inside files: whether the plan offers text in images (OCR of photos, screenshots and scanned PDFs — the first 10 pages of each file, self-hosted) and whether it is on. A Drive without search inside files isn't listed.

Key scope: drive.read

Response 200

itemsarray of SearchSettingsItem
items.workspacestringThe Drive's workspace: pass it back as `workspace` to change this Drive.
items.kind"personal" | "team"
items.teamobject | null
items.team.idstringThe team plan.
items.team.namestring | null
items.planstring | null
items.ocrobject
items.ocr.allowedbooleanThe plan offers text in images: Drive 5 TB, Team Drive Pro and Team Drive Enterprise.
items.ocr.onboolean
items.ocr.sincedate-time | null
items.ocr.untildate-time | null
items.ocr.cap_pagesstring | integer | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/search/settings" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/search/settings
PUT/search/settingsTurn text in images on or off for a Drive

Opt-in, per Drive. On: the images and scanned PDFs already there are read newest first (the OCR backfill, first come first served, its progress in GET /v1/search/indexing), and new ones as they arrive; a trial reads 200 pages. Off: text read from images leaves search at once and is deleted after 30 days unless it's turned on again. Your own Drive by default; a team's Drive (workspace from GET /v1/search/settings) by its owner and admins. 403 plan_feature when the plan doesn't offer it. Each change is on the audit log.

Key scope: drive.write

Request body (JSON)

ocrbooleanOn (true) or off (false).
workspace?stringThe Drive to change (from `GET /v1/search/settings`); default your own Drive.

A name ending in ? is optional.

Response 200

itemSearchSettingsItem
item.workspacestringThe Drive's workspace: pass it back as `workspace` to change this Drive.
item.kind"personal" | "team"
item.teamobject | null
item.team.idstringThe team plan.
item.team.namestring | null
item.planstring | null
item.ocrobject
item.ocr.allowedbooleanThe plan offers text in images: Drive 5 TB, Team Drive Pro and Team Drive Enterprise.
item.ocr.onboolean
item.ocr.sincedate-time | null
item.ocr.untildate-time | null
item.ocr.cap_pagesstring | integer | null
changedbooleanFalse when it already was that way.
backfillstring | null

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/search/settings" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"ocr":true}'
GET/search/similar/{nodeId}Find pictures that look like this one

Pictures in your Drive (and the team Drives and folders shared with you) that look like this one, compared by their image embeddings (SigLIP 2, made on our own servers; never a face or a person's identity, only how the whole picture looks). Only where the Drive's plan searches inside files (Drive 2 TB and up, every Team Drive plan). similar says what happened: searched; not_a_picture (a folder, or an end-to-end encrypted file: never read); not_in_plan; no_vector (this picture hasn't been processed yet); unavailable (search isn't answering: try again); blocked_by_team (an API key or app whose person's team doesn't let apps read file contents).

Key scope: drive.read

Path parameters

nodeIdstringThe picture's id (a Drive file).

Query parameters

limit?stringHow many, 1–24 (default 12).

Response 200

itemsarray of objectMost alike first. Only pictures you can open now, in the Drives you can search (each one checked again).
items.idstring
items.parent_idstring | null
items.kind"folder" | "file"
items.namestring
items.sizestringThe current version's size (0 for a folder).
items.mimestring | null
items.content_hashstring | null
items.versionsstring | integerVersions kept (the current one included).
items.modified_atdate-timeWhen the content last changed.
items.updated_atdate-timeWhen anything last changed (a rename, a move…).
items.created_atdate-time
items.trashed_atdate-time | null
items.purge_afterdate-time | null
items.starred?boolean
items.seqstringThe change that last touched it (see GET /v1/drive/changes).
items.thumb_url?string | null
items.inside?string | integerTrash: how many items a trashed folder holds.
items.scan_state?"pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error"
items.version_idstring | null
items.lock?objectLocked for editing: others' saves are refused until then (P6).
items.lock.user_idstring
items.lock.namestring
items.lock.untildate-time
items.lock.mineboolean
items.conflict?objectThis file is a sync conflict's copy of `file_id` (P6).
items.conflict.idstring
items.conflict.file_idstring
items.patharray of objectThe folders above it, from the top you can see.
items.path.idstring
items.path.namestring
items.spaceanyIts space (as in `GET /v1/drive/search`).
items.scorenumberHow alike the two pictures are: 0.5 (unrelated) to 1 (the same picture), from their image embeddings.
similar"searched" | "not_a_picture" | "not_in_plan" | "no_vector" | "unavailable" | "blocked_by_team"

A name ending in ? is optional.

An error: not_found, no_drive, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/search/similar/{nodeId}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

backup

Backup — your devices' backup sets: devices, the chunk index, pack uploads, snapshots, and where blobs are for a restore.

GET/backupYour Backup plans and devices

The plans you back up to (your own, and each team's you're on) with what they store and how many devices they cover, and every device you can see — a team's owner and admins see every member's.

Key scope: backup.read

Response 200

plansarray of BackupPlan
plans.scope"personal" | "team"
plans.team_idstring | null
plans.team_namestring | null
plans.planstring
plans.plan_namestring
plans.status"active" | "trialing" | "past_due" | "read_only"
plans.writablebooleanFalse on a plan that ended: backups can be restored until deleted, nothing new is backed up.
plans.bytesstringThe room.
plans.storedstringWhat the plan's packs take.
plans.devices_limitstring | integer | null
plans.devices_usedstring | integer
plans.version_daysstring | integer
plans.private_keybooleanThe plan offers a private key (only you can read the backup).
plans.managesbooleanThe caller sees and manages every device on the plan (a team's owner or admins).
plans.team_policymap of any | null
devicesarray of object
devices.idstring
devices.namestring
devices.kind"computer" | "server" | "phone" | "volume"
devices.osstring | null
devices.client"cli" | "desktop" | "phone"
devices.client_versionstring | null
devices.volume_serialstring | null
devices.status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"
devices.state"idle" | "running" | "error"
devices.progressmap of any | null
devices.pausedboolean
devices.key_mode"sendflo" | "private"
devices.suspiciousobject | null
devices.suspicious.atdate-time
devices.suspicious.reason?string
devices.suspicious.changed?string | integer
devices.suspicious.sample?array of string
devices.changes_ok_untildate-time | null
devices.policymap of any | null
devices.bytesstringWhat the last complete snapshot covers.
devices.filesstring | integer
devices.last_snapshot_idstring | null
devices.last_snapshot_atdate-time | null
devices.last_seen_atdate-time | null
devices.last_errorstring | null
devices.last_error_atdate-time | null
devices.created_atdate-time
devices.minebooleanIt's the caller's own device (false: a team member's, seen by a team admin).
devices.userobject
devices.user.idstring
devices.user.name?string
devices.user.email?string
devices.team_idstring | null

A name ending in ? is optional.

An error: no_backup, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/backup
POST/backup/devicesAdd a device

Register this computer, server, phone or external drive as a device with its own backup set. It goes on your own plan, or team_id's (choose_team when you could back up to more than one). Computers, servers and phones count against the plan's devices (device_limit); an external drive (kind: volume, with its volume_serial) doesn't, and registering it again reattaches its set (200, reattached: true).

Key scope: backup.write

Request body (JSON)

namestringWhat to call it ("Dana's laptop").
kind?string`computer` (the default), `server`, `phone` or `volume`.
os?string | nullIts operating system and version.
client?string`cli` (the default), `desktop` or `phone`.
client_version?string | nullThe backup client's version.
volume_serial?string | nullAn external drive's volume serial.
policy?map of any | nullWhat the client backs up: paths, exclude, schedule_hours, max_file_bytes.
key_mode?string`sendflo` (the default: previews and restore from the web work) or `private` (only your key opens it).
team_id?string | nullThe team plan to back up to.

A name ending in ? is optional.

Response 200

deviceBackupDevice
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.client"cli" | "desktop" | "phone"
device.client_versionstring | null
device.volume_serialstring | null
device.status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"
device.state"idle" | "running" | "error"
device.progressmap of any | null
device.pausedboolean
device.key_mode"sendflo" | "private"
device.suspiciousobject | null
device.suspicious.atdate-time
device.suspicious.reason?string
device.suspicious.changed?string | integer
device.suspicious.sample?array of string
device.changes_ok_untildate-time | null
device.policymap of any | null
device.bytesstringWhat the last complete snapshot covers.
device.filesstring | integer
device.last_snapshot_idstring | null
device.last_snapshot_atdate-time | null
device.last_seen_atdate-time | null
device.last_errorstring | null
device.last_error_atdate-time | null
device.created_atdate-time
device.minebooleanIt's the caller's own device (false: a team member's, seen by a team admin).
device.userobject
device.user.idstring
device.user.name?string
device.user.email?string
reattachedtrue
planBackupPlan
plan.scope"personal" | "team"
plan.team_idstring | null
plan.team_namestring | null
plan.planstring
plan.plan_namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: backups can be restored until deleted, nothing new is backed up.
plan.bytesstringThe room.
plan.storedstringWhat the plan's packs take.
plan.devices_limitstring | integer | null
plan.devices_usedstring | integer
plan.version_daysstring | integer
plan.private_keybooleanThe plan offers a private key (only you can read the backup).
plan.managesbooleanThe caller sees and manages every device on the plan (a team's owner or admins).
plan.team_policymap of any | null

A name ending in ? is optional.

An error: no_backup, read_only, choose_team, not_on_team, device_limit, bad_name, bad_kind, bad_client, bad_key_mode, no_private_key, volume_serial, bad_team, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"…"}'
GET/backup/devices/{id}Get a device

Key scope: backup.read

Path parameters

idstringThe device's id.

Response 200

deviceBackupDevice
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.client"cli" | "desktop" | "phone"
device.client_versionstring | null
device.volume_serialstring | null
device.status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"
device.state"idle" | "running" | "error"
device.progressmap of any | null
device.pausedboolean
device.key_mode"sendflo" | "private"
device.suspiciousobject | null
device.suspicious.atdate-time
device.suspicious.reason?string
device.suspicious.changed?string | integer
device.suspicious.sample?array of string
device.changes_ok_untildate-time | null
device.policymap of any | null
device.bytesstringWhat the last complete snapshot covers.
device.filesstring | integer
device.last_snapshot_idstring | null
device.last_snapshot_atdate-time | null
device.last_seen_atdate-time | null
device.last_errorstring | null
device.last_error_atdate-time | null
device.created_atdate-time
device.minebooleanIt's the caller's own device (false: a team member's, seen by a team admin).
device.userobject
device.user.idstring
device.user.name?string
device.user.email?string
planBackupPlan
plan.scope"personal" | "team"
plan.team_idstring | null
plan.team_namestring | null
plan.planstring
plan.plan_namestring
plan.status"active" | "trialing" | "past_due" | "read_only"
plan.writablebooleanFalse on a plan that ended: backups can be restored until deleted, nothing new is backed up.
plan.bytesstringThe room.
plan.storedstringWhat the plan's packs take.
plan.devices_limitstring | integer | null
plan.devices_usedstring | integer
plan.version_daysstring | integer
plan.private_keybooleanThe plan offers a private key (only you can read the backup).
plan.managesbooleanThe caller sees and manages every device on the plan (a team's owner or admins).
plan.team_policymap of any | null
live_report_due?boolean

A name ending in ? is optional.

An error: no_backup, no_device, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/devices/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PATCH/backup/devices/{id}Rename, pause or set a device's policy

A paused device's client stops at its next heartbeat; its packs and snapshots are refused (paused) until it's resumed.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

name?stringA new name.
paused?booleanPause or resume its backup.
policy?map of any | nullWhat the client backs up.

A name ending in ? is optional.

Response 200

deviceBackupDevice
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.client"cli" | "desktop" | "phone"
device.client_versionstring | null
device.volume_serialstring | null
device.status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"
device.state"idle" | "running" | "error"
device.progressmap of any | null
device.pausedboolean
device.key_mode"sendflo" | "private"
device.suspiciousobject | null
device.suspicious.atdate-time
device.suspicious.reason?string
device.suspicious.changed?string | integer
device.suspicious.sample?array of string
device.changes_ok_untildate-time | null
device.policymap of any | null
device.bytesstringWhat the last complete snapshot covers.
device.filesstring | integer
device.last_snapshot_idstring | null
device.last_snapshot_atdate-time | null
device.last_seen_atdate-time | null
device.last_errorstring | null
device.last_error_atdate-time | null
device.created_atdate-time
device.minebooleanIt's the caller's own device (false: a team member's, seen by a team admin).
device.userobject
device.user.idstring
device.user.name?string
device.user.email?string

A name ending in ? is optional.

An error: no_backup, no_device, bad_name, backup_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PATCH "https://api.sendflo.app/v1/backup/devices/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{}'
DELETE/backup/devices/{id}Remove a device

It stops counting against the plan and can't back up any more. Its backups stay until the plan's retention ends.

Key scope: backup.write

Path parameters

idstringThe device's id.

Response 200

removedtrue
kept_untildate-time | null

A name ending in ? is optional.

An error: no_backup, no_device, backup_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/backup/devices/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/backup/devices/{id}/blobs/haveWhich chunks the plan already stores

Ask before packing: the blobs (BLAKE3 ids of chunks and folder trees) the plan holds already — from any of its devices — need no upload. Up to 5,000 ids a call.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

idsarray of stringBlob ids (64 hex characters).

A name ending in ? is optional.

Response 200

havearray of string

A name ending in ? is optional.

An error: no_backup, no_device, bad_ids, bad_blob_id, too_many, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/blobs/have" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"ids":["…"]}'
POST/backup/devices/{id}/blobs/locateWhere blobs are stored (restore)

For each blob: a short-lived URL of the pack that holds it, and its byte range (offset, length); compression: df means the bytes are raw-deflate compressed (RFC 1951; a browser inflates them with DecompressionStream("deflate-raw")). Up to 5,000 ids a call; ids the plan doesn't hold are left out.

Key scope: backup.read

Path parameters

idstringThe device's id.

Request body (JSON)

idsarray of stringBlob ids.

A name ending in ? is optional.

Response 200

blobsarray of object
blobs.idstring
blobs.kind"data" | "tree"
blobs.urlstring
blobs.offsetstring
blobs.lengthstring | integer
blobs.compressionstring | null

A name ending in ? is optional.

An error: no_backup, no_device, bad_ids, too_many, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/blobs/locate" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"ids":["…"]}'
POST/backup/devices/{id}/heartbeatReport a device's backup state

The client reports every ~15 seconds while it runs (running with its progress), when it finishes (idle) and when it fails (error with what happened). The answer says whether the device is paused and its policy.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

statestring`running`, `idle` or `error`.
progress?map of any | nullphase, files_done, files_total, bytes_done, bytes_total.
error?string | nullWhat went wrong.
client_version?string | nullThe client's version.
run_start?boolean`true` on a run's first heartbeat, before it asks which blobs are stored: a snapshot from a run that a cleanup overtook is refused (`pruned_during_run`).

A name ending in ? is optional.

Response 200

pausedboolean
policymap of any | null
status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"

A name ending in ? is optional.

An error: no_backup, no_device, backup_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/heartbeat" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"state":"…"}'
POST/backup/devices/{id}/live-reportsStart a live report (private-key device)

The server can't read a private-key device's folder lists, so its computer reports which blobs its owner's live snapshots use; the prune then frees that person's unused private packs. The answer names every snapshot to walk (all of the owner's private devices in this plan — they share a key). live_report_due on the device says when one is worth making.

Key scope: backup.write

Path parameters

idstringThe device's id (a private-key device, its own computer).

Response 200

reportobject
report.idstring
report.statestring
snapshotsarray of object
snapshots.idstring
snapshots.root_treestring

A name ending in ? is optional.

An error: no_backup, no_device, read_only, paused, backup_role, not_private, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/live-reports" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/backup/devices/{id}/live-reports/{rid}/blobsAdd blobs to a live report

Blob ids (trees and chunks) the walked snapshots use, up to 20,000 a call.

Key scope: backup.write

Path parameters

idstringThe device's id.
ridstringThe report's id.

Request body (JSON)

idsarray of stringBlob ids (64 hex characters).

A name ending in ? is optional.

Response 200

addedstring | integer

A name ending in ? is optional.

An error: no_backup, no_device, read_only, paused, no_report, report_closed, too_many, bad_blob_id, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/live-reports/{rid}/blobs" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"ids":["…"]}'
POST/backup/devices/{id}/live-reports/{rid}/completeFinish a live report

The snapshots walked — exactly those the report named; every one's root folder list must be among the blobs. A cleanup is then queued. A report counts for a day, and only while no private snapshot of its owner was made after it began.

Key scope: backup.write

Path parameters

idstringThe device's id.
ridstringThe report's id.

Request body (JSON)

snapshotsarray of stringThe ids of the snapshots walked.

A name ending in ? is optional.

Response 200

reportobject
report.idstring
report.statestring
report.blobsstring | integer

A name ending in ? is optional.

An error: no_backup, no_device, read_only, paused, no_report, report_closed, snapshots_differ, root_missing, no_chunks, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/live-reports/{rid}/complete" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"snapshots":["…"]}'
GET/backup/devices/{id}/locationWhere a computer is

Find a lost or stolen computer. Off unless the computer's person turns it on (in the app); then each heartbeat's approximate place is kept: the city of the address it comes from (about 25 km), else that address's country. A new place when it moved more than 5 km or six hours passed; the newest 50. The person sees it always; a team's owner and admins only when the team shows its members' locations and the person shared this computer with the team (location_private otherwise; each look is in the audit log).

Key scope: backup.read

Path parameters

idstringThe device's id.

Response 200

onboolean
share_teamboolean
sincedate-time | null
mineboolean
teamobject | null
team.idstring
team.namestring | null
team.showsboolean
lastobject | null
last.atdate-time
last.latnumber
last.lonnumber
last.accuracy_kmstring | integer
last.citystring | null
last.regionstring | null
last.countrystring | null
last.source"ip" | "country"
historyarray of object
history.atdate-time
history.latnumber
history.lonnumber
history.accuracy_kmstring | integer
history.citystring | null
history.regionstring | null
history.countrystring | null
history.source"ip" | "country"

A name ending in ? is optional.

An error: no_device, location_private, no_backup, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/devices/{id}/location" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
PUT/backup/devices/{id}/locationTurn a computer's location on or off

The computer's own person only. on: true (optionally share_team: true on a team's plan) works only in the app's signed-in session — a key gets session_only; a key may turn it off. Off deletes every place kept.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

onbooleantrue to keep where it is, false to stop and delete the history.
share_team?booleanAlso let the team's owner and admins see it (only while the team shows locations).

A name ending in ? is optional.

Response 200

onboolean
share_teamboolean
sincedate-time | null

A name ending in ? is optional.

An error: no_device, backup_role, session_only, no_backup, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/backup/devices/{id}/location" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"on":true}'
POST/backup/devices/{id}/packsStart a pack's upload

A pack is blobs back to back, then a JSON header ({v: 1, blobs: [{id, kind, offset, length, z?}]}), its length as a 32-bit little-endian integer, and the magic SFPK1. Declare the blobs here exactly as the header lists them; PUT each part to its URL (part_size each, the last one the rest), then POST …/complete. A pack the plan already holds (the same BLAKE3) answers state: done. SendFlo reads each new pack once to check it; one that disagrees is dropped and its chunks are uploaded again on the next run. backup_full past the plan's room.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

hashstringBLAKE3 of the whole pack (64 hex characters).
sizestring | integerThe pack's bytes (up to 1 GiB).
blobsarray of objectEach blob's id, kind (`data` or `tree`), offset, length and `z: df` when raw-deflate compressed.
blobs.idstring
blobs.kindstring
blobs.offsetstring | integer
blobs.lengthstring | integer
blobs.z?string

A name ending in ? is optional.

Response 200

state"done"
pack_idstring

A name ending in ? is optional.

An error: no_backup, no_device, read_only, paused, backup_role, bad_hash, bad_size, bad_blobs, pack_too_large, backup_full, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/packs" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"hash":"…","size":"…","blobs":[{"id":"…","kind":"…","offset":"…","length":"…"}]}'
POST/backup/devices/{id}/restoresRestore files as a ZIP

Choose files and folders from a backup: SendFlo puts them together into one ZIP (stored entries, ZIP64 when needed) and tells you when it's ready — download it within 7 days. Up to 100,000 files and 2 TB; bigger restores: the desktop app or the CLI. A path is a JSON list of names from the snapshot's root — the backed-up path, then folders (["C:/Users/dana","Documents"]); [] is the root.

Key scope: backup.read

Path parameters

idstringThe device's id.

Request body (JSON)

snapshot_idstringThe backup to restore from.
pathsarray of array of stringThe files and folders, each a JSON list of names.

A name ending in ? is optional.

Response 201

restoreBackupRestore
restore.idstring
restore.device_idstring
restore.snapshot_idstring
restore.namestringThe ZIP's name as downloaded.
restore.state"queued" | "running" | "ready" | "failed" | "canceled" | "expired"
restore.filesstring | integer
restore.bytesstringWhat the files hold.
restore.zip_bytesstringThe ZIP's exact size.
restore.done_bytesstringHow much of the ZIP is put together.
restore.pathsarray of array of string
restore.errorstring | null
restore.created_atdate-time
restore.finished_atdate-time | null
restore.expires_atdate-time | null
restore.downloaded_atdate-time | null
restore.user_idstringWho asked for it.

A name ending in ? is optional.

An error: no_backup, no_device, no_snapshot, no_path, bad_path, bad_paths, bad_snapshot, nothing_to_restore, zip_too_large, sealed, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/restores" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"snapshot_id":"…","paths":[["…"]]}'
POST/backup/devices/{id}/snapshotsMake a snapshot

After a run's packs are complete: the snapshot is the device as it was, from its root tree (tree_missing until the tree's pack has arrived). skipped lists files the client couldn't read (the snapshot is partial). pruned_during_run: old backups were cleaned up after the run began (its run_start heartbeat), so a blob it was told is stored may be gone — run again; nothing already sent is sent twice.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

root_treestringThe root tree blob's id.
started_at?string | nullWhen the run began.
files?string | integerFiles it covers.
dirs?string | integerFolders.
bytes?string | integer | stringBytes it covers.
new_bytes?string | integer | stringBytes this run stored.
new_blobs?string | integerBlobs this run stored.
paths?array of stringThe backed-up paths.
skipped?array of anyFiles not read, each {path, reason}.
trigger?string`first`, `scheduled` or `manual`.
client_version?string | nullThe client's version.

A name ending in ? is optional.

Response 201

snapshotBackupSnapshot
snapshot.idstring
snapshot.device_idstring
snapshot.root_treestringThe root tree blob's id: one entry per backed-up path, named by the full path.
snapshot.status"complete" | "partial"
snapshot.trigger"first" | "scheduled" | "manual"
snapshot.started_atdate-time
snapshot.finished_atdate-time
snapshot.filesstring | integer
snapshot.dirsstring | integer
snapshot.bytesstringEverything the snapshot covers.
snapshot.new_bytesstringWhat this run newly stored.
snapshot.new_blobsstring | integer
snapshot.pathsarray of string
snapshot.skippedarray of anyFiles the client couldn't read (path and reason).
snapshot.client_versionstring | null

A name ending in ? is optional.

An error: no_backup, no_device, read_only, paused, backup_role, bad_tree, tree_missing, bad_started_at, pruned_during_run, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/snapshots" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"root_tree":"…"}'
GET/backup/devices/{id}/snapshotsList a device's snapshots

Key scope: backup.read

Path parameters

idstringThe device's id.

Query parameters

limit?string | numberAt most this many (100 by default, up to 500).

Response 200

dataarray of BackupSnapshot
data.idstring
data.device_idstring
data.root_treestringThe root tree blob's id: one entry per backed-up path, named by the full path.
data.status"complete" | "partial"
data.trigger"first" | "scheduled" | "manual"
data.started_atdate-time
data.finished_atdate-time
data.filesstring | integer
data.dirsstring | integer
data.bytesstringEverything the snapshot covers.
data.new_bytesstringWhat this run newly stored.
data.new_blobsstring | integer
data.pathsarray of string
data.skippedarray of anyFiles the client couldn't read (path and reason).
data.client_versionstring | null

A name ending in ? is optional.

An error: no_backup, no_device, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/devices/{id}/snapshots" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/backup/devices/{id}/snapshots/{sid}/fileWhere one backed-up file's chunks are

One file of a backup, to put together on your side: each chunk's pack URL and byte range in order (compression: df = raw deflate), each chunk's BLAKE3 is its id. Up to 2 GB (use_zip past it). A path is a JSON list of names from the snapshot's root — the backed-up path, then folders (["C:/Users/dana","Documents"]); [] is the root.

Key scope: backup.read

Path parameters

idstringThe device's id.
sidstringThe snapshot's id.

Query parameters

pathstringThe file, as a JSON list of names.

Response 200

namestring
sizestring | integer
mtimedate-time
chunksarray of object
chunks.idstring
chunks.urlstring
chunks.offsetstring | integer
chunks.lengthstring | integer
chunks.compressionstring | null

A name ending in ? is optional.

An error: no_backup, no_device, no_snapshot, no_path, not_a_file, use_zip, bad_path, sealed, blob_missing, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/devices/{id}/snapshots/{sid}/file" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/backup/devices/{id}/snapshots/{sid}/treeList a folder of a backup

The files and folders in a folder of one of a device's backups: names, sizes, dates. A path is a JSON list of names from the snapshot's root — the backed-up path, then folders (["C:/Users/dana","Documents"]); [] is the root. A device backed up with a private key answers sealed (only its key reads it).

Key scope: backup.read

Path parameters

idstringThe device's id.
sidstringThe snapshot's id.

Query parameters

path?stringThe folder, as a JSON list of names.

Response 200

snapshotBackupSnapshot
snapshot.idstring
snapshot.device_idstring
snapshot.root_treestringThe root tree blob's id: one entry per backed-up path, named by the full path.
snapshot.status"complete" | "partial"
snapshot.trigger"first" | "scheduled" | "manual"
snapshot.started_atdate-time
snapshot.finished_atdate-time
snapshot.filesstring | integer
snapshot.dirsstring | integer
snapshot.bytesstringEverything the snapshot covers.
snapshot.new_bytesstringWhat this run newly stored.
snapshot.new_blobsstring | integer
snapshot.pathsarray of string
snapshot.skippedarray of anyFiles the client couldn't read (path and reason).
snapshot.client_versionstring | null
patharray of string
entriesarray of object
entries.namestring
entries.type"file" | "dir" | "symlink"
entries.sizestring | integer | null
entries.mtimedate-time
entries.target?string

A name ending in ? is optional.

An error: no_backup, no_device, no_snapshot, no_path, not_a_folder, bad_path, sealed, blob_missing, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/devices/{id}/snapshots/{sid}/tree" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/backup/devices/{id}/suspiciousReport ransomware-like changes

A backup client saw many files change in an unusual way (rewritten unreadable, ransomware extensions, ransom notes) and uploaded none of them: the device pauses (status suspicious) — its last good backup stays the latest — and its owner and a team's admins are alerted. Resuming it (PATCH paused: false, "It was me") clears it for a day.

Key scope: backup.write

Path parameters

idstringThe device's id.

Request body (JSON)

reasonstring`extensions`, `ransom_note` or `entropy`.
changedstring | integerHow many files changed.
sample?array of stringSome of their paths.
extensions?map of string | integerCounts per extension.

A name ending in ? is optional.

Response 200

deviceBackupDevice
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.client"cli" | "desktop" | "phone"
device.client_versionstring | null
device.volume_serialstring | null
device.status"ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious"
device.state"idle" | "running" | "error"
device.progressmap of any | null
device.pausedboolean
device.key_mode"sendflo" | "private"
device.suspiciousobject | null
device.suspicious.atdate-time
device.suspicious.reason?string
device.suspicious.changed?string | integer
device.suspicious.sample?array of string
device.changes_ok_untildate-time | null
device.policymap of any | null
device.bytesstringWhat the last complete snapshot covers.
device.filesstring | integer
device.last_snapshot_idstring | null
device.last_snapshot_atdate-time | null
device.last_seen_atdate-time | null
device.last_errorstring | null
device.last_error_atdate-time | null
device.created_atdate-time
device.minebooleanIt's the caller's own device (false: a team member's, seen by a team admin).
device.userobject
device.user.idstring
device.user.name?string
device.user.email?string

A name ending in ? is optional.

An error: no_backup, no_device, backup_role, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/devices/{id}/suspicious" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"reason":"…","changed":"…"}'
GET/backup/keysYour private key's wrapped copies

For a plan where you chose "Only me": your data key, wrapped by your passphrase (PBKDF2-SHA256) and by your recovery key — base64 of nonce | AES-256-GCM ciphertext | tag. SendFlo never holds it unwrapped. Your client unwraps it and checks it against key_id.

Key scope: backup.read

Query parameters

team_id?stringA team plan (your own plan when absent).

Response 200

key_idstring | null
keysarray of object
keys.kind"passphrase" | "recovery"
keys.wrappedstring
keys.kdfmap of any
keys.key_idstring
keys.created_atdate-time

A name ending in ? is optional.

An error: no_backup, not_on_team, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/keys" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/backup/keys
POST/backup/keysSet up your private key

Store a new data key's wrapped copies (by your passphrase and by your recovery key). One per person per plan: key_exists when you have one. Then register devices with key_mode: private; their chunks and folder trees are sealed on the device, and SendFlo can't recover the backup if both the passphrase and the recovery key are lost.

Key scope: backup.write

Request body (JSON)

team_id?string | nullA team plan.
key_idstringBLAKE3 derive_key("sendflo backup 2026-10-29 key id", key), hex.
passphraseobject{wrapped, kdf: {alg: pbkdf2-sha256, iterations ≥ 600000, salt}}.
passphrase.wrappedstring
passphrase.kdfmap of any
recoveryobject{wrapped, kdf: {alg: raw}}.
recovery.wrappedstring
recovery.kdfmap of any

A name ending in ? is optional.

Response 201

key_idstring
keysarray of object
keys.kindstring
keys.created_atdate-time

A name ending in ? is optional.

An error: no_backup, no_private_key, key_exists, bad_key, not_on_team, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/keys" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"key_id":"…","passphrase":{"wrapped":"…","kdf":{}},"recovery":{"wrapped":"…","kdf":{}}}'
PUT/backup/keys/passphraseChange your private key's passphrase

The same key, wrapped by a new passphrase (key_mismatch when key_id isn't this plan's key). The recovery key still opens it.

Key scope: backup.write

Request body (JSON)

team_id?string | nullA team plan.
key_idstringThe key's id.
wrappedstringThe key wrapped by the new passphrase.
kdfmap of anyIts PBKDF2 parameters.

A name ending in ? is optional.

Response 200

key_idstring
created_atdate-time

A name ending in ? is optional.

An error: no_backup, no_key, key_mismatch, bad_key, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X PUT "https://api.sendflo.app/v1/backup/keys/passphrase" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"key_id":"…","wrapped":"…","kdf":{}}'
DELETE/backup/packs/{upload}Cancel a pack's upload

Key scope: backup.write

Path parameters

uploadstringThe upload's id.

Response 200

cancelledtrue

A name ending in ? is optional.

An error: no_backup, no_upload, upload_closed, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/backup/packs/{upload}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/backup/packs/{upload}/completeFinish a pack's upload

SendFlo checks the parts the storage holds against the pack's size, then adds its blobs to the plan's index. Safe to call again.

Key scope: backup.write

Path parameters

uploadstringThe upload's id.

Response 200

pack_idstring
sizestring
blobsstring | integer

A name ending in ? is optional.

An error: no_backup, no_upload, upload_closed, read_only, parts_missing, size_mismatch, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/packs/{upload}/complete" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/backup/packs/{upload}/partsMore part URLs for a pack

Key scope: backup.write

Path parameters

uploadstringThe upload's id.

Request body (JSON)

partsarray of string | integerPart numbers (from 1).

A name ending in ? is optional.

Response 200

part_urlsarray of string

A name ending in ? is optional.

An error: no_backup, no_upload, upload_closed, bad_parts, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/backup/packs/{upload}/parts" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"parts":["…"]}'
GET/backup/restoresList your restores

Your ZIP restores, newest first, with each one's device; a team's owner and admins also see the restores of the team's devices.

Key scope: backup.read

Response 200

dataarray of object
data.idstring
data.device_idstring
data.snapshot_idstring
data.namestringThe ZIP's name as downloaded.
data.state"queued" | "running" | "ready" | "failed" | "canceled" | "expired"
data.filesstring | integer
data.bytesstringWhat the files hold.
data.zip_bytesstringThe ZIP's exact size.
data.done_bytesstringHow much of the ZIP is put together.
data.pathsarray of array of string
data.errorstring | null
data.created_atdate-time
data.finished_atdate-time | null
data.expires_atdate-time | null
data.downloaded_atdate-time | null
data.user_idstringWho asked for it.
data.device_namestring | null
data.mineboolean

A name ending in ? is optional.

An error: no_backup, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/restores" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/backup/restores
GET/backup/restores/{rid}Get a restore

Key scope: backup.read

Path parameters

ridstringThe restore's id.

Response 200

restoreBackupRestore
restore.idstring
restore.device_idstring
restore.snapshot_idstring
restore.namestringThe ZIP's name as downloaded.
restore.state"queued" | "running" | "ready" | "failed" | "canceled" | "expired"
restore.filesstring | integer
restore.bytesstringWhat the files hold.
restore.zip_bytesstringThe ZIP's exact size.
restore.done_bytesstringHow much of the ZIP is put together.
restore.pathsarray of array of string
restore.errorstring | null
restore.created_atdate-time
restore.finished_atdate-time | null
restore.expires_atdate-time | null
restore.downloaded_atdate-time | null
restore.user_idstringWho asked for it.

A name ending in ? is optional.

An error: no_restore, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/restores/{rid}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
DELETE/backup/restores/{rid}Cancel a restore

Stop a ZIP that's waiting or being put together.

Key scope: backup.read

Path parameters

ridstringThe restore's id.

Response 200

restoreBackupRestore
restore.idstring
restore.device_idstring
restore.snapshot_idstring
restore.namestringThe ZIP's name as downloaded.
restore.state"queued" | "running" | "ready" | "failed" | "canceled" | "expired"
restore.filesstring | integer
restore.bytesstringWhat the files hold.
restore.zip_bytesstringThe ZIP's exact size.
restore.done_bytesstringHow much of the ZIP is put together.
restore.pathsarray of array of string
restore.errorstring | null
restore.created_atdate-time
restore.finished_atdate-time | null
restore.expires_atdate-time | null
restore.downloaded_atdate-time | null
restore.user_idstringWho asked for it.

A name ending in ? is optional.

An error: no_restore, not_running, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/backup/restores/{rid}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/backup/restores/{rid}/downloadDownload a restore's ZIP

A short-lived URL for a ready ZIP (not_ready before; expired after its 7 days).

Key scope: backup.read

Path parameters

ridstringThe restore's id.

Response 200

urlstring
namestring
sizestring

A name ending in ? is optional.

An error: no_restore, not_ready, expired, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/restores/{rid}/download" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/backup/teams/{id}/locationDoes a team show its members' computers' locations

shows for anyone on the team. To its owner and admins while it does, each computer whose person shared it with the team, with its last place (the look is in the audit log).

Key scope: backup.read

Path parameters

idstringThe team plan's id.

Response 200

showsboolean
managesboolean
devicesarray of object
devices.device_idstring
devices.lastobject
devices.last.atdate-time
devices.last.latnumber
devices.last.lonnumber
devices.last.accuracy_kmstring | integer
devices.last.citystring | null
devices.last.regionstring | null
devices.last.countrystring | null
devices.last.source"ip" | "country"

A name ending in ? is optional.

An error: not_on_team, no_backup, no_team, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/teams/{id}/location" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET/backup/teams/{id}/policyA team's Backup policy

What every member's computer follows: required folders (desktop, documents, pictures), extra exclusions, an upload cap, whether members may pause or remove a device, and who restores (members, admins, or members through a backup app). The team's owner and admins only; changed in the app (two-factor).

Key scope: backup.read

Path parameters

idstringThe team plan's id.

Response 200

policyobject
policy.requiredarray of "desktop" | "documents" | "pictures"
policy.excludearray of string
policy.max_mbpsnumber | null
policy.may_pauseboolean
policy.may_removeboolean
policy.restore"members" | "admins" | "own_devices"

A name ending in ? is optional.

An error: team_admin, not_on_team, no_backup, no_team, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/backup/teams/{id}/policy" \
  -H "authorization: Bearer $SENDFLO_API_KEY"

encryption

Make the SDK or the CLI one of your encryption devices, to open end-to-end files you received.

POST/e2e/devicesAsk to become one of your encryption devices

The machine makes its own X-Wing key pair and sends the public half; the device waits (pending) until another of the person's devices approves it after both show the same six digits (code), or it unlocks itself with the recovery key. The person's other devices are told. A key asks as platform cli (422 platform_cli otherwise). At most five devices wait at once.

Key scope: encryption.devices

Request body (JSON)

labelstringWhat the person sees in Settings → Encryption ("CLI on build-box").
platform"web" | "desktop" | "ios" | "android" | "cli"`cli` for the SDK and the CLI.
public_keystringThe device's X-Wing public key, base64url (1216 bytes).

A name ending in ? is optional.

Response 201

idstring
labelstring
platform"web" | "desktop" | "ios" | "android" | "cli"
status"pending" | "active" | "revoked"
public_keystringbase64url
fingerprintstring
codestringThe six digits the person compares on the device that approves this one.
unlocked_via"setup" | "device" | "recovery" | "passkey" | null
approved_bystring | null
created_atdate-time
approved_atdate-time | null
revoked_atdate-time | null
enrolled_by_keybooleanAn API key or an app asked for it (the SDK or the CLI).

A name ending in ? is optional.

An error: no_keys, invalid_key, platform_cli, too_many_pending, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/e2e/devices" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"label":"…","platform":"web","public_key":"…"}'
GET/e2e/devicesList your encryption devices

Every device of the person, waiting, unlocked or removed — poll it to see when yours is approved.

Key scope: encryption.devices

Response 200

dataarray of E2eDevice
data.idstring
data.labelstring
data.platform"web" | "desktop" | "ios" | "android" | "cli"
data.status"pending" | "active" | "revoked"
data.public_keystringbase64url
data.fingerprintstring
data.codestringThe six digits the person compares on the device that approves this one.
data.unlocked_via"setup" | "device" | "recovery" | "passkey" | null
data.approved_bystring | null
data.created_atdate-time
data.approved_atdate-time | null
data.revoked_atdate-time | null
data.enrolled_by_keybooleanAn API key or an app asked for it (the SDK or the CLI).

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/e2e/devices" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/e2e/devices
DELETE/e2e/devices/{id}Forget a device this key enrolled

Removes the device and its sealed copy. A key removes only a device it enrolled; revoking the key removes them too. A device that was unlocked kept a copy of the identity key, so rotating keys in the app afterwards is recommended (rotate_recommended).

Key scope: encryption.devices

Path parameters

idstringThe device's id.

Response 200

oktrue
rotate_recommendedboolean

A name ending in ? is optional.

An error: not_found, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X DELETE "https://api.sendflo.app/v1/e2e/devices/{id}" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
POST/e2e/devices/{id}/unlockUnlock a waiting device with the recovery key

The device opened the recovery seal (GET /v1/e2e/seals?kind=recovery) with the person's recovery key and sends the identity seed sealed to its own key. A key unlocks only a device it enrolled, and only with the recovery key (422 recovery_only; passkeys are for browsers).

Key scope: encryption.devices

Path parameters

idstringThe waiting device's id.

Request body (JSON)

via"recovery" | "passkey"`recovery`.
credential?stringUnused for `recovery`.
sealedstringThe current identity seed sealed to the device's own key (HPKE, `SEAL_INFO.identity`).

A name ending in ? is optional.

Response 200

idstring
labelstring
platform"web" | "desktop" | "ios" | "android" | "cli"
status"pending" | "active" | "revoked"
public_keystringbase64url
fingerprintstring
codestringThe six digits the person compares on the device that approves this one.
unlocked_via"setup" | "device" | "recovery" | "passkey" | null
approved_bystring | null
created_atdate-time
approved_atdate-time | null
revoked_atdate-time | null
enrolled_by_keybooleanAn API key or an app asked for it (the SDK or the CLI).

A name ending in ? is optional.

An error: not_found, invalid_seal, recovery_only, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl -X POST "https://api.sendflo.app/v1/e2e/devices/{id}/unlock" \
  -H "authorization: Bearer $SENDFLO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"via":"recovery","sealed":"…"}'
GET/e2e/eventsYour key-event chain

Every change to the person's keys, oldest first, each hash covering the one before (SHA-256). Recompute it on the device to check nothing was changed or left out; the SDK's verifyKeyChain does.

Key scope: encryption.devices

Response 200

dataarray of object
data.seqstring | integer
data.kindstring
data.payloadmap of any
data.device_idstring | null
data.atdate-time
data.prev_hashstring
data.hashstring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/e2e/events" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/e2e/events
GET/e2e/keysYour encryption keys and devices

The person's identity key (null until they set up encryption in the app), their devices, whether a recovery key is set, the passkeys that unlock, and the head of the key-event chain.

Key scope: encryption.devices

Response 200

suitestring
user_idstringThe person the keys belong to (the key's maker).
identityE2eIdentity | null
identity.versionstring | integer
identity.suitestring`xwing-hpke1`: X-Wing (X25519 + ML-KEM-768) with HPKE (RFC 9180).
identity.public_keystringbase64url
identity.fingerprintstring
identity.created_atdate-time
devicesarray of E2eDevice
devices.idstring
devices.labelstring
devices.platform"web" | "desktop" | "ios" | "android" | "cli"
devices.status"pending" | "active" | "revoked"
devices.public_keystringbase64url
devices.fingerprintstring
devices.codestringThe six digits the person compares on the device that approves this one.
devices.unlocked_via"setup" | "device" | "recovery" | "passkey" | null
devices.approved_bystring | null
devices.created_atdate-time
devices.approved_atdate-time | null
devices.revoked_atdate-time | null
devices.enrolled_by_keybooleanAn API key or an app asked for it (the SDK or the CLI).
recoveryboolean
passkeysarray of string
chainobject | null
chain.seqstring | integer
chain.hashstring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/e2e/keys" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/e2e/keys
GET/e2e/sealsThe sealed copies a device needs

?device=<id>: the identity seeds sealed to that unlocked device (a key reads only a device it enrolled), plus the history — each older version sealed to the next, so the newest seed opens every older one. ?kind=recovery: the copy sealed to the recovery key, to unlock a waiting device. Opening them happens on the device.

Key scope: encryption.devices

Query parameters

device?stringAn unlocked device's id.
kind?"recovery" | "passkey"`recovery` (a key can't read passkey copies).
credential?stringWith `kind=passkey`, from a browser only.

Response 200

identitiesarray of E2eIdentity
identities.versionstring | integer
identities.suitestring`xwing-hpke1`: X-Wing (X25519 + ML-KEM-768) with HPKE (RFC 9180).
identities.public_keystringbase64url
identities.fingerprintstring
identities.created_atdate-time
sealsarray of object
seals.versionstring | integer
seals.kindstring
seals.targetstring
seals.sealedstringbase64url: HPKE enc ‖ ciphertext of a 32-byte seed.
historyarray of object
history.versionstring | integer
history.kindstring
history.targetstring
history.sealedstringbase64url: HPKE enc ‖ ciphertext of a 32-byte seed.

A name ending in ? is optional.

An error: not_found, invalid_query, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/e2e/seals" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/e2e/seals
GET/e2e/team-sealsThe team keys sealed to you

Each team key the person holds (a team inbox's key, or the team's recovery key for its owner and admins), sealed to their identity key, with the key's public half — what a device needs to open a team inbox's encrypted files or a recovery copy. A key the person no longer holds has no copy.

Key scope: encryption.devices

Response 200

dataarray of object
data.team_key_idstring
data.plan_idstring
data.kind"recovery" | "inbox"
data.ref_idstring | null
data.versionstring | integer
data.public_keystring
data.fingerprintstring
data.identity_versionstring | integer
data.sealedstring

A name ending in ? is optional.

An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).

curl "https://api.sendflo.app/v1/e2e/team-seals" \
  -H "authorization: Bearer $SENDFLO_API_KEY"
GET /v1/e2e/team-seals

Webhook events

Register an endpoint with POST /v1/webhooks (subscribe to a name, a family like file.*, or *). Each delivery is a JSON { id, type, created_at, data }, signed the Standard Webhooks way (webhook-id, webhook-timestamp, webhook-signature) with the secret you got once at registration — any Standard Webhooks library verifies it, or use this. Until 28 April 2027 the old sendflo-signature header comes too. Every attempt is in the endpoint's delivery log for 30 days, where you can Resend it.

// Node 18+ — verify before trusting a delivery (Standard Webhooks).
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySendflo(rawBody: string, headers: Record<string, string | undefined>, secret: string): boolean {
  const id = headers["webhook-id"];
  const ts = Number(headers["webhook-timestamp"]);
  if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) return false; // older than 5 minutes
  // The key is the secret after "whsec_", base64-decoded (older secrets use base64url; both decode).
  const key = Buffer.from(secret.replace(/^whsec_/, "").replace(/-/g, "+").replace(/_/g, "/"), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
  // Several signatures may come, space-separated ("v1,<base64>"): any one matching is enough.
  return String(headers["webhook-signature"] ?? "")
    .split(" ")
    .some((part) => {
      const given = Buffer.from(part.startsWith("v1,") ? part.slice(3) : "", "base64");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
}
access.downloadedA recipient downloaded a file

A download session ended — details.complete says whether every byte arrived. Team Pro and Team Enterprise only; a team-wide endpoint (team: true) receives every member's.

data

event_idstringThe audit log's event id (UUIDv7) — the same row GET /v1/audit returns.
occurred_atdate-time
action"LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD"
outcome"success" | "failure" | "denied"
link_idstring | null
file_idstring | null
file_namestring | null
sender_user_idstring | null
recipientobject
recipient.kind"user" | "recipient" | "guest"
recipient.emailstring | null
recipient.verifiedbooleanThe address was proved with a code (or is a signed-in account's own).
recipient.user_idstring | null
ipstring | null
ip_source"cloudflare" | "claimed" | "direct" | "internal" | "none" | null
networkobject | null
network.asnstring | integer | null
network.orgstring | null
network.classstring | null
countrystring | null
devicestring | null
session_idstring | null
detailsmap of anyBytes and whether the download completed, the preview kind, …

A name ending in ? is optional.

access.openedA recipient opened a link

Someone opened a link's page (verified, emailed or anonymous). Team Pro and Team Enterprise only; a team-wide endpoint (team: true) receives every member's.

data

event_idstringThe audit log's event id (UUIDv7) — the same row GET /v1/audit returns.
occurred_atdate-time
action"LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD"
outcome"success" | "failure" | "denied"
link_idstring | null
file_idstring | null
file_namestring | null
sender_user_idstring | null
recipientobject
recipient.kind"user" | "recipient" | "guest"
recipient.emailstring | null
recipient.verifiedbooleanThe address was proved with a code (or is a signed-in account's own).
recipient.user_idstring | null
ipstring | null
ip_source"cloudflare" | "claimed" | "direct" | "internal" | "none" | null
networkobject | null
network.asnstring | integer | null
network.orgstring | null
network.classstring | null
countrystring | null
devicestring | null
session_idstring | null
detailsmap of anyBytes and whether the download completed, the preview kind, …

A name ending in ? is optional.

access.previewedA recipient previewed a file

Someone viewed a file in the browser. Team Pro and Team Enterprise only; a team-wide endpoint (team: true) receives every member's.

data

event_idstringThe audit log's event id (UUIDv7) — the same row GET /v1/audit returns.
occurred_atdate-time
action"LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD"
outcome"success" | "failure" | "denied"
link_idstring | null
file_idstring | null
file_namestring | null
sender_user_idstring | null
recipientobject
recipient.kind"user" | "recipient" | "guest"
recipient.emailstring | null
recipient.verifiedbooleanThe address was proved with a code (or is a signed-in account's own).
recipient.user_idstring | null
ipstring | null
ip_source"cloudflare" | "claimed" | "direct" | "internal" | "none" | null
networkobject | null
network.asnstring | integer | null
network.orgstring | null
network.classstring | null
countrystring | null
devicestring | null
session_idstring | null
detailsmap of anyBytes and whether the download completed, the preview kind, …

A name ending in ? is optional.

agent_task.canceledA task was canceled

The task was canceled (canceled).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

drive.item.addedA Drive file or folder was added

An upload finished or a folder was made in a space.

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.changedA Drive file has a new version

Its content changed (an upload over it, a version restored).

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.renamedA Drive file or folder was renamed

Its name changed.

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.movedA Drive file or folder was moved

It went to another folder.

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.trashedA Drive file or folder went to the trash

Someone deleted it (it can be restored for the plan's days).

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.restoredA Drive file or folder came back from the trash

Its owner restored it.

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.item.deletedA Drive file or folder was deleted for good

It left the trash for good (by hand, or its days ran out).

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
actor_user_idstring | null

A name ending in ? is optional.

drive.sharedA Drive file or folder was shared

Someone gave a person, an address, a group or the team access to it.

data

itemDriveItemV2
item.idstring
item.kind"file" | "folder"
item.namestring | null
item.parent_idstring | null
item.version_idstring | null
item.spaceobject
item.space.idstring
item.space.kind"personal" | "team"
item.seqstringThe change's place in the space's journal (GET /v1/drive/changes).
rolestring
granteeobject
grantee.kind"user" | "email" | "group" | "team"
grantee.idstring | null
actor_user_idstring | null

A name ending in ? is optional.

backup.device.addedA device started backing up

A computer, server, phone or external drive was added to a Backup plan.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer

A name ending in ? is optional.

backup.device.removedA device was removed from Backup

Its owner (or a team admin) removed it; its backups are kept until retention ends.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer

A name ending in ? is optional.

backup.snapshot.createdA backup finished

A device's backup run made its snapshot.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer
snapshotobject
snapshot.idstring
snapshot.status"complete" | "partial"
snapshot.triggerstring
snapshot.filesstring | integer
snapshot.bytesstring
snapshot.new_bytesstring
snapshot.started_atdate-time
snapshot.finished_atdate-time

A name ending in ? is optional.

backup.device.overdueA device stopped backing up

No backup for 72 hours (or three of its scheduled runs) — once per episode.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer
errorstring | null

A name ending in ? is optional.

backup.device.failingA device's backup failed

Its last run stopped with an error — once per error.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer
errorstring | null

A name ending in ? is optional.

backup.restore.readyA restore is ready

A ZIP of files from a backup was put together; it can be downloaded for 7 days.

data

restoreobject
restore.idstring
restore.device_idstring
restore.namestring
restore.filesstring | integer
restore.zip_bytesstring
restore.expires_atdate-time | null
restore.user_idstring

A name ending in ? is optional.

backup.device.suspiciousA device's backup saw ransomware-like changes

Many files changed in an unusual way (rewritten unreadable, ransomware extensions or notes): nothing was uploaded and the device's backup paused until its owner resumes it.

data

deviceBackupDeviceV2
device.idstring
device.namestring
device.kind"computer" | "server" | "phone" | "volume"
device.osstring | null
device.user_idstringWhose device it is.
device.statusstring | null
device.last_snapshot_atdate-time | null
device.bytesstringWhat its last backup covers.
device.filesstring | integer
reportobject
report.reasonstring
report.changedstring | integer
report.samplearray of string
report.extensionsmap of string | integer

A name ending in ? is optional.

agent_task.completedA task completed

The task finished (completed).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.createdA task was sent to an agent

A task to an agent on a computer, or to a cloud agent, is created (not when one is run again).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time

A name ending in ? is optional.

agent_task.expiredA task expired

Nobody took the task in time (expired).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.failedA task failed

The task failed (failed): the agent's error, a refused URL, or no progress.

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.needs_approvalA task waits for approval

The computer's owner must approve the task first (awaiting_approval).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.needs_inputThe agent asked a question

The task waits for an answer (input_required); task.question holds it.

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.rejectedA task was declined

The computer's owner declined the task, or its files (rejected).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

agent_task.startedThe agent took a task

The task is handed to the agent (handed_off).

data

taskAgentTaskA task handed to an AI agent on a computer or in the cloud.
task.idstring
task.state"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired"
task.labelstringThe state in plain words ("Done", "Needs your answer"…).
task.target_kind"device" | "cloud"
task.agentobject
task.agent.refstring
task.agent.namestring
task.agent.runnerstringfolder, command, acp, channel, session, webhook, a2a, routine or pull.
task.deviceobject | null
task.device.idstring
task.device.namestring | null
task.project_idstring | null
task.cloud_agent_idstring | null
task.session_refstring | null
task.fromobject
task.from.namestring
task.from.user_idstring
task.from.device_idstring | null
task.instructionstring | null
task.instruction_encryptedboolean
task.presetstring | null
task.voice_filestring | null
task.filesarray of object
task.files.namestring
task.files.sizestring | integer
task.files.sha256?string
task.files.mime?string
task.total_bytesstringBytes, as a decimal string.
task.reasonstring | null
task.detailstring | null
task.questionAgentTaskQuestion | null
task.question.idstring
task.question.kind"question" | "permission" | "approval"
task.question.textstring
task.question.options?array of object
task.question.free_text?boolean
task.question.asked_atdate-time
task.question.answer?object
task.resultAgentTaskResult | null
task.result.summarystring | null
task.result.filesarray of object
task.result.kept_atstring | null
task.result.expires_atdate-time | null
task.result.data?anyFree-form JSON the agent returned.
task.policyobject | null
task.policy.ask_first?boolean
task.policy.direct_only?boolean
task.policy.results?"send" | "keep"
task.device_transfer_idstring | null
task.transfer_statestring | null
task.cancel_requestedboolean
task.retry_ofstring | null
task.role"sender" | "owner" | "both" | null
task.actionsarray of "approve" | "reject" | "answer" | "cancel" | "retry"What you may do now.
task.attemptsstring | integer
task.created_atdate-time
task.updated_atdate-time
task.delivered_atdate-time | null
task.started_atdate-time | null
task.finished_atdate-time | null
task.expires_atdate-time
from"submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required"
actor"sender" | "owner" | "server" | "connector" | "agent" | "staff" | "system"

A name ending in ? is optional.

credits.lowThe credit balance is low

A credit-billed workspace drops to 500 credits or less; once, until the balance goes back above 500.

data

balancestringCredits, as a decimal string (can be negative).
thresholdstring

A name ending in ? is optional.

credits.topped_upCredits were added

A credit pack bought for the API & Agents workspace is paid.

data

amountstringCredits added.
balancestring
centsstring | integerThe pack's price in US cents, before tax.
transaction_idstringPaddle's transaction id (it's on the receipt).

A name ending in ? is optional.

destination.deliveredA file arrived at a destination

A file is delivered to a team destination that notifies on arrival.

data

destinationstringThe destination's slug.
file_idstring
file_namestring

A name ending in ? is optional.

feedback.approval.setA recipient approved a file or asked for changes

A recipient approves a file or asks for changes (withdrawing sends nothing).

data

transferobject
transfer.idstringThe send's first link.
transfer.slugstring
fileobject
file.link_idstring
file.slugstring | null
file.namestring | null
approval"approved" | "changes"
authorobject
author.namestring
author.emailstring
author.proof"account" | "access" | "email"

A name ending in ? is optional.

feedback.comment.createdA recipient commented

A recipient comments or replies on a send (not the sender's own comments).

data

transferobject
transfer.idstringThe send's first link.
transfer.slugstring
fileobject
file.link_idstring
file.slugstring | null
file.namestring | null
commentobject
comment.idstring
comment.parent_idstring | null
comment.bodystring | null
comment.encryptedboolean
comment.anchorFeedbackAnchor | null
comment.created_atdate-time
authorobject
author.namestring
author.emailstring
author.proof"account" | "access" | "email"

A name ending in ? is optional.

feedback.rating.setA recipient rated a file

A recipient rates a file or changes the rating (clearing it sends nothing).

data

transferobject
transfer.idstringThe send's first link.
transfer.slugstring
fileobject
file.link_idstring
file.slugstring | null
file.namestring | null
ratingobject
rating.valuestring | integer
rating.scale"stars5" | "score10"
authorobject
author.namestring
author.emailstring
author.proof"account" | "access" | "email"

A name ending in ? is optional.

feedback.transfer.approvedEvery file of a send is approved

An approval leaves every live file of the send approved with no change requests (sent again on each later approval while that holds).

data

transferobject
transfer.idstringThe send's first link.
transfer.slugstring
filesstring | integerThe send's live files.

A name ending in ? is optional.

file.flaggedThe malware scan blocked a file

A file, a collection item or an agent task's result is blocked by the malware scan (on upload, the daily re-check or a rescan).

data

file_idstringThe file's id (for `subject: agent`, the task's id).
subject"file" | "asset" | "agent" | "drive"
namestring
severity"low" | "medium" | "high" | "critical"
signaturestring
whole_send_blockedboolean

A name ending in ? is optional.

file.releasedA blocked file was marked safe

SendFlo staff, the owner or a team admin releases a blocked file.

data

file_idstring
subject"file" | "asset" | "agent" | "drive"
namestring
by"staff" | "owner" | "team_admin"
reasonstring

A name ending in ? is optional.

file_request.fulfilledA file arrived on an upload page

Each file someone uploads through a file request or upload page, once its malware scan has finished (at most 2 minutes after it arrived; scan says which). Also sent to the page's own notify.webhook_url, and to a team's team-wide endpoints.

data

file_request_idstring
slugstring
linkobject
link.idstring
link.namestring | null
link.kind"request" | "reception"
fileobject
file.idstring
file.namestring
file.sizestringBytes, as a decimal string.
inbox_item_idstring
fromstring | null
from_emailstring | null
answersarray of object
answers.idstring
answers.labelstring
answers.valuestring
answers_v2array of objectThe same answers typed (DZ4). `answers` keeps them as text.
answers_v2.idstring
answers_v2.labelstring
answers_v2.typestringshort_text, long_text, email, number, date, multiple_choice, opinion_scale, phone, yes_no or consent.
answers_v2.valueanyThe answer as its type: text, a number, a YYYY-MM-DD day, a list of choices (several answers), true/false (yes/no), or { agreed: true, at } (consent).
contactobject | null
contact.namestring
contact.emailstring
contact.email_verifiedboolean
submission_idstring | null
retention_daysstring | integer
expires_atdate-time
ad_supportedboolean
urlstringThe received file in SendFlo.
scanobjectThe file's scan when the event was sent. `pending`: still being checked after 2 minutes (a rejection after that comes as `file_request.rejected`).
scan.state"clean" | "flagged" | "released" | "failed" | "not_scannable" | "pending" | "rejected" | "deleted"
scan.severitystring | null
scan.signaturestring | null

A name ending in ? is optional.

file_request.rejectedA file uploaded to a page was rejected

Malware scanning flagged a file uploaded to a page set to reject such files: it was deleted and doesn't count toward the page's limits. Sent instead of file_request.fulfilled, or after it when the scan finished later. Also sent to the page's own webhook.

data

file_request_idstring
slugstring
linkobject
link.idstring
link.namestring | null
link.kind"request" | "reception"
fileobject
file.idstring | null
file.namestring
file.sizestring
inbox_item_idstring
fromstring | null
from_emailstring | null
submission_idstring | null
urlstring
scanobjectThe file's scan when the event was sent. `pending`: still being checked after 2 minutes (a rejection after that comes as `file_request.rejected`).
scan.state"clean" | "flagged" | "released" | "failed" | "not_scannable" | "pending" | "rejected" | "deleted"
scan.severitystring | null
scan.signaturestring | null

A name ending in ? is optional.

file_request.submission_completedAn upload to a page is complete

Once per upload (all the files someone sent together), after every file's file_request.fulfilled or file_request.rejected: when the last file arrived, or 10 minutes after the latest one if the rest never came. Also sent to the page's own webhook.

data

submission_idstring
file_request_idstring
slugstring
linkobject
link.idstring
link.namestring | null
link.kind"request" | "reception"
fromstring | null
from_emailstring | null
contactobject | null
contact.namestring
contact.emailstring
contact.email_verifiedboolean
answersarray of object
answers.idstring
answers.labelstring
answers.valuestring
answers_v2array of object
answers_v2.idstring
answers_v2.labelstring
answers_v2.typestring
answers_v2.valueany
filesarray of object
files.inbox_item_idstring
files.file_idstring | null
files.namestring
files.sizestring
files.scanobjectThe file's scan when the event was sent. `pending`: still being checked after 2 minutes (a rejection after that comes as `file_request.rejected`).
files.scan.state"clean" | "flagged" | "released" | "failed" | "not_scannable" | "pending" | "rejected" | "deleted"
files.scan.severitystring | null
files.scan.signaturestring | null
files.urlstring
countsobject
counts.filesstring | integer
counts.rejectedstring | integer
counts.checkingstring | integerFiles still being checked.
urlstring

A name ending in ? is optional.

key.anomalyAn API key or an app behaved abnormally

A key or an app the person allowed spends over 5,000 credits in an hour (the key is suspended, the app's access revoked) or fails over half of 20+ requests in an hour; once per key or app, kind and hour.

data

api_key_idstringThe key's id, or for an app its grant's (Connected apps).
kind"spend_velocity" | "error_rate" | "content_probe"
detailstring
suspendedboolean
subject"api_key" | "app"
app_namestring | null

A name ending in ? is optional.

transfer.createdA transfer was sent

The first link of a send exists (one file, or the first of a bundle).

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

transfer.completedA transfer is complete

Every file of the send is stored and its links exist: no new file joined the send for 30 seconds.

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

transfer.pausedA transfer was paused

The sender (or their team) paused the whole send.

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

transfer.resumedA paused transfer was resumed

The sender resumed it.

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

transfer.revokedA transfer was revoked

Every file of the send was revoked (by the sender, a takedown or a policy).

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

transfer.expiredA transfer expired

The send's last live link expired.

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.

A name ending in ? is optional.

link.createdA link was created

A link to a file exists (each file of a send has one).

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).

A name ending in ? is optional.

link.openedA link's page was opened

Someone opened the link's page (on every plan; the team access.* stream stays the audit-grade feed).

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).
whoWebhookWho
who.emailstring | null
who.via"verified" | "emailed" | "sender" | "anonymous"
who.countrystring | null

A name ending in ? is optional.

link.downloadedA file was downloaded

A download through the link ended; complete says whether every byte arrived.

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).
whoWebhookWho
who.emailstring | null
who.via"verified" | "emailed" | "sender" | "anonymous"
who.countrystring | null
bytesstring
completeboolean

A name ending in ? is optional.

link.download_limit_reachedA link reached its download limit

The last allowed download was counted; the link answers 410 from now on.

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).
max_downloadsstring | integer

A name ending in ? is optional.

link.revokedA link was revoked

The link was revoked (by the sender, a takedown, a policy or open-once).

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).

A name ending in ? is optional.

link.expiredA link expired

The link passed its expiry.

data

linkWebhookLink
link.idstring
link.slugstring
link.transfer_idstringThe send's first link.
link.file_idstring
link.file_namestring"Encrypted file.<ext>" when the name is sealed.
link.sizestringBytes, as a decimal string.
link.encryptedboolean
link.accessstring
link.expires_atdate-time | null
link.urlstringThe link in My Links (sign-in needed).

A name ending in ? is optional.

recipient.addedSomeone was added to a transfer

The sender emailed more people or replaced one (Restricted sends list them).

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.
emailstring
replacedstring | null

A name ending in ? is optional.

recipient.removedSomeone's access was removed

The sender removed a person from some or all of a send's files.

data

transferWebhookTransfer
transfer.idstringThe send's first link.
transfer.slugstring
transfer.file_countstring | integer
transfer.subjectstring | null
transfer.encryptedboolean
transfer.accessstringpublic, tracked or restricted.
transfer.expires_atdate-time | null
transfer.urlstringThe send in SendFlo (sign-in needed); never a download.
emailstring
link_idsarray of string

A name ending in ? is optional.

file_request.createdAn upload page or request was created

A file request or reception link exists.

data

file_requestWebhookRequest
file_request.idstring
file_request.slugstring
file_request.namestring | null
file_request.kind"request" | "reception"
file_request.urlstringThe upload page.

A name ending in ? is optional.

file_request.closedA request was closed

A request got every file it asked for, or its owner closed it.

data

file_requestWebhookRequest
file_request.idstring
file_request.slugstring
file_request.namestring | null
file_request.kind"request" | "reception"
file_request.urlstringThe upload page.
reason"fulfilled" | "cancelled"

A name ending in ? is optional.

file_request.expiredA request expired

A request or upload page passed its expiry.

data

file_requestWebhookRequest
file_request.idstring
file_request.slugstring
file_request.namestring | null
file_request.kind"request" | "reception"
file_request.urlstringThe upload page.

A name ending in ? is optional.

inbox_item.receivedA file was received

A file arrived in Received (an upload page, a request, a reply), before its scan settles.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

inbox_item.readyA received file is ready

Its malware scan came back clean (or it can't be scanned: end-to-end).

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

inbox_item.flaggedA received file was flagged

Its malware scan flagged it; it can't be opened until released.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).
severitystring | null
signaturestring | null

A name ending in ? is optional.

inbox_item.releasedA flagged file was released

Its owner (or a team admin) marked it safe.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

inbox_item.viewedA received file was previewed

Someone of the workspace previewed it.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

inbox_item.downloadedA received file was downloaded

Someone of the workspace downloaded it.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

inbox_item.deletedA received file was deleted

Its owner deleted it, or it expired.

data

itemWebhookReceivedItem
item.idstring
item.file_idstring
item.file_namestring
item.sizestring
item.from_emailstring | null
item.file_request_idstring | null
item.received_atdate-time
item.urlstringReceived, in SendFlo (sign-in needed).

A name ending in ? is optional.

intake.submittedFiles arrived in a secure intake

An upload into the intake finished (its scan may still be running).

data

intakeWebhookIntake
intake.idstring
intake.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
intake.external_refsmap of any | null
intake.countstring | integerHow many files arrived.
intake.filesarray of object
intake.files.item_idstring
intake.files.typestring | null
intake.files.sizestringBytes, as a decimal string.
intake.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
intake.files.namestring"Collected file.<ext>" unless the intake shares names.
intake.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
intake.answersarray of objectOnly the answers the intake shares with the bot.
intake.answers.idstring
intake.answers.labelstring
intake.answers.valuestring
intake.handed_toarray of object
intake.handed_to.idstring
intake.handed_to.kind"user" | "group"
intake.handed_to.grantee_idstring
intake.handed_to.emailstring | null
intake.handed_to.permission"view" | "manage"
intake.handed_to.expires_atdate-time | null
intake.expires_atdate-time | null
intake.submitted_atdate-time | null
intake.ready_atdate-time | null
intake.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

intake.readyA secure intake is ready

Every file is stored and scanned clean, and no upload is still running: when bots and people should act.

data

intakeWebhookIntake
intake.idstring
intake.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
intake.external_refsmap of any | null
intake.countstring | integerHow many files arrived.
intake.filesarray of object
intake.files.item_idstring
intake.files.typestring | null
intake.files.sizestringBytes, as a decimal string.
intake.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
intake.files.namestring"Collected file.<ext>" unless the intake shares names.
intake.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
intake.answersarray of objectOnly the answers the intake shares with the bot.
intake.answers.idstring
intake.answers.labelstring
intake.answers.valuestring
intake.handed_toarray of object
intake.handed_to.idstring
intake.handed_to.kind"user" | "group"
intake.handed_to.grantee_idstring
intake.handed_to.emailstring | null
intake.handed_to.permission"view" | "manage"
intake.handed_to.expires_atdate-time | null
intake.expires_atdate-time | null
intake.submitted_atdate-time | null
intake.ready_atdate-time | null
intake.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

intake.rejectedA secure intake was rejected

The malware scan blocked a file it collected.

data

intakeWebhookIntake
intake.idstring
intake.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
intake.external_refsmap of any | null
intake.countstring | integerHow many files arrived.
intake.filesarray of object
intake.files.item_idstring
intake.files.typestring | null
intake.files.sizestringBytes, as a decimal string.
intake.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
intake.files.namestring"Collected file.<ext>" unless the intake shares names.
intake.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
intake.answersarray of objectOnly the answers the intake shares with the bot.
intake.answers.idstring
intake.answers.labelstring
intake.answers.valuestring
intake.handed_toarray of object
intake.handed_to.idstring
intake.handed_to.kind"user" | "group"
intake.handed_to.grantee_idstring
intake.handed_to.emailstring | null
intake.handed_to.permission"view" | "manage"
intake.handed_to.expires_atdate-time | null
intake.expires_atdate-time | null
intake.submitted_atdate-time | null
intake.ready_atdate-time | null
intake.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).
reason"scan"

A name ending in ? is optional.

intake.expiredA secure intake expired

It passed its expiry with nothing uploaded.

data

intakeWebhookIntake
intake.idstring
intake.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
intake.external_refsmap of any | null
intake.countstring | integerHow many files arrived.
intake.filesarray of object
intake.files.item_idstring
intake.files.typestring | null
intake.files.sizestringBytes, as a decimal string.
intake.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
intake.files.namestring"Collected file.<ext>" unless the intake shares names.
intake.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
intake.answersarray of objectOnly the answers the intake shares with the bot.
intake.answers.idstring
intake.answers.labelstring
intake.answers.valuestring
intake.handed_toarray of object
intake.handed_to.idstring
intake.handed_to.kind"user" | "group"
intake.handed_to.grantee_idstring
intake.handed_to.emailstring | null
intake.handed_to.permission"view" | "manage"
intake.handed_to.expires_atdate-time | null
intake.expires_atdate-time | null
intake.submitted_atdate-time | null
intake.ready_atdate-time | null
intake.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).

A name ending in ? is optional.

intake.handed_offA secure intake was handed to someone

A team member or group was given its files (when it became ready, or later).

data

intakeWebhookIntake
intake.idstring
intake.status"open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed"
intake.external_refsmap of any | null
intake.countstring | integerHow many files arrived.
intake.filesarray of object
intake.files.item_idstring
intake.files.typestring | null
intake.files.sizestringBytes, as a decimal string.
intake.files.scanstringpending, clean, flagged, released, not_scannable or rejected.
intake.files.namestring"Collected file.<ext>" unless the intake shares names.
intake.uploader_verifiedbooleanThe uploader proved their email (never the address itself).
intake.answersarray of objectOnly the answers the intake shares with the bot.
intake.answers.idstring
intake.answers.labelstring
intake.answers.valuestring
intake.handed_toarray of object
intake.handed_to.idstring
intake.handed_to.kind"user" | "group"
intake.handed_to.grantee_idstring
intake.handed_to.emailstring | null
intake.handed_to.permission"view" | "manage"
intake.handed_to.expires_atdate-time | null
intake.expires_atdate-time | null
intake.submitted_atdate-time | null
intake.ready_atdate-time | null
intake.urlstringThe intake's files in SendFlo, for the people they're handed to (sign-in needed).
toobject
to.kind"user" | "group"
to.idstring
to.permission"view" | "manage"

A name ending in ? is optional.