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 machinecode. - 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) andwebhook-signature: v1,<base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw body>">, keyed with the secret afterwhsec_, base64-decoded. Reject a timestamp more than 5 minutes off. Until 28 April 2027 the oldsendflo-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)
| name | string | The file name. |
| sealed_name? | string | End-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`). |
| size | string | integer | The exact size in bytes; the finished upload must match. |
| expires_at? | string | The expiry you'll give its link (clamped to the plan); it picks the storage tier. |
| overage_ad? | boolean | For the signed-in app (an ad was watched to go over quota). |
| overage_ad_nonce? | string | For the signed-in app (the ad's credit). |
| overage_ad_batch? | string | For the signed-in app (one ad for a multi-file send). |
| transfer_total_bytes? | string | integer | The whole send's size, so an oversize send is refused before its first byte. |
| intent? | object | The 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? | object | For `origin: watch`: the folder `{ id, label, device_id?, destination? }`. |
| watch.id | string | |
| watch.label | string | |
| watch.device_id? | string | null | |
| watch.destination? | string | null | |
| watch.delivery_id? | string | null | |
| approval? | object | For the signed-in app (an upload for an approval request). |
| approval.id | string | |
| approval.entry_id | string | |
| file_keys? | array of object | For 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.id | string | |
| file_keys.version | string | integer | |
| file_keys.sealed | string |
A name ending in ? is optional.
Response 200
| id | string | |
| name | string | |
| size | string | integer | The size you declared (a number here; byte counts are strings elsewhere). |
| state | "open" | |
| storage_class | string | |
| placement | object | |
| placement.tier | "pool" | "flat" | "fallback" | |
| placement.reason | string | |
| placement.pool_multiple | number | null | |
| placement.eligible | boolean | null | |
| placement.eligibility_reason | string | null | |
| upload | object | |
| upload.upload_id | string | |
| upload.part_size | string | integer | |
| upload.part_urls | array 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
| id | string | The upload's id from POST /v1/transfers. |
Request body (JSON)
| parts | array of object | Required but not read: send `[]`. |
| parts.part_number | string | integer | |
| parts.etag | string | |
| blake3? | string | The file's BLAKE3 hash, if you have it. |
| client_ref? | string | Your own reference for this upload (64 characters at most). |
A name ending in ? is optional.
Response 200
| id | string | |
| state | "complete" | |
| file_id | string | |
| size | string | |
| approval? | object | |
| approval.id | string | |
| approval.status | string |
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":"…"}]}'links
Share links for stored files: create, list, change, revoke.
POST/forwardsForward a received link without downloading it
Copies the link's file into your workspace (billed like an upload; the copy finishes in the background, file.copying) and makes a new link to it with the same rules as POST /v1/links. The new link never gives more than the original: it keeps the original's delivery mode unless you ask for a narrower one (422 delivery_not_allowed otherwise), and a password-protected original keeps a password (yours in link.password, or the original's). On economy storage the forward uses one of the original link's downloads (source.consumed_download). The sender sees the forward; your account is named only on links that verify their recipients. On Free with ads on, it needs an ad credit a key can't get (402 ad_required); an API & Agents workspace out of credits gets 402 insufficient_credits.
Key scope: links.write
Request body (JSON)
| slug | string | The received link's slug (the last part of its URL). |
| password? | string | The link's password, when it has one. |
| email? | string | The recipient email, on a link that asks for one. |
| viewer_token? | string | A tracked or restricted link's proof of recipient (from its email verification). |
| r? | string | A per-recipient token from the link's URL (`?r=`). |
| link? | object | The new link's options: any POST /v1/links field except `file_id` and `encrypted` (`{}` 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_id | string | |
| link.e2e_recovery.sealed | string | |
| 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.enabled | boolean | |
| link.restore.duration_days? | string | integer | |
| ad_nonce? | string | For the signed-in app (a Free plan's ad credit). |
| overage_ad? | boolean | For the signed-in app (an ad was watched to go over quota). |
| overage_ad_nonce? | string | For the signed-in app (that ad's credit). |
A name ending in ? is optional.
Response 200
| link | LinkCreated | A new link (or, for a retried create, the earlier one with `replayed: true`). |
| link.id | string | |
| link.slug | string | |
| link.url | string | The download page, https://sendflo.app/l/{slug}. |
| link.file_id | string | |
| link.has_password | boolean | |
| link.expires_at | date-time | null | |
| link.max_downloads | string | integer | null | |
| link.download_count | string | integer | |
| link.note | string | null | |
| link.subject | string | null | |
| link.require_email | boolean | |
| link.access | "public" | "tracked" | "restricted" | |
| link.allowed_emails | array of string | |
| link.encrypted | boolean | |
| link.e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| link.e2e_each | boolean | Mark 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_label | string | null | |
| link.viewer_mark | boolean | Each 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_guests | boolean | Guests 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_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| link.viewers_invite | boolean | Viewers 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_mark | boolean | Download 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_id | string | null | |
| link.available_at | date-time | null | |
| link.notify_on_download | boolean | |
| link.feedback | FeedbackSettings | The transfer's feedback settings. |
| link.feedback.comments | "off" | "open" | "private" | |
| link.feedback.ratings | boolean | |
| link.feedback.favorites | boolean | |
| link.feedback.approvals | boolean | |
| link.feedback.rating_scale | "stars5" | "score10" | |
| link.feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| link.created_at | date-time | |
| link.restore_until | date-time | null | |
| link.revoked_at | date-time | null | |
| link.paused_by | "owner" | "trust" | null | |
| link.capabilities? | LinkCapabilities | null | |
| link.capabilities.tier | "flat" | "pool" | |
| link.capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| link.capabilities.download_limit | object | |
| link.capabilities.access | object | |
| link.capabilities.recipients | object | |
| link.capabilities.bonus | object | null | |
| link.capabilities.send_again | object | |
| link.capabilities.permissions | object | |
| link.placement | LinkPlacement | |
| link.placement.tier | string | null | |
| link.placement.relocating | boolean | |
| link.placement.eligible | boolean | null | |
| link.restore? | RestoreState | |
| link.restore.active | true | |
| link.preview? | LinkPreview | null | |
| link.preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| link.preview.kind | string | null | |
| link.preview.variant | string | null | |
| link.preview.pages | string | integer | null | |
| link.expiry_capped? | ExpiryCapped | null | |
| link.expiry_capped.reason | "billing_cycle" | |
| link.expiry_capped.billing_cycle | "monthly" | "annual" | |
| link.expiry_capped.max_days | string | integer | |
| link.expiry_capped.expires_at | date-time | |
| link.expiry_capped.message | string | |
| link.replayed? | true | |
| file | object | |
| file.id | string | |
| file.copying | boolean | |
| charged_bytes | string | |
| source | object | |
| source.consumed_download | boolean | |
| source.downloads_left | string | integer | null | |
| source.can_download_after | boolean | |
| delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| password | "inherited" | "own" | "none" |
A name ending in ? is optional.
An error: password_required, download_disabled, expired, exhausted, scan_pending, content_blocked, own_file, encrypted, link_paused, account_under_review, forwarding_disabled, forwarding_unverified, delivery_not_allowed, forward_quota_exhausted, ad_required, overage_requires_ad, storage_quota_exceeded, insufficient_credits, approval_required, 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/forwards" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{"slug":"…"}'POST/forwards/checkCheck what forwarding a received link would do
Forwarding copies a link you received into your own storage and gives you a new link to it, without downloading it first. It passes the same checks as a download: password, recipient verification, expiry, download limit and safety scan. It is refused for preview-only and watermarked links, end-to-end encrypted links, your own files (use send again) and when the sender or their team turned forwarding off (403 forwarding_disabled). The answer says what it costs (the copy is billed to your workspace like an upload: plan storage, or credits on an API & Agents workspace), whether it uses one of the link's downloads (economy storage), and what the new link may be: never more than the original lets you.
Key scope: files.read
Request body (JSON)
| slug | string | The received link's slug (the last part of its URL). |
| password? | string | The link's password, when it has one. |
| email? | string | The recipient email, on a link that asks for one. |
| viewer_token? | string | A tracked or restricted link's proof of recipient (from its email verification). |
| r? | string | A per-recipient token from the link's URL (`?r=`). |
A name ending in ? is optional.
Response 200
| allowed | boolean | |
| allowance | object | Forwards of files on included storage are tracked; when the platform caps them, your monthly allowance is checked (409 `forward_quota_exhausted` past it). |
| allowance.tracked | boolean | |
| allowance.capped | boolean | |
| allowance.needs_bytes | string | |
| allowance.remaining_bytes | string | null | |
| allowance.period_end | date-time | null | |
| source | object | |
| source.name | string | |
| source.size | string | |
| source.tier | "pool" | "flat" | |
| source.delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| source.has_password | boolean | |
| consumes_download | boolean | Economy (pool) storage: the forward uses one of the link's downloads. |
| downloads_left | string | integer | null | |
| can_download_after | boolean | False when the forward uses the link's last download. |
| charge_bytes | string | |
| quota | object | |
| quota.used | string | |
| quota.limit | string | |
| quota.after | string | |
| quota.allowed | boolean | |
| quota.code? | string | |
| ad_required | boolean | |
| delivery | object | The new link's mode: the original's by default; only these (the same or narrower) are accepted. |
| delivery.default | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| delivery.allowed | array of "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| password | object | |
| password.required | boolean | The original has a password, so the new link keeps one: yours, or the original's when you set none. |
A name ending in ? is optional.
An error: password_required, download_disabled, expired, exhausted, not_yet_available, scan_pending, content_blocked, own_file, encrypted, link_paused, account_under_review, forwarding_disabled, forwarding_unverified, 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/forwards/check" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{"slug":"…"}'POST/linksCreate a share link
Creates a link for a stored file (file_id from completing an upload). The expiry is clamped to the plan; expiry_capped says why when yours was shortened. Team policy (no public links, a password required, a maximum lifetime) and approval before send apply. Retrying the same create within 2 minutes returns the first link with replayed: true.
Key scope: links.write
Request body (JSON)
| file_id | string | The stored file. |
| slug? | string | A custom link name (paid plans): 4–40 letters, digits, `_` or `-`. |
| password? | string | A download password. |
| expires_at? | string | When the link expires; clamped to the plan (Free: 48 hours by default, at most 7 days; paid: 30 days on monthly billing, a year on annual). |
| max_downloads? | string | integer | A download limit; leave it out for unlimited. |
| note? | string | A note shown to recipients (2000 characters at most). |
| subject? | string | The email and page subject (200 characters at most). |
| sealed_meta? | string | With `encrypted`: the subject and note sealed on your side with the link key, instead of `subject` and `note` (422 `sealed_meta_with_plain` with either; 422 `sealed_meta_requires_e2e` without `encrypted`). The envelope is `sfm1.<iv>.<ciphertext>`: AES-256-GCM under HKDF-SHA256(link key, salt `sendflo/meta/v1`, info `sendflo link metadata`), additional data `meta`, over the JSON `{"v":1,"subject"?,"note"?}`. SendFlo can't read it; the link page opens it in the recipient's browser, and SendFlo's email says the message is sealed. The SDK and the CLI seal it for you. |
| require_email? | boolean | Older option: recipients type an email (not verified) before downloading. |
| access? | "public" | "tracked" | "restricted" | `public` (the default), `tracked` (recipients verify their email) or `restricted` (verified and on `allowed_emails`). |
| allowed_emails? | array of string | Up to 50 addresses; required for `restricted`. |
| encrypted? | boolean | The file was encrypted end to end on the sender's device (paid plans). |
| e2e_half? | string | With `encrypted`, a split key: the SERVER half (32 random bytes, base64url). The file is sealed with HKDF-SHA256(client half ‖ server half, salt `sendflo/e2e-split/v1`, info `sendflo link key`); the link you share carries only the client half, as `#k=2.<client half>`. SendFlo stores this half sealed, gives it to a recipient only after the link's gate (its password, their verified email), and deletes it when the link is revoked or expires. A split link can be emailed by SendFlo (`key_fragment` on the email calls); a whole-key link can't. 422 `e2e_half_without_encryption` without `encrypted`. |
| e2e_half_derivation? | 1 | The split key's derivation: `1` (the only one; the default). |
| e2e_recovery? | object | With `encrypted`, on a team that keeps a recovery key (Team Pro, Team Enterprise): the link's key sealed on your side to the team's recovery key, `{team_key_id, sealed}` (HPKE with X-Wing, info `sendflo/link-recovery/v1/<team_key_id>`). Without it the send answers 422 `recovery_copy_required`, whose body names the key (`recovery.team_key_id`, `recovery.public_key`): seal and send again. 422 `recovery_copy_stale` when the key changed. |
| e2e_recovery.team_key_id | string | |
| e2e_recovery.sealed | string | |
| kind? | "file" | "message" | `file` (the default) or `message`: a secure message. Its file is the text, sealed on your side with SFE2 under a split key (`encrypted` and `e2e_half` required; 256 KB sealed at most, 413 `message_too_large`). It opens only on its page (every download, preview and feedback route answers 409 `message_link`), and takes a password, who can open it, an expiry and `open_once`; anything else answers 422 `message_options`. |
| open_once? | boolean | With `kind: "message"`: the message opens once. The first reveal deletes it (the link is revoked and its key half wiped), and the page then answers 410 `already_opened`. 422 `open_once_requires_message` on a file link. |
| e2e_stamp? | boolean | With `encrypted`: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. Only a client that stamps sets it — the SendFlo apps, the SDK (`sendFile({ encrypt, stamp })`) and the CLI (`send --encrypt --stamp`) — so this needs their `X-SendFlo-Stamp: v1` header (422 `stamp_unsupported` otherwise; 422 `stamp_requires_encryption` without `encrypted`). A team that requires the sender's mark accepts an end-to-end link only stamped (403 `policy_locked`, `setting: e2e_stamp`). |
| e2e_each? | boolean | Team Pro and Team Enterprise, with `e2e_stamp` on a `restricted` link of 1–50 people, the copies within the sender's storage (413 `e2e_each_storage`; 422 `e2e_each_requires_stamp`, `e2e_each_requires_restricted`, `e2e_each_recipients`; 403 `e2e_each_plan`): the sender's device stamps one copy per listed person with `watermark_label` and their email, seals each with the link's key and registers it (the SendFlo apps do this; the API has no public call for it). A confirmed person downloads their own copy. Someone added later needs `e2e_copies` on the change (409 `e2e_each_choice`): `make` (their copy is made on the sender's device; until it's registered their download answers 425 `copy_pending`) or `mark_only` (they get the link's file, which carries the sender's mark alone). |
| delivery? | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | `preview_download` (the default), `download_only`, or the paid `preview_only` and `preview_watermark`. With `encrypted`, a preview mode is end-to-end view-only: images (JPEG, PNG, WebP, GIF, AVIF, BMP) and PDFs up to 200 MB that the recipient's page decrypts and shows, never a download (422 `e2e_view_unsupported`, 413 `e2e_view_too_large`). |
| watermark_label? | string | For `preview_watermark`: the mark baked into the previews; with `download_mark`: the mark on every downloaded copy (and preview). 60 characters at most. |
| viewer_mark? | boolean | Team Pro and Team Enterprise: each viewer's own copy of every image and PDF page carries the label AND their confirmed email; on a video the player draws that email over the picture (`overlay` in the preview answer). Needs `preview_watermark` or `download_mark`, and `tracked` or `restricted` access (422 `viewer_mark_requires_verified_access`). Each copy counts against the team's pool: a viewed page or image once, a downloaded image 1 per started 12 MP, a downloaded PDF or text 1 per started 30 MB. Never on `encrypted` links (422 `viewer_mark_not_on_e2e`). |
| allow_guests? | boolean | With `viewer_mark` on a `tracked` link (422 `guests_require_viewer_mark`, `guests_require_tracked`; never `encrypted`, 422 `guests_not_on_e2e`): guests may also view without confirming an email. Each guest view is its own copy, marked "Guest · <IP> · <UTC time> · view <id>", counted against the team's pool; guests view only unless `guest_downloads`. A guest on a VPN, proxy, Tor, a server, Apple Private Relay or an unknown network confirms an email instead. A team protection policy can forbid guests (403 `policy_locked`, setting `no_guests`). |
| guest_downloads? | boolean | With `allow_guests` and `download_mark` (422 `guest_downloads_require_guests`, `guest_downloads_require_marked_downloads`): guests may also download — each download is their own marked copy with their guest mark, never the original. Off by default; guests view only. |
| viewers_invite? | boolean | On `tracked` or `restricted` links (422 `invites_require_verified_access`; never `encrypted`, 422 `invites_not_on_e2e`): a recipient who confirmed their email may invite people by email from the link's page — 5 per recipient, 50 per send. Each invited person is added to the send as if you had added them (a `restricted` send lists them; your plan's limits apply), confirms their own email to open it and gets their own copies and marks. Your activity shows who invited whom. Off by default; turning it off stops new invitations, and leaving `tracked` / `restricted` turns it off. |
| download_mark? | boolean | Paid plans, on `preview_download` or `download_only`: recipients download a MARKED copy, never the original — the label on every image and PDF page (text files arrive as marked PDFs, HEIC as JPEG), and, with `viewer_mark`, each recipient's confirmed email too. Previews are marked the same way. The sender's own downloads stay the original. Not with `encrypted` (422 `delivery_conflict`: the device stamps those, `e2e_stamp`). |
| mark_unsupported? | "block" | "send" | With `download_mark`: what happens to files that can't be marked (archives, password-protected documents…): `block` (the default — recipients can't download them) or `send` (downloaded unmarked). |
| preview_variant? | "720p" | "480p" | "1080p" | For preview-only video and audio: `720p` (the default), `480p` (half the video allowance) or, on Team Enterprise, `1080p` (twice the allowance; 403 `preview_1080p_plan` on other plans). |
| bundle? | boolean | `true` starts a multi-file send with this file as its first link. |
| bundle_id? | string | Add this file to the send whose first link has this id. |
| available_at? | string | Schedule the link to go live later (Plus and up). |
| notify_on_download? | boolean | Email the sender on each download. |
| brand_id? | string | The brand kit for the download page (paid plans); the default kit when left out. |
| comments_mode? | "off" | "open" | "private" | Recipient comments: `off` (the default), `open` or `private`. |
| ratings_enabled? | boolean | Let recipients rate files. |
| favorites_enabled? | boolean | Let recipients favourite files. |
| approvals_enabled? | boolean | Let recipients approve files or ask for changes. |
| restore? | object | File Restore: `{ enabled, duration_days }`; off on Free. |
| restore.enabled | boolean | |
| restore.duration_days? | string | integer |
A name ending in ? is optional.
Response 200
| id | string | |
| slug | string | |
| url | string | The download page, https://sendflo.app/l/{slug}. |
| file_id | string | |
| has_password | boolean | |
| expires_at | date-time | null | |
| max_downloads | string | integer | null | |
| download_count | string | integer | |
| note | string | null | |
| subject | string | null | |
| require_email | boolean | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| encrypted | boolean | |
| e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| e2e_each | boolean | Mark 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. |
| delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| watermark_label | string | null | |
| viewer_mark | boolean | Each 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. |
| allow_guests | boolean | Guests 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`. |
| guest_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| viewers_invite | boolean | Viewers 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. |
| download_mark | boolean | Download with watermark: recipients download a marked copy (and see marked previews), never the original. |
| mark_unsupported | "block" | "send" | With `download_mark`: files that can't be marked are blocked (`block`) or downloaded as their originals (`send`). |
| bundle_id | string | null | |
| available_at | date-time | null | |
| notify_on_download | boolean | |
| feedback | FeedbackSettings | The transfer's feedback settings. |
| feedback.comments | "off" | "open" | "private" | |
| feedback.ratings | boolean | |
| feedback.favorites | boolean | |
| feedback.approvals | boolean | |
| feedback.rating_scale | "stars5" | "score10" | |
| feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| created_at | date-time | |
| restore_until | date-time | null | |
| revoked_at | date-time | null | |
| paused_by | "owner" | "trust" | null | |
| capabilities? | LinkCapabilities | null | |
| capabilities.tier | "flat" | "pool" | |
| capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| capabilities.download_limit | object | |
| capabilities.download_limit.editable | boolean | |
| capabilities.download_limit.reason | "pool_locked" | null | |
| capabilities.access | object | |
| capabilities.access.editable | boolean | |
| capabilities.access.reason | "pool_read" | null | |
| capabilities.recipients | object | |
| capabilities.recipients.add | "free" | "bonus" | "none" | |
| capabilities.recipients.replace | boolean | |
| capabilities.bonus | object | null | |
| capabilities.bonus.remaining_bytes | string | |
| capabilities.bonus.needs_bytes | string | |
| capabilities.bonus.period_end | date-time | null | |
| capabilities.send_again | object | |
| capabilities.send_again.same | boolean | |
| capabilities.send_again.flexible | boolean | |
| capabilities.send_again.copy_remaining_bytes | string | null | |
| capabilities.permissions | object | |
| capabilities.permissions.editable | true | |
| capabilities.permissions.preview_charge_bytes | string | null | |
| placement | LinkPlacement | |
| placement.tier | string | null | |
| placement.relocating | boolean | |
| placement.eligible | boolean | null | |
| restore? | RestoreState | |
| restore.active | true | |
| preview? | LinkPreview | null | |
| preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| preview.kind | string | null | |
| preview.variant | string | null | |
| preview.pages | string | integer | null | |
| expiry_capped? | ExpiryCapped | null | |
| expiry_capped.reason | "billing_cycle" | |
| expiry_capped.billing_cycle | "monthly" | "annual" | |
| expiry_capped.max_days | string | integer | |
| expiry_capped.expires_at | date-time | |
| expiry_capped.message | string | |
| replayed? | true |
A name ending in ? is optional.
An error: file_not_ready, awaiting_approval, approval_required, 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/links" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{"file_id":"…"}'GET/linksList your share links
Every link in the workspace, revoked and expired ones included, newest first, 25 per page (up to 100 with limit). Pass next_cursor back as cursor for the next page. With q: the links whose note, subject, file names, recipients or slug match (typos forgiven), best first, 25 a page (page 1–8; see search). An end-to-end link is found by its file type, never its name.
Key scope: links.write
Query parameters
| cursor? | string | `next_cursor` from the previous page. |
| limit? | string | integer | Links per page, 1–100 (25 by default). |
| q? | string | Search words (200 characters at most). |
| page? | string | With `q`: the page, 1–8. |
Response 200
| data | array of LinkListItem | |
| data.id | string | |
| data.slug | string | |
| data.url | string | The download page, https://sendflo.app/l/{slug}. |
| data.file_id | string | |
| data.has_password | boolean | |
| data.expires_at | date-time | null | |
| data.max_downloads | string | integer | null | |
| data.download_count | string | integer | |
| data.note | string | null | |
| data.subject | string | null | |
| data.require_email | boolean | |
| data.access | "public" | "tracked" | "restricted" | |
| data.allowed_emails | array of string | |
| data.encrypted | boolean | |
| data.e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| data.e2e_each | boolean | Mark 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. |
| data.delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| data.watermark_label | string | null | |
| data.viewer_mark | boolean | Each 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. |
| data.allow_guests | boolean | Guests 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`. |
| data.guest_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| data.viewers_invite | boolean | Viewers 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. |
| data.download_mark | boolean | Download with watermark: recipients download a marked copy (and see marked previews), never the original. |
| data.mark_unsupported | "block" | "send" | With `download_mark`: files that can't be marked are blocked (`block`) or downloaded as their originals (`send`). |
| data.bundle_id | string | null | |
| data.available_at | date-time | null | |
| data.notify_on_download | boolean | |
| data.feedback | FeedbackSettings | The transfer's feedback settings. |
| data.feedback.comments | "off" | "open" | "private" | |
| data.feedback.ratings | boolean | |
| data.feedback.favorites | boolean | |
| data.feedback.approvals | boolean | |
| data.feedback.rating_scale | "stars5" | "score10" | |
| data.feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| data.created_at | date-time | |
| data.restore_until | date-time | null | |
| data.revoked_at | date-time | null | |
| data.paused_by | "owner" | "trust" | null | |
| data.capabilities? | LinkCapabilities | null | |
| data.capabilities.tier | "flat" | "pool" | |
| data.capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| data.capabilities.download_limit | object | |
| data.capabilities.access | object | |
| data.capabilities.recipients | object | |
| data.capabilities.bonus | object | null | |
| data.capabilities.send_again | object | |
| data.capabilities.permissions | object | |
| data.scan | LinkScan | null | |
| data.scan.state | string | pending, clean, flagged, failed, released or not_scannable. |
| data.scan.severity | "low" | "medium" | "high" | "critical" | null | |
| data.scan.labels | array of string | |
| data.scan.coverage | string | null | |
| data.preview | LinkPreview | null | |
| data.preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| data.preview.kind | string | null | |
| data.preview.variant | string | null | |
| data.preview.pages | string | integer | null | |
| data.feedback_summary | LinkFeedbackSummary | null | |
| data.feedback_summary.comments | string | integer | |
| data.feedback_summary.favorites | string | integer | |
| data.feedback_summary.rating_count | string | integer | |
| data.feedback_summary.rating_avg | number | null | |
| data.feedback_summary.rating_max | string | integer | |
| data.feedback_summary.approved_files | string | integer | |
| data.feedback_summary.changes_requested | string | integer | |
| next_cursor | string | null | |
| search? | SearchMeta | Present when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first. |
| search.engine | "meilisearch" | "postgres" | |
| search.page | string | integer | This page (1–8). |
| search.total | string | integer | About how many match (the search engine's estimate). |
| search.next_page | string | 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/links" \ -H "authorization: Bearer $SENDFLO_API_KEY"
POST/links/from-urlSend a file from a link
SendFlo fetches the file at a public https address (resolved and checked first: never a private or local network; no redirects; up to 100 MB in 60 seconds), stores it in your workspace and makes a share link for it, as POST /v1/links does. The key also needs transfers.write (the upload). For automation apps (Zapier, Make, n8n) and scripts that have an address, not the file; A2A's send_file with a URL part does the same.
Key scope: links.write
Request body (JSON)
| url | string | The file's https address. |
| name? | string | The file's name (default: the address's last path segment). |
| note? | string | A note for the recipient. |
A name ending in ? is optional.
Response 200
| id | string | |
| slug | string | |
| url | string | The download page, https://sendflo.app/l/{slug}. |
| file_id | string | |
| has_password | boolean | |
| expires_at | date-time | null | |
| max_downloads | string | integer | null | |
| download_count | string | integer | |
| note | string | null | |
| subject | string | null | |
| require_email | boolean | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| encrypted | boolean | |
| e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| e2e_each | boolean | Mark 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. |
| delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| watermark_label | string | null | |
| viewer_mark | boolean | Each 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. |
| allow_guests | boolean | Guests 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`. |
| guest_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| viewers_invite | boolean | Viewers 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. |
| download_mark | boolean | Download with watermark: recipients download a marked copy (and see marked previews), never the original. |
| mark_unsupported | "block" | "send" | With `download_mark`: files that can't be marked are blocked (`block`) or downloaded as their originals (`send`). |
| bundle_id | string | null | |
| available_at | date-time | null | |
| notify_on_download | boolean | |
| feedback | FeedbackSettings | The transfer's feedback settings. |
| feedback.comments | "off" | "open" | "private" | |
| feedback.ratings | boolean | |
| feedback.favorites | boolean | |
| feedback.approvals | boolean | |
| feedback.rating_scale | "stars5" | "score10" | |
| feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| created_at | date-time | |
| restore_until | date-time | null | |
| revoked_at | date-time | null | |
| paused_by | "owner" | "trust" | null | |
| capabilities? | LinkCapabilities | null | |
| capabilities.tier | "flat" | "pool" | |
| capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| capabilities.download_limit | object | |
| capabilities.download_limit.editable | boolean | |
| capabilities.download_limit.reason | "pool_locked" | null | |
| capabilities.access | object | |
| capabilities.access.editable | boolean | |
| capabilities.access.reason | "pool_read" | null | |
| capabilities.recipients | object | |
| capabilities.recipients.add | "free" | "bonus" | "none" | |
| capabilities.recipients.replace | boolean | |
| capabilities.bonus | object | null | |
| capabilities.bonus.remaining_bytes | string | |
| capabilities.bonus.needs_bytes | string | |
| capabilities.bonus.period_end | date-time | null | |
| capabilities.send_again | object | |
| capabilities.send_again.same | boolean | |
| capabilities.send_again.flexible | boolean | |
| capabilities.send_again.copy_remaining_bytes | string | null | |
| capabilities.permissions | object | |
| capabilities.permissions.editable | true | |
| capabilities.permissions.preview_charge_bytes | string | null | |
| placement | LinkPlacement | |
| placement.tier | string | null | |
| placement.relocating | boolean | |
| placement.eligible | boolean | null | |
| restore? | RestoreState | |
| restore.active | true | |
| preview? | LinkPreview | null | |
| preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| preview.kind | string | null | |
| preview.variant | string | null | |
| preview.pages | string | integer | null | |
| expiry_capped? | ExpiryCapped | null | |
| expiry_capped.reason | "billing_cycle" | |
| expiry_capped.billing_cycle | "monthly" | "annual" | |
| expiry_capped.max_days | string | integer | |
| expiry_capped.expires_at | date-time | |
| expiry_capped.message | string | |
| replayed? | true |
A name ending in ? is optional.
An error: invalid_url, file_unreachable, file_too_large, upload_failed, file_not_ready, awaiting_approval, approval_required, 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/links/from-url" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{"url":"…"}'PATCH/links/{id}Change a link's settings
Changes only the fields you send. A file on included (pool) storage keeps its download limit from upload, and once it has been read its access, recipients and a shorter expiry are fixed too (409 pool_locked, with field and why; see capabilities on the link); a file on unlimited-download storage is open to every change within the plan's limits. The password, note, subject, delivery mode and download emails are never locked. Feedback settings apply to every file of the send.
Key scope: links.write
Path parameters
| id | string | The link's id. |
Request body (JSON)
| password? | string | null | A new password; null or "" removes it. |
| expires_at? | string | A new expiry (clamped to the plan). |
| max_downloads? | string | integer | null | A new download limit; null = unlimited. |
| note? | string | null | The note; null clears it. |
| subject? | string | null | The subject; null clears it. |
| sealed_meta? | string | null | An end-to-end link's sealed subject and note (see POST /v1/links); null clears them. A link whose subject and note are sealed refuses a readable one (422 `meta_sealed`). |
| replies_off? | boolean | Turn secure replies off (or on) for the whole send: verified recipients of a Tracked or Restricted send can reply on its page with a message and files (GET /v1/links/:id/replies). |
| require_email? | boolean | Older option: recipients type an email first. |
| access? | "public" | "tracked" | "restricted" | `public`, `tracked` or `restricted`. |
| allowed_emails? | array of string | Replaces the restricted list (50 at most). |
| base_allowed_emails? | array of string | Optional, with `allowed_emails`: the list as your editor showed it. Then only who you added or removed since is applied over the list as it is now, so someone added meanwhile (from People, another device or the API) keeps access. |
| e2e_copies? | "make" | "mark_only" | On a Mark each recipient link (`e2e_each`), required when `allowed_emails` adds people (409 `e2e_each_choice`): `make` — their own copies are made on the sender's device (the SendFlo apps), their download waits (425 `copy_pending`) until then — or `mark_only`, the copy with the sender's mark alone. |
| notify_on_download? | boolean | Email the sender on each download. |
| delivery? | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | The delivery mode (the preview-only modes on paid plans). |
| watermark_label? | string | null | The watermark; null = the default mark. |
| viewer_mark? | boolean | Add or remove each viewer's confirmed email in their own copies (Team Pro and Team Enterprise; tracked or restricted access). |
| allow_guests? | boolean | Let guests view without an email (their IP, the time and a view id are printed instead), or stop letting them in. Needs `viewer_mark` and `tracked` access. |
| guest_downloads? | boolean | Let guests download their own marked copies (their guest mark on every image and PDF page), or keep them view-only. Needs `allow_guests` and `download_mark` (422 `guest_downloads_require_guests`, `guest_downloads_require_marked_downloads`). Turning guests or marked downloads off turns this off too. |
| viewers_invite? | boolean | Let recipients who confirmed their email invite people (5 each, 50 per send), or stop new invitations — people already invited keep their access. Needs `tracked` or `restricted` access (422 `invites_require_verified_access`); never `encrypted` (422 `invites_not_on_e2e`). Moving the link to `public` turns this off. |
| download_mark? | boolean | Download with watermark on or off (paid; a downloadable mode). |
| mark_unsupported? | "block" | "send" | `block` or `send`: files that can't be marked. |
| preview_variant? | "720p" | "480p" | "1080p" | `720p`, `480p`, or `1080p` on Team Enterprise (403 `preview_1080p_plan`). |
| comments_mode? | "off" | "open" | "private" | `off`, `open` or `private` (the whole send). |
| ratings_enabled? | boolean | Ratings on or off (the whole send). |
| favorites_enabled? | boolean | Favourites on or off (the whole send). |
| approvals_enabled? | boolean | Approvals on or off (the whole send). |
| rating_scale? | "stars5" | "score10" | `stars5` or `score10`, only while the send has no ratings. |
A name ending in ? is optional.
Response 200
| id | string | |
| slug | string | |
| url | string | The download page, https://sendflo.app/l/{slug}. |
| file_id | string | |
| has_password | boolean | |
| expires_at | date-time | null | |
| max_downloads | string | integer | null | |
| download_count | string | integer | |
| note | string | null | |
| subject | string | null | |
| require_email | boolean | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| encrypted | boolean | |
| e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| e2e_each | boolean | Mark 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. |
| delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| watermark_label | string | null | |
| viewer_mark | boolean | Each 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. |
| allow_guests | boolean | Guests 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`. |
| guest_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| viewers_invite | boolean | Viewers 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. |
| download_mark | boolean | Download with watermark: recipients download a marked copy (and see marked previews), never the original. |
| mark_unsupported | "block" | "send" | With `download_mark`: files that can't be marked are blocked (`block`) or downloaded as their originals (`send`). |
| bundle_id | string | null | |
| available_at | date-time | null | |
| notify_on_download | boolean | |
| feedback | FeedbackSettings | The transfer's feedback settings. |
| feedback.comments | "off" | "open" | "private" | |
| feedback.ratings | boolean | |
| feedback.favorites | boolean | |
| feedback.approvals | boolean | |
| feedback.rating_scale | "stars5" | "score10" | |
| feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| created_at | date-time | |
| restore_until | date-time | null | |
| revoked_at | date-time | null | |
| paused_by | "owner" | "trust" | null | |
| capabilities? | LinkCapabilities | null | |
| capabilities.tier | "flat" | "pool" | |
| capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| capabilities.download_limit | object | |
| capabilities.download_limit.editable | boolean | |
| capabilities.download_limit.reason | "pool_locked" | null | |
| capabilities.access | object | |
| capabilities.access.editable | boolean | |
| capabilities.access.reason | "pool_read" | null | |
| capabilities.recipients | object | |
| capabilities.recipients.add | "free" | "bonus" | "none" | |
| capabilities.recipients.replace | boolean | |
| capabilities.bonus | object | null | |
| capabilities.bonus.remaining_bytes | string | |
| capabilities.bonus.needs_bytes | string | |
| capabilities.bonus.period_end | date-time | null | |
| capabilities.send_again | object | |
| capabilities.send_again.same | boolean | |
| capabilities.send_again.flexible | boolean | |
| capabilities.send_again.copy_remaining_bytes | string | null | |
| capabilities.permissions | object | |
| capabilities.permissions.editable | true | |
| capabilities.permissions.preview_charge_bytes | string | null | |
| preview | LinkPreview | null | |
| preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| preview.kind | string | null | |
| preview.variant | string | null | |
| preview.pages | string | integer | null | |
| placement | LinkPlacement | |
| placement.tier | string | null | |
| placement.relocating | boolean | |
| placement.eligible | boolean | null | |
| expiry_capped | ExpiryCapped | null | |
| expiry_capped.reason | "billing_cycle" | |
| expiry_capped.billing_cycle | "monthly" | "annual" | |
| expiry_capped.max_days | string | integer | |
| expiry_capped.expires_at | date-time | |
| expiry_capped.message | string |
A name ending in ? is optional.
An error: not_found, pool_locked, policy_password_required, invalid_expiry, policy_ttl_exceeded, allowed_emails_required, paid_required, delivery_conflict, e2e_view_unsupported, e2e_view_too_large, viewer_mark_not_on_e2e, feedback_policy_off, rating_scale_locked, empty_patch, viewer_mark_plan, viewer_mark_requires_watermark, viewer_mark_requires_verified_access, guests_require_viewer_mark, guests_require_tracked, guests_not_on_e2e, policy_locked, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).
curl -X PATCH "https://api.sendflo.app/v1/links/{id}" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{}'DELETE/links/{id}Revoke a share link
Revokes one link for good and frees the file's storage, unless another live link, an open restore window or a safety review still needs it. Answers 204, also for an unknown or already revoked link.
Key scope: links.write
Path parameters
| id | string | The link's id. |
Response 200
| revoked | true | |
| retained_until | date-time | |
| storage_freed | true |
A name ending in ? is optional.
An error unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).
curl -X DELETE "https://api.sendflo.app/v1/links/{id}" \
-H "authorization: Bearer $SENDFLO_API_KEY"GET/links/{id}/transferGet the whole send behind a link
The send as one unit: its state, counts and every file's link (revoked ones included).
Key scope: links.write
Path parameters
| id | string | Any link of the send (its first link or any other); a single-file send is a send of one. |
Response 200
| transfer_id | string | The send's first link. |
| files | string | integer | |
| live | string | integer | |
| revoked | boolean | |
| paused | boolean | |
| paused_by | "owner" | "trust" | null | |
| created_at | date-time | |
| links | array of object | |
| links.link_id | string | |
| links.slug | string | |
| links.url | string | |
| links.file_id | string | |
| links.name | string | |
| links.size | string | |
| links.download_count | string | integer | |
| links.expires_at | date-time | null | |
| links.revoked_at | date-time | null | |
| links.paused_by | "owner" | "trust" | null | |
| links.live | boolean | |
| links.capabilities | LinkCapabilities | null | |
| links.capabilities.tier | "flat" | "pool" | |
| links.capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| links.capabilities.download_limit | object | |
| links.capabilities.access | object | |
| links.capabilities.recipients | object | |
| links.capabilities.bonus | object | null | |
| links.capabilities.send_again | object | |
| links.capabilities.permissions | object |
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" \
-H "authorization: Bearer $SENDFLO_API_KEY"PUT/links/{id}/transfer/pausePause every file of a send
Every file stops opening and downloading; recipients see "Paused by the sender". Safe to repeat. 409 transfer_revoked when every file is revoked.
Key scope: links.write
Path parameters
| id | string | Any link of the send (its first link or any other); a single-file send is a send of one. |
Response 200
| transfer_id | string | The send's first link. |
| files | string | integer | |
| live | string | integer | |
| revoked | boolean | |
| paused | boolean | |
| paused_by | "owner" | "trust" | null |
A name ending in ? is optional.
An error: not_found, transfer_revoked, 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}/transfer/pause" \
-H "authorization: Bearer $SENDFLO_API_KEY"DELETE/links/{id}/transfer/pauseResume a paused send
Lifts your own pause; a Trust & Safety pause stays (paused_by: "trust"). Safe to repeat.
Key scope: links.write
Path parameters
| id | string | Any link of the send (its first link or any other); a single-file send is a send of one. |
Response 200
| transfer_id | string | The send's first link. |
| files | string | integer | |
| live | string | integer | |
| revoked | boolean | |
| paused | boolean | |
| paused_by | "owner" | "trust" | 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 -X DELETE "https://api.sendflo.app/v1/links/{id}/transfer/pause" \
-H "authorization: Bearer $SENDFLO_API_KEY"POST/links/{id}/transfer/revokeRevoke every file of a send
Ends every file for good and frees storage (as revoking one link does). storage says what was freed, held on pooled storage, or kept (shared, under review, or in a restore window). Safe to repeat: revoked_now is 0 the second time.
Key scope: links.write
Path parameters
| id | string | Any link of the send (its first link or any other); a single-file send is a send of one. |
Response 200
| transfer_id | string | The send's first link. |
| files | string | integer | |
| live | string | integer | |
| revoked | boolean | |
| paused | boolean | |
| paused_by | "owner" | "trust" | null | |
| revoked_now | string | integer | |
| storage | object | |
| storage.freed_bytes | string | |
| storage.held | array of object | |
| storage.held.file_id | string | |
| storage.held.until | date-time | |
| storage.kept | array of object | |
| storage.kept.file_id | string | |
| storage.kept.reason | "shared" | "review" | "restore" |
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/links/{id}/transfer/revoke" \
-H "authorization: Bearer $SENDFLO_API_KEY"POST/prepared-sendsAsk the person to send files
For an assistant that can't reach the person's files (a hosted chat app): open url for the person — the MCP server does it with a URL elicitation. Signed in as the same account, they pick files and send them the way the Send page does; another account is refused. Poll GET /v1/prepared-sends/{id} for the links. Lasts an hour.
Key scope: links.write
Request body (JSON)
| note? | string | What to send, shown to the person (500 characters). |
| message? | string | The message for the recipients (2000 characters). |
| emails? | array of string | Up to 10 recipients to email the links to. |
A name ending in ? is optional.
Response 201
| id | string | |
| state | "waiting" | "sent" | "declined" | |
| url | string | The page the person opens, signed in as the account the key or app acts for. |
| links | array of object | The links the person's send made (empty until `sent`). |
| links.id | string | |
| links.url | string | |
| links.name | string | |
| expires_at | date-time |
A name ending in ? is optional.
An error: no_person, or unauthenticated (401), insufficient_scope (403), invalid_request or invalid_id (422), rate_limited (429).
curl -X POST "https://api.sendflo.app/v1/prepared-sends" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{}'GET/prepared-sends/{id}See whether the person sent the files
Key scope: links.write
Path parameters
| id | string | The prepared send's id. |
Response 200
| id | string | |
| state | "waiting" | "sent" | "declined" | |
| url | string | The page the person opens, signed in as the account the key or app acts for. |
| links | array of object | The links the person's send made (empty until `sent`). |
| links.id | string | |
| links.url | string | |
| links.name | string | |
| expires_at | date-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/prepared-sends/{id}" \
-H "authorization: Bearer $SENDFLO_API_KEY"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
| id | string | The link's id. |
Response 200
| data | array of LinkEvent | |
| data.kind | string | open, view, download, restore, forward, comment, signup, access_off, access_on. |
| data.bytes | string | |
| data.recipient | string | null | |
| data.verified | boolean | |
| data.stamp_check? | string | null | |
| data.created_at | date-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
| id | string | Any 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 | integer | Events per page, 1–200 (50 by default). |
Response 200
| transfer_id | string | |
| slug | string | |
| files | array of object | |
| files.link_id | string | |
| files.name | string | |
| people | array of ActivityPerson | null | |
| people.key | string | |
| people.email | string | null | |
| people.label | string | The email, or "Visitor N" for someone who never gave one. |
| people.verified | boolean | |
| people.opened_at | date-time | null | |
| people.last_at | date-time | |
| people.files_total | string | integer | |
| people.downloaded | array of object | |
| people.downloaded.link_id | string | |
| people.downloaded.name | string | |
| people.downloaded.at | date-time | |
| people.downloaded.proof | "receipts" | "handed_off" | null | |
| people.countries | array of string | |
| people.devices | array of string | |
| people.proof | "receipts" | "handed_off" | null | |
| people.events | string | integer | |
| people.ips? | array of string | Team Pro and Team Enterprise only: the addresses they came from. |
| people_total | string | integer | null | |
| events | array of ActivityEvent | |
| events.id | string | |
| events.link_id | string | |
| events.file | string | |
| events.kind | string | open, view, download, restore, forward, comment, signup, access_off, access_on. |
| events.bytes | string | |
| events.recipient | string | null | |
| events.verified | boolean | |
| events.country | string | null | |
| events.transport | string | null | |
| events.device | string | null | |
| events.proof | "receipts" | "handed_off" | null | |
| events.stamp_check? | "found" | "missing" | |
| events.ip? | string | null | |
| events.network? | object | null | |
| events.network.org | string | null | |
| events.network.class | string | null | |
| events.created_at | date-time | |
| next_cursor | string | 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
| id | string | Any 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
| id | string | Any 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" | |
| manageable | boolean | |
| files | array of object | |
| files.link_id | string | |
| files.name | string | |
| files.live | boolean | |
| people | array of TransferPerson | |
| people.email | string | |
| people.sources | array of "listed" | "verified" | "emailed" | |
| people.files | array of object | |
| people.files.link_id | string | |
| people.files.name | string | |
| people.files.access | "open" | "removed" | |
| people.open | string | 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
| id | string | Any link of the send (its first link or any other); a single-file send is a send of one. |
Request body (JSON)
| string | The person's email. | |
| files | "all" | array of string | `"all"`, or link ids of this send. |
A name ending in ? is optional.
Response 200
| string | ||
| open | string | integer | |
| changes | array of object | |
| changes.link_id | string | |
| changes.change | "removed" | "restored" | "none" | "not_listed" | |
| person | TransferPerson | null | |
| person.email | string | |
| person.sources | array of "listed" | "verified" | "emailed" | |
| person.files | array of object | |
| person.files.link_id | string | |
| person.files.name | string | |
| person.files.access | "open" | "removed" | |
| person.open | string | 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 | integer | The period in days, 1–90 (30 by default). |
Response 200
| period | object | |
| period.from | date-time | |
| period.to | date-time | |
| period.days | number | |
| totals | object | |
| totals.links | string | integer | |
| totals.opens | string | integer | |
| totals.downloads | string | integer | |
| totals.views | string | integer | |
| totals.signups | string | integer | |
| totals.egress_bytes | string | |
| data | array of TransferReportRow | |
| data.id | string | |
| data.slug | string | |
| data.name | string | |
| data.size | string | |
| data.created_at | date-time | |
| data.expires_at | date-time | null | |
| data.revoked | boolean | |
| data.opens | string | integer | |
| data.downloads | string | integer | |
| data.views | string | integer | |
| data.signups | string | integer | |
| data.egress_bytes | string | |
| data.comments | string | integer | |
| data.favorites | string | integer | |
| data.rating_count | string | integer | |
| data.rating_avg | number | null | |
| data.rating_max | string | 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"
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
| data | array of File | |
| data.id | string | |
| data.name | string | |
| data.size | string | |
| data.content_type | string | null | |
| data.created_at | date-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/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
| id | string | The stored file's id. |
Response 200
| file | object | |
| file.id | string | |
| file.name | string | |
| file.size | string | |
| file.expires_at | date-time | null | |
| file.tier | "pool" | "flat" | null | |
| file.downloaded | boolean | |
| file.capabilities | LinkCapabilities | null | |
| file.capabilities.tier | "flat" | "pool" | |
| file.capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| file.capabilities.download_limit | object | |
| file.capabilities.access | object | |
| file.capabilities.recipients | object | |
| file.capabilities.bonus | object | null | |
| file.capabilities.send_again | object | |
| file.capabilities.permissions | object | |
| targets | object | Where 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.same | object | A copy on the same provider (a file on included storage): a new file under the same rules, fixed download limit. |
| targets.same.available | boolean | |
| targets.same.counts_as_read | boolean | |
| targets.flexible | object | A copy to unlimited-download storage within the monthly copy allowance: its download limit is editable afterwards. |
| targets.flexible.available | boolean | |
| targets.flexible.needs_bytes | string | |
| targets.flexible.remaining_bytes | string | null | |
| targets.flexible.size_bytes | string | null | |
| targets.flexible.period_end | date-time | null | |
| targets.flexible.reason | "no_flat_target" | "copy_quota_exhausted" | null | |
| eligible | boolean | |
| reasons | array of object | |
| reasons.code | "staging" | "archived" | "relocating" | "scan" | "expired" | "encrypted" | "paused" | "abuse" | "suspended" | |
| reasons.message | string | |
| live_links | array of object | |
| live_links.id | string | |
| live_links.slug | string | |
| live_links.url | string | |
| live_links.expires_at | date-time | null | |
| live_links.max_downloads | string | integer | null | |
| live_links.download_count | string | integer | |
| live_links.downloads_left | string | integer | null | |
| live_links.delivery | "preview_download" | "download_only" | "preview_only" | "preview_watermark" | |
| live_links.has_password | boolean | |
| live_links.access | "public" | "tracked" | "restricted" | |
| plan | object | |
| plan.mode | "reuse" | "copy" | |
| plan.representative_file_id | string | |
| plan.representative_expires_at | date-time | null | |
| plan.copying | boolean | |
| plan.pool_headroom | number | null | |
| plan.charge_bytes | string | |
| plan.repoints_original_links | boolean | |
| extend | object | |
| extend.near_expiry | boolean | |
| extend.current_until | date-time | null | |
| extend.max_until | date-time | |
| quota | object | |
| quota.used | string | |
| quota.limit | string | |
| quota.after | string | |
| quota.allowed | boolean | |
| quota.code? | "storage_quota_exceeded" | "overage_requires_ad" | |
| ad_required | boolean |
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
| id | string | The stored file's id. |
Request body (JSON)
| link | object | The 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_id | string | |
| link.e2e_recovery.sealed | string | |
| 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.enabled | boolean | |
| 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? | string | For the signed-in app (a Free plan's ad credit). |
| overage_ad? | boolean | For the signed-in app (an ad was watched to go over quota). |
| overage_ad_nonce? | string | For the signed-in app (that ad's credit). |
A name ending in ? is optional.
Response 200
| mode | "reuse" | "copy" | |
| target? | "same" | "flexible" | |
| link | LinkCreated | A new link (or, for a retried create, the earlier one with `replayed: true`). |
| link.id | string | |
| link.slug | string | |
| link.url | string | The download page, https://sendflo.app/l/{slug}. |
| link.file_id | string | |
| link.has_password | boolean | |
| link.expires_at | date-time | null | |
| link.max_downloads | string | integer | null | |
| link.download_count | string | integer | |
| link.note | string | null | |
| link.subject | string | null | |
| link.require_email | boolean | |
| link.access | "public" | "tracked" | "restricted" | |
| link.allowed_emails | array of string | |
| link.encrypted | boolean | |
| link.e2e_stamp | boolean | End-to-end sends: the sender's device stamped `watermark_label` into this file (PDFs and images) before encrypting it. |
| link.e2e_each | boolean | Mark 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_label | string | null | |
| link.viewer_mark | boolean | Each 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_guests | boolean | Guests 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_downloads | boolean | Guests may also download: each download is their own marked copy, carrying their guest mark. Needs `allow_guests` and `download_mark`. |
| link.viewers_invite | boolean | Viewers 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_mark | boolean | Download 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_id | string | null | |
| link.available_at | date-time | null | |
| link.notify_on_download | boolean | |
| link.feedback | FeedbackSettings | The transfer's feedback settings. |
| link.feedback.comments | "off" | "open" | "private" | |
| link.feedback.ratings | boolean | |
| link.feedback.favorites | boolean | |
| link.feedback.approvals | boolean | |
| link.feedback.rating_scale | "stars5" | "score10" | |
| link.feedback.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| link.created_at | date-time | |
| link.restore_until | date-time | null | |
| link.revoked_at | date-time | null | |
| link.paused_by | "owner" | "trust" | null | |
| link.capabilities? | LinkCapabilities | null | |
| link.capabilities.tier | "flat" | "pool" | |
| link.capabilities.read | boolean | The file has been read (downloaded, previewed from the original, or copied). |
| link.capabilities.download_limit | object | |
| link.capabilities.access | object | |
| link.capabilities.recipients | object | |
| link.capabilities.bonus | object | null | |
| link.capabilities.send_again | object | |
| link.capabilities.permissions | object | |
| link.placement | LinkPlacement | |
| link.placement.tier | string | null | |
| link.placement.relocating | boolean | |
| link.placement.eligible | boolean | null | |
| link.restore? | RestoreState | |
| link.restore.active | true | |
| link.preview? | LinkPreview | null | |
| link.preview.state | string | queued, ready, failed, quota (or unsupported on create). |
| link.preview.kind | string | null | |
| link.preview.variant | string | null | |
| link.preview.pages | string | integer | null | |
| link.expiry_capped? | ExpiryCapped | null | |
| link.expiry_capped.reason | "billing_cycle" | |
| link.expiry_capped.billing_cycle | "monthly" | "annual" | |
| link.expiry_capped.max_days | string | integer | |
| link.expiry_capped.expires_at | date-time | |
| link.expiry_capped.message | string | |
| link.replayed? | true | |
| file | object | |
| file.id | string | |
| file.copying | boolean | |
| charged_bytes | string | |
| allowance_bytes? | string | What the copy used of the monthly copy allowance (0 for `same`). |
| counts_as_read? | boolean | The 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? | string | Only files that arrived through this file request. |
| inbox_id? | string | Only files delivered into this inbox. |
| source? | string | Only files requested from this platform (its name: `zendesk`, `testdesk` …). |
| connection_id? | string | Only files requested through this helpdesk connection. |
| ticket? | string | Only files requested from this ticket (its number, with or without #). |
| q? | string | A search: the uploader's name and email, file names, the request and the reference (2–100 characters). |
| page? | string | With `q`: the page of results, 1–8. |
| since? | date-time | Only files received at or after this moment. |
| until? | date-time | Only files received before this moment. |
| scan_state? | string | Comma-separated scan states. |
| limit? | string | integer | How many, 1–200 (default 200). |
| cursor? | string | `next_cursor` from the previous page. |
Response 200
| data | array of ReceivedFile | |
| data.id | string | |
| data.file_id | string | |
| data.name | string | |
| data.size | string | |
| data.from | string | null | |
| data.from_email | string | null | |
| data.answers | array of object | |
| data.answers.id | string | |
| data.answers.label | string | |
| data.answers.type | string | |
| data.answers.value | string | |
| data.contact | object | null | |
| data.contact.name | string | |
| data.contact.email | string | |
| data.contact.email_verified | boolean | |
| data.submission_id | string | null | |
| data.scan_state | string | rejected, or the file's scan state (pending, clean, flagged, failed, released, not_scannable). |
| data.preview_kind | "image" | "video" | "audio" | "pdf" | null | |
| data.scan_labels | array of string | |
| data.rejected_at | date-time | null | |
| data.reject_reason | string | null | |
| data.received_at | date-time | |
| data.expires_at | date-time | null | |
| data.ad_supported | boolean | |
| data.file_request_id | string | null | |
| data.inbox_id | string | null | |
| data.link | object | null | |
| data.link.id | string | |
| data.link.name | string | null | |
| data.link.kind | string | null | |
| data.link.slug | string | null | |
| data.view_only | boolean | A view-only PDF or image (its page's setting or the team's rule): preview it; the download is refused. |
| data.member_mark | boolean | Its previews are your own copies with your email on them. |
| data.can_open_original | boolean | You're the team's Owner or an Admin: the app's View original… opens it, with a reason. |
| next_cursor | string | null | |
| search? | SearchMeta | Present when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first. |
| search.engine | "meilisearch" | "postgres" | |
| search.page | string | integer | This page (1–8). |
| search.total | string | integer | About how many match (the search engine's estimate). |
| search.next_page | string | 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"
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
| id | string | The received item's id (from GET /v1/inbox-items). |
Response 200
| url | string | |
| name | string | |
| size | string | |
| ticket | string |
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
| id | string | The received item's id. |
Response 200
| url | string | |
| kind | "image" | "video" | "audio" | "pdf" | |
| content_type | string | |
| plane | "stored" | "direct" | |
| name | string | |
| size | string | |
| view_only? | boolean | |
| marked? | boolean | |
| pages? | string | integer | A view-only PDF's page count. |
| page_urls? | array of string | null | A 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
| id | string | The received item's id. |
Request body (JSON)
| from | string | integer | The first page to return, counting from 0 (5 after the first screen). |
| count? | string | integer | How many pages (10 at most). |
A name ending in ? is optional.
Response 200
| from | string | integer | |
| pages | string | integer | |
| page_urls | array 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
| data | array of FileRequest | |
| data.id | string | |
| data.slug | string | |
| data.url | string | The upload page, https://sendflo.app/u/{slug}. |
| data.state | "open" | "fulfilled" | "cancelled" | "expired" | |
| data.kind | "request" | "reception" | |
| data.personal | boolean | The account's personal upload link. |
| data.name | string | null | |
| data.prompt | string | null | |
| data.max_files | string | integer | null | |
| data.max_bytes_per_file | string | null | |
| data.max_bytes_per_upload | string | null | |
| data.max_files_per_upload | string | integer | null | |
| data.max_total_bytes | string | null | |
| data.max_total_uploads | string | integer | null | |
| data.max_bytes_per_person | string | null | |
| data.max_uploads_per_person | string | integer | null | |
| data.show_space_left | boolean | The page shows uploaders how much room is left. |
| data.usage | object | null | |
| data.usage.received_bytes | string | Bytes, as a decimal string. |
| data.usage.received_files | string | integer | |
| data.usage.uploads | string | integer | |
| data.usage.reserved_bytes | string | Bytes held for uploads still landing. |
| data.files_received | string | integer | |
| data.has_password | boolean | |
| data.notify | object | null | |
| data.notify.emails? | array of string | |
| data.notify.webhook_url? | string | |
| data.access | "public" | "tracked" | "restricted" | |
| data.allowed_emails | array of string | |
| data.retention_days | string | integer | null | |
| data.effective_retention_days | string | integer | |
| data.allowed_types | array of string | null | |
| data.reject_flagged | boolean | |
| data.brand_id | string | null | |
| data.questions | array of UploadQuestion | |
| data.questions.id | string | |
| data.questions.label | string | |
| data.questions.type | "short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent" | |
| data.questions.required | boolean | |
| data.questions.help? | string | |
| data.questions.options? | array of string | |
| data.questions.settings? | object | The type's own settings (DZ4). |
| data.require_contact | boolean | |
| data.view_only | boolean | Received PDFs and images are view-only (team seats). |
| data.member_mark | boolean | Each preview is the member's own copy with their email on it. |
| data.view_only_other | "download" | "block" | |
| data.team_visible | boolean | The team's Owner and Admins see what arrives through this page (team seats). |
| data.embed_origins | array of string | The 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_answers | boolean | The host page's `all_complete` widget event carries the uploader's answers. |
| data.widget | object | The script widget for this page. |
| data.widget.allowed | boolean | The plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan. |
| data.widget.needs | string | The plan the script widget needs. |
| data.widget.secret_issued | boolean | A widget secret was issued (GET /v1/file-requests/:id/widget-secret). |
| data.e2e | boolean | Uploads 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_available | boolean | The plan has encrypted uploads (Pro, Team Pro, Team Enterprise). |
| data.expires_at | date-time | null | |
| data.created_at | date-time | |
| data.updated_at | date-time | |
| limits | FileRequestLimits | |
| limits.plan_class | "free" | "starter" | "plus" | "pro" | "enterprise" | |
| limits.reception_links | object | |
| limits.reception_links.used | string | integer | |
| limits.reception_links.max | string | integer | 0 = unlimited. |
| limits.retention_default_days | string | integer | |
| limits.retention_max_days | string | integer | |
| limits.request_expiry_free_days | string | integer | |
| limits.paid | boolean | |
| limits.max_transfer_bytes | string | The 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"
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 | null | Your own label, never shown to uploaders (120 characters at most). |
| prompt? | string | null | The 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 | integer | 1–50 (1 by default); ignored for `reception`. |
| max_bytes_per_file? | string | integer | null | A size limit per file, in bytes; null = none. |
| max_bytes_per_upload? | string | integer | null | A size limit for the files someone sends together, in bytes; null = none. Every plan. |
| max_files_per_upload? | string | integer | null | How many files someone can send together (1–1000); null = the plan's files per transfer. |
| max_total_bytes? | string | integer | null | A 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 | null | How many uploads the page accepts in all (1–1,000,000); null = no limit. Every plan. |
| max_bytes_per_person? | string | integer | null | A 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 | null | How many uploads one verified uploader can send (1–100,000); null = no limit. Paid plans; like `max_bytes_per_person`. |
| show_space_left? | boolean | Show uploaders how much room the page has left. |
| password? | string | null | An upload-page password; null = none. |
| notify? | object | null | Who'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 | null | Required for `restricted` (100 at most). |
| retention_days? | string | integer | null | How long received files are kept; null = the plan's default. |
| allowed_types? | array of string | null | File categories (image, video, document…) or extensions; empty = any type. |
| reject_flagged? | boolean | Delete uploads the malware scan blocks (true by default). |
| brand_id? | string | null | A brand kit of this workspace; null = the default kit. |
| deliver_to_inbox_id? | string | An inbox that also receives the uploads. |
| expires_at? | string | null | When the page stops accepting uploads; null = never. |
| questions? | array of object | null | Up 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.label | string | |
| questions.type | string | |
| questions.required? | boolean | |
| questions.options? | array of string | |
| questions.help? | string | |
| questions.settings? | map of string | number | boolean | null | |
| require_contact? | boolean | Uploaders must give their name and email. |
| view_only? | boolean | Team 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? | boolean | Team 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" | null | While `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? | boolean | Team 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 | null | The 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? | boolean | The script widget's `all_complete` event, sent to the host page, carries the uploader's answers. Off by default. Pro and up. |
| e2e? | boolean | Encrypt 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? | string | A 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
| id | string | |
| slug | string | |
| url | string | The upload page, https://sendflo.app/u/{slug}. |
| state | "open" | "fulfilled" | "cancelled" | "expired" | |
| kind | "request" | "reception" | |
| personal | boolean | The account's personal upload link. |
| name | string | null | |
| prompt | string | null | |
| max_files | string | integer | null | |
| max_bytes_per_file | string | null | |
| max_bytes_per_upload | string | null | |
| max_files_per_upload | string | integer | null | |
| max_total_bytes | string | null | |
| max_total_uploads | string | integer | null | |
| max_bytes_per_person | string | null | |
| max_uploads_per_person | string | integer | null | |
| show_space_left | boolean | The page shows uploaders how much room is left. |
| usage | object | null | |
| usage.received_bytes | string | Bytes, as a decimal string. |
| usage.received_files | string | integer | |
| usage.uploads | string | integer | |
| usage.reserved_bytes | string | Bytes held for uploads still landing. |
| files_received | string | integer | |
| has_password | boolean | |
| notify | object | null | |
| notify.emails? | array of string | |
| notify.webhook_url? | string | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| retention_days | string | integer | null | |
| effective_retention_days | string | integer | |
| allowed_types | array of string | null | |
| reject_flagged | boolean | |
| brand_id | string | null | |
| questions | array of UploadQuestion | |
| questions.id | string | |
| questions.label | string | |
| questions.type | "short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent" | |
| questions.required | boolean | |
| questions.help? | string | |
| questions.options? | array of string | |
| questions.settings? | object | The 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_contact | boolean | |
| view_only | boolean | Received PDFs and images are view-only (team seats). |
| member_mark | boolean | Each preview is the member's own copy with their email on it. |
| view_only_other | "download" | "block" | |
| team_visible | boolean | The team's Owner and Admins see what arrives through this page (team seats). |
| embed_origins | array of string | The sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site. |
| widget_share_answers | boolean | The host page's `all_complete` widget event carries the uploader's answers. |
| widget | object | The script widget for this page. |
| widget.allowed | boolean | The plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan. |
| widget.needs | string | The plan the script widget needs. |
| widget.secret_issued | boolean | A widget secret was issued (GET /v1/file-requests/:id/widget-secret). |
| e2e | boolean | Uploads 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_available | boolean | The plan has encrypted uploads (Pro, Team Pro, Team Enterprise). |
| expires_at | date-time | null | |
| created_at | date-time | |
| updated_at | date-time | |
| notify_webhook_secret? | string | The 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
| id | string | The file request's id. |
Response 200
| id | string | |
| slug | string | |
| url | string | The upload page, https://sendflo.app/u/{slug}. |
| state | "open" | "fulfilled" | "cancelled" | "expired" | |
| kind | "request" | "reception" | |
| personal | boolean | The account's personal upload link. |
| name | string | null | |
| prompt | string | null | |
| max_files | string | integer | null | |
| max_bytes_per_file | string | null | |
| max_bytes_per_upload | string | null | |
| max_files_per_upload | string | integer | null | |
| max_total_bytes | string | null | |
| max_total_uploads | string | integer | null | |
| max_bytes_per_person | string | null | |
| max_uploads_per_person | string | integer | null | |
| show_space_left | boolean | The page shows uploaders how much room is left. |
| usage | object | null | |
| usage.received_bytes | string | Bytes, as a decimal string. |
| usage.received_files | string | integer | |
| usage.uploads | string | integer | |
| usage.reserved_bytes | string | Bytes held for uploads still landing. |
| files_received | string | integer | |
| has_password | boolean | |
| notify | object | null | |
| notify.emails? | array of string | |
| notify.webhook_url? | string | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| retention_days | string | integer | null | |
| effective_retention_days | string | integer | |
| allowed_types | array of string | null | |
| reject_flagged | boolean | |
| brand_id | string | null | |
| questions | array of UploadQuestion | |
| questions.id | string | |
| questions.label | string | |
| questions.type | "short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent" | |
| questions.required | boolean | |
| questions.help? | string | |
| questions.options? | array of string | |
| questions.settings? | object | The 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_contact | boolean | |
| view_only | boolean | Received PDFs and images are view-only (team seats). |
| member_mark | boolean | Each preview is the member's own copy with their email on it. |
| view_only_other | "download" | "block" | |
| team_visible | boolean | The team's Owner and Admins see what arrives through this page (team seats). |
| embed_origins | array of string | The sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site. |
| widget_share_answers | boolean | The host page's `all_complete` widget event carries the uploader's answers. |
| widget | object | The script widget for this page. |
| widget.allowed | boolean | The plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan. |
| widget.needs | string | The plan the script widget needs. |
| widget.secret_issued | boolean | A widget secret was issued (GET /v1/file-requests/:id/widget-secret). |
| e2e | boolean | Uploads 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_available | boolean | The plan has encrypted uploads (Pro, Team Pro, Team Enterprise). |
| expires_at | date-time | null | |
| created_at | date-time | |
| updated_at | date-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
| id | string | The file request's id. |
Request body (JSON)
| name? | string | null | Your own label, never shown to uploaders (120 characters at most). |
| prompt? | string | null | The 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 | integer | 1–50 (1 by default); ignored for `reception`. |
| max_bytes_per_file? | string | integer | null | A size limit per file, in bytes; null = none. |
| max_bytes_per_upload? | string | integer | null | A size limit for the files someone sends together, in bytes; null = none. Every plan. |
| max_files_per_upload? | string | integer | null | How many files someone can send together (1–1000); null = the plan's files per transfer. |
| max_total_bytes? | string | integer | null | A 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 | null | How many uploads the page accepts in all (1–1,000,000); null = no limit. Every plan. |
| max_bytes_per_person? | string | integer | null | A 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 | null | How many uploads one verified uploader can send (1–100,000); null = no limit. Paid plans; like `max_bytes_per_person`. |
| show_space_left? | boolean | Show uploaders how much room the page has left. |
| password? | string | null | An upload-page password; null = none. |
| notify? | object | null | Who'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 | null | Required for `restricted` (100 at most). |
| retention_days? | string | integer | null | How long received files are kept; null = the plan's default. |
| allowed_types? | array of string | null | File categories (image, video, document…) or extensions; empty = any type. |
| reject_flagged? | boolean | Delete uploads the malware scan blocks (true by default). |
| brand_id? | string | null | A brand kit of this workspace; null = the default kit. |
| deliver_to_inbox_id? | string | An inbox that also receives the uploads. |
| expires_at? | string | null | When the page stops accepting uploads; null = never. |
| questions? | array of object | null | Up 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.label | string | |
| questions.type | string | |
| questions.required? | boolean | |
| questions.options? | array of string | |
| questions.help? | string | |
| questions.settings? | map of string | number | boolean | null | |
| require_contact? | boolean | Uploaders must give their name and email. |
| view_only? | boolean | Team 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? | boolean | Team 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" | null | While `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? | boolean | Team 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 | null | The 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? | boolean | The script widget's `all_complete` event, sent to the host page, carries the uploader's answers. Off by default. Pro and up. |
| e2e? | boolean | Encrypt 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
| id | string | |
| slug | string | |
| url | string | The upload page, https://sendflo.app/u/{slug}. |
| state | "open" | "fulfilled" | "cancelled" | "expired" | |
| kind | "request" | "reception" | |
| personal | boolean | The account's personal upload link. |
| name | string | null | |
| prompt | string | null | |
| max_files | string | integer | null | |
| max_bytes_per_file | string | null | |
| max_bytes_per_upload | string | null | |
| max_files_per_upload | string | integer | null | |
| max_total_bytes | string | null | |
| max_total_uploads | string | integer | null | |
| max_bytes_per_person | string | null | |
| max_uploads_per_person | string | integer | null | |
| show_space_left | boolean | The page shows uploaders how much room is left. |
| usage | object | null | |
| usage.received_bytes | string | Bytes, as a decimal string. |
| usage.received_files | string | integer | |
| usage.uploads | string | integer | |
| usage.reserved_bytes | string | Bytes held for uploads still landing. |
| files_received | string | integer | |
| has_password | boolean | |
| notify | object | null | |
| notify.emails? | array of string | |
| notify.webhook_url? | string | |
| access | "public" | "tracked" | "restricted" | |
| allowed_emails | array of string | |
| retention_days | string | integer | null | |
| effective_retention_days | string | integer | |
| allowed_types | array of string | null | |
| reject_flagged | boolean | |
| brand_id | string | null | |
| questions | array of UploadQuestion | |
| questions.id | string | |
| questions.label | string | |
| questions.type | "short_text" | "long_text" | "email" | "number" | "date" | "multiple_choice" | "opinion_scale" | "phone" | "yes_no" | "consent" | |
| questions.required | boolean | |
| questions.help? | string | |
| questions.options? | array of string | |
| questions.settings? | object | The 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_contact | boolean | |
| view_only | boolean | Received PDFs and images are view-only (team seats). |
| member_mark | boolean | Each preview is the member's own copy with their email on it. |
| view_only_other | "download" | "block" | |
| team_visible | boolean | The team's Owner and Admins see what arrives through this page (team seats). |
| embed_origins | array of string | The sites allowed to frame the script widget (Pro, Team Pro, Team Enterprise): exact origins, `https://*.example.com` for subdomains. Empty = any site. |
| widget_share_answers | boolean | The host page's `all_complete` widget event carries the uploader's answers. |
| widget | object | The script widget for this page. |
| widget.allowed | boolean | The plan has the script widget (Pro, Team Pro, Team Enterprise). The plain iframe works on every plan. |
| widget.needs | string | The plan the script widget needs. |
| widget.secret_issued | boolean | A widget secret was issued (GET /v1/file-requests/:id/widget-secret). |
| e2e | boolean | Uploads 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_available | boolean | The plan has encrypted uploads (Pro, Team Pro, Team Enterprise). |
| expires_at | date-time | null | |
| created_at | date-time | |
| updated_at | date-time | |
| notify_webhook_secret? | string | The 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
| id | string | The file request's id. |
Response 200
| ok | true |
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
| id | string | The file request's id. |
Query parameters
| limit? | string | integer | How many (1–200, 50 by default). |
Response 200
| data | array of WebhookDelivery | |
| data.id | string | |
| data.event_id | string | The event's id — the `webhook-id` header; the same on every attempt and on Resend. |
| data.event_type | string | |
| data.attempt | string | integer | 1–6; 0 for a Resend. |
| data.ok | boolean | Your server answered 2xx. |
| data.status_code | string | integer | null | |
| data.error | string | null | |
| data.duration_ms | string | integer | null | |
| data.response_excerpt | string | null | |
| data.created_at | date-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
| id | string | The file request's id. |
| deliveryId | string | A delivery from the log. |
Response 202
| queued | true | |
| event_id | string |
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
| id | string | The file request's id. |
Response 200
| id | string | |
| webhook_paused_at | null | |
| webhook_failed_events | string | 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
| id | string | The file request's id. |
Response 200
| secret | string | null | |
| version | string | integer | How 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
| id | string | The file request's id. |
Response 200
| secret | string | |
| version | string | 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
| data | array of Inbox | |
| data.id | string | |
| data.name | string | null | |
| data.address | string | |
| data.default_ttl_seconds | string | integer | How long received items are kept. |
| data.created_at | date-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"
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? | string | Your label (120 characters at most). |
| default_ttl_seconds? | string | integer | How long items are kept: 60 seconds to 30 days (7 days by default). |
A name ending in ? is optional.
Response 201
| id | string | |
| address | string | |
| url | string |
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
| id | string | The inbox's id. |
Response 200
| data | array of InboxItem | |
| data.id | string | |
| data.file | object | |
| data.file.id | string | |
| data.file.name | string | |
| data.file.size | string | |
| data.file.content_type | string | null | |
| data.file.scan_state | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | |
| data.from | string | null | |
| data.metadata | map of any | null | |
| data.received_at | date-time | |
| data.expires_at | date-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? | string | Event types, comma-separated. |
| since? | string | ISO time. |
| cursor? | string | The previous page's `next`. |
| limit? | string | integer | 1–100 (50). |
Response 200
| data | array of StoredEvent | |
| data.id | string | |
| data.type | string | |
| data.version | string | integer | |
| data.created_at | date-time | |
| data.data | any | |
| next | string | 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/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
| id | string | An 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
| id | string | A 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
| data | array of WebhookEndpoint | |
| data.id | string | |
| data.url | string | |
| data.events | array of string | |
| data.disabled | boolean | |
| data.failure_count | string | integer | Failed attempts since the last success. |
| data.failed_events | string | integer | Events in a row whose every attempt failed; the endpoint pauses at 5. |
| data.paused_at | date-time | null | |
| data.team | boolean | A team-wide endpoint: every member's events. |
| data.created_at | date-time | |
| data.description | string | null | |
| data.filters | any | null | |
| data.payload_version | string | integer | The payload version this endpoint receives (2). |
| data.app | string | null | |
| data.plan_paused | boolean | Paused because the plan has no webhooks; an upgrade resumes it. |
| data.plan_pause_at | date-time | null | |
| data.paused_reason | "failing" | "address" | "manual" | "staff" | null | |
| data.secret_rotated_at | date-time | null | |
| data.deliveries_24h? | string | integer | |
| data.success_rate_24h? | number | null | |
| allowed | boolean | This 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"
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)
| url | string | An https URL (2048 characters at most). |
| events | array of string | The events to receive. |
| team? | boolean | Team 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 | null | What 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
| id | string | |
| url | string | |
| events | array of string | |
| team | boolean | |
| secret | string | Shown only now. |
| description | string | null | |
| filters | any | null | |
| payload_version | string | integer | |
| app | string | null | |
| disabled | boolean | |
| plan_paused | boolean | |
| plan_pause_at | date-time | null | |
| failure_count | string | integer | |
| failed_events | string | integer | |
| paused_at | date-time | null | |
| paused_reason | string | null | |
| secret_rotated_at | date-time | null | |
| created_at | date-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
| data | array of object | |
| data.type | string | |
| data.family | string | |
| data.version | string | integer | |
| data.summary | string | |
| data.when | string | |
| data.team_only | boolean | |
| data.sample | any | A 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"
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
| id | string | The endpoint's id. |
Request body (JSON)
| url? | string | An https, public URL. |
| events? | array of string | The events to receive. |
| description? | string | null | What it's for (300 characters). |
| filters? | object | null | Only 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? | boolean | On 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
| id | string | The 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
| id | string | The endpoint's id. |
Query parameters
| limit? | string | integer | How many (1–200, 50 by default). |
Response 200
| data | array of WebhookDelivery | |
| data.id | string | |
| data.event_id | string | The event's id — the `webhook-id` header; the same on every attempt and on Resend. |
| data.event_type | string | |
| data.attempt | string | integer | 1–6; 0 for a Resend. |
| data.ok | boolean | Your server answered 2xx. |
| data.status_code | string | integer | null | |
| data.error | string | null | |
| data.duration_ms | string | integer | null | |
| data.response_excerpt | string | null | |
| data.created_at | date-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
| id | string | The endpoint's id. |
| deliveryId | string | A delivery from the log. |
Response 202
| queued | true | |
| event_id | string |
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
| id | string | The endpoint's id. |
Request body (JSON)
| since | string | ISO time the window starts. |
| until? | string | ISO time it ends (now by default). |
| types? | array of string | Only these event types. |
| only_failed? | boolean | Only events this endpoint never received. |
A name ending in ? is optional.
Response 202
| queued | string | 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
| id | string | The endpoint's id. |
Response 200
| id | string | |
| disabled | boolean | |
| paused_at | null | |
| failed_events | string | 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
| id | string | The endpoint's id. |
Response 200
| id | string | |
| secret | string | |
| previous_valid_until | date-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
| id | string | The endpoint's id. |
Request body (JSON)
| type? | string | An event type (file_request.fulfilled by default). |
A name ending in ? is optional.
Response 202
| queued | boolean | |
| event_id | string | |
| type | string |
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
| data | array of NotificationChannel | |
| data.id | string | |
| data.channel | "email" | "slack" | "discord" | "teams" | "webhook" | |
| data.name | string | |
| data.target | string | The address or URL, partly masked. |
| data.active | boolean | |
| data.pending? | true | |
| data.detail? | string | |
| limits | NotificationChannelLimits | |
| limits.email | object | |
| limits.email.used | string | integer | |
| limits.email.allowance | string | integer | |
| limits.slack | object | |
| limits.slack.used | string | integer | |
| limits.slack.allowance | string | integer | |
| limits.discord | object | |
| limits.discord.used | string | integer | |
| limits.discord.allowance | string | integer | |
| limits.teams | object | |
| limits.teams.used | string | integer | |
| limits.teams.allowance | string | integer | |
| limits.webhook | object | |
| limits.webhook.used | string | integer | |
| limits.webhook.allowance | string | 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"
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)
| channel | string | `email`, `slack`, `discord`, `teams` or `webhook`. |
| name | string | Your label (120 characters at most). |
| target | string | An email address or an https webhook URL. |
A name ending in ? is optional.
Response 200
| id | string | |
| 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
| id | string | The channel's id. |
Request body (JSON)
| name? | string | Your label. |
| target? | string | A new address or URL. |
| active? | boolean | On or off. |
A name ending in ? is optional.
Response 200
| ok | true | |
| 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
| id | string | The channel's id. |
Response 200
| ok | true |
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
| id | string | The channel's id. |
Response 200
| ok | true |
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
| id | string | The id of any file (link) in the transfer. |
Response 200
| settings | FeedbackSettings | The transfer's feedback settings. |
| settings.comments | "off" | "open" | "private" | |
| settings.ratings | boolean | |
| settings.favorites | boolean | |
| settings.approvals | boolean | |
| settings.rating_scale | "stars5" | "score10" | |
| settings.encrypted | boolean | An end-to-end encrypted transfer: comment bodies are sealed envelopes (sfx1.<iv>.<ct>). |
| root_id | string | The transfer (its first link's id). |
| ended_at | date-time | null | |
| kept_until | date-time | null | |
| all_approved | boolean | Approvals are on, every live file is approved, and nobody asked for changes. |
| files | array of FeedbackFileSummary | |
| files.link_id | string | |
| files.slug | string | |
| files.name | string | |
| files.live | boolean | Not revoked, not expired, already available. |
| files.comments | string | integer | |
| files.hidden | string | integer | |
| files.favorites | string | integer | |
| files.rating | object | |
| files.rating.count | string | integer | |
| files.rating.average | number | null | |
| files.rating.distribution | map of string | integer | Rating value → how many gave it. |
| files.approvals | object | |
| files.approvals.approved | string | integer | |
| files.approvals.changes | string | integer | |
| reviewers | array of FeedbackReviewerSummary | |
| reviewers.id | string | |
| reviewers.name | string | |
| reviewers.email | string | |
| reviewers.proof | "account" | "access" | "email" | |
| reviewers.sender | boolean | |
| reviewers.blocked | boolean | |
| reviewers.created_at | date-time | |
| reviewers.last_active_at | date-time | |
| reviewers.comments | string | integer | Their comments (deleted ones with replies included). |
| reviewers.ratings | string | integer | |
| reviewers.favorites | string | integer | |
| reviewers.approvals | string | 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
| id | string | The id of any file (link) in the transfer. |
Query parameters
| file | string | The file's link id or slug (it must be in this transfer). |
| after? | string | The `next` of the previous page. |
Response 200
| comments | array of FeedbackComment | |
| comments.id | string | |
| comments.file | string | The file's link slug. |
| comments.parent_id | string | null | |
| comments.body | string | null | |
| comments.encrypted? | true | |
| comments.anchor | FeedbackAnchor | null | |
| comments.created_at | date-time | |
| comments.edited | boolean | |
| comments.resolved | boolean | |
| comments.mine | boolean | |
| comments.hidden | boolean | Hidden from recipients (a block, or Trust & Safety). |
| comments.author | object | |
| comments.author.name | string | |
| comments.author.verified | true | |
| comments.author.proof | "account" | "access" | "email" | |
| comments.author.sender | boolean | |
| comments.author.email | string | |
| comments.author.id | string | The person's reviewer id (for block and unblock). |
| comments.author.blocked | boolean | |
| next | string | 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
| id | string | The id of any file (link) in the transfer. |
| cid | string | The 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
| id | string | The id of any file (link) in the transfer. |
| cid | string | The top-level comment's id. |
Response 200
| comment | object | |
| comment.id | string | |
| comment.resolved | boolean |
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
| id | string | The id of any file (link) in the transfer. |
| cid | string | The top-level comment's id. |
Response 200
| comment | object | |
| comment.id | string | |
| comment.resolved | boolean |
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
| id | string | The 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
| id | string | The id of any file (link) in the transfer. |
| rid | string | The person's reviewer id (`author.id`). |
Response 200
| reviewer | FeedbackReviewer | A person giving feedback on one transfer. |
| reviewer.id | string | |
| reviewer.name | string | |
| reviewer.email | string | |
| reviewer.proof | "account" | "access" | "email" | |
| reviewer.sender | boolean | |
| reviewer.blocked | boolean | |
| reviewer.created_at | date-time | |
| reviewer.last_active_at | date-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
| id | string | The id of any file (link) in the transfer. |
| rid | string | The person's reviewer id. |
Response 200
| reviewer | FeedbackReviewer | A person giving feedback on one transfer. |
| reviewer.id | string | |
| reviewer.name | string | |
| reviewer.email | string | |
| reviewer.proof | "account" | "access" | "email" | |
| reviewer.sender | boolean | |
| reviewer.blocked | boolean | |
| reviewer.created_at | date-time | |
| reviewer.last_active_at | date-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
| id | string | The id of any file (link) in the transfer. |
Response 200
| replies | TransferReplies | |
| replies.id | string | |
| replies.from_email | string | The replier's verified address. |
| replies.body | string | null | |
| replies.encrypted | boolean | |
| replies.created_at | date-time | |
| replies.files_expected | string | integer | How many files the replier said they'd send. |
| replies.files | array of object | |
| replies.files.item_id | string | |
| replies.files.file_id | string | |
| replies.files.name | string | |
| replies.files.sealed_name? | string | |
| replies.files.size | string | |
| replies.files.scan | string | |
| replies.key_half? | string | A 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_off | boolean | The sender turned replies off for this transfer. |
| replies_on | boolean | Recipients 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_id | string | null | |
| logo_key | string | null | |
| wallpaper_key | string | null | |
| accent | string | null | |
| heading_color | string | null | |
| page_text | string | null | |
| link_url | string | null | |
| link_label | string | null | |
| logo_url | string | null | |
| wallpaper_url | string | 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"
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? | string | The kit's name, used only when this creates it. |
| logo_key? | string | null | A key from POST /v1/branding/assets; null or "" clears it. |
| wallpaper_key? | string | null | Like `logo_key`. |
| accent? | string | null | A colour, `#RRGGBB`. |
| heading_color? | string | null | A colour, `#RRGGBB`. |
| page_text? | string | null | Text shown on your pages (500 characters at most). |
| link_url? | string | null | An `https:` link shown on your pages. |
| link_label? | string | null | The link's label (80 characters at most). |
| thank_you_text? | string | null | Shown 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? | boolean | Mail 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 | null | Where replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit. |
| redirect_url? | string | null | An `https:` address uploaders are sent to after their files arrive. Plus and up. |
| favicon_key? | string | null | A key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up. |
| tab_title? | string | null | The browser tab's title (70 characters). Plus and up. |
| og_image_key? | string | null | A key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up. |
| og_title? | string | null | The link preview's title (90 characters). Plus and up. |
| og_description? | string | null | The 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 | null | Up to 4 more wallpaper keys: the page rotates through all of them. Pro and up. |
| video_key? | string | null | A 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 | null | The upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up. |
| texts? | map of string | null | Your own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up. |
A name ending in ? is optional.
Response 200
| ok | true | |
| logo_url | string | null | |
| wallpaper_url | string | 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_type | string | An image type (`image/…`), or `video/mp4` / `video/webm` for a video. |
| size | string | integer | The file's exact size in bytes. |
A name ending in ? is optional.
Response 200
| key | string | |
| put_url | string |
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
| data | array of Brand | |
| data.id | string | |
| data.name | string | |
| data.is_default | boolean | |
| data.logo_key | string | null | |
| data.wallpaper_key | string | null | |
| data.accent | string | null | |
| data.heading_color | string | null | |
| data.page_text | string | null | |
| data.link_url | string | null | |
| data.link_label | string | null | |
| data.thank_you_text | string | null | |
| data.appearance | "auto" | "light" | "dark" | null | |
| data.receipt | boolean | |
| data.sender_name | string | null | |
| data.reply_to | string | null | |
| data.reply_to_verified | boolean | Replies go to `reply_to` only once it was confirmed with the code mailed to it. |
| data.redirect_url | string | null | |
| data.favicon_key | string | null | |
| data.tab_title | string | null | |
| data.og_image_key | string | null | |
| data.og_title | string | null | |
| data.og_description | string | null | |
| data.font | string | null | |
| data.extra_wallpaper_keys | array of string | |
| data.video_key | string | null | |
| data.language | string | null | |
| data.texts | object | null | |
| data.texts.heading? | string | |
| data.texts.intro? | string | |
| data.texts.button? | string | |
| data.logo_url | string | null | |
| data.wallpaper_url | string | null | |
| data.previews | object | |
| data.previews.favicon_url | string | null | |
| data.previews.og_image_url | string | null | |
| data.previews.video_url | string | null | |
| data.previews.extra_wallpaper_urls | array of string | null | |
| data.created_at | date-time | |
| data.updated_at | date-time | |
| limit | object | |
| limit.used | string | integer | |
| limit.max | string | integer | Kits the plan allows (0 on Free). |
| limit.plan_class | "free" | "starter" | "plus" | "pro" | "enterprise" | |
| options | map of object | Each 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_until | date-time | null | |
| team | object | null | |
| team.plan_id | string | The team (its subscription id). |
| team.allowed | boolean | Team Plus and up: the team's kits and its lock apply. |
| team.needs | string | The plan team kits need (Team Plus). |
| team.kits | array of Brand | The team's kits, read-only here: a page or link may name any of them. |
| team.kits.id | string | |
| team.kits.name | string | |
| team.kits.is_default | boolean | |
| team.kits.logo_key | string | null | |
| team.kits.wallpaper_key | string | null | |
| team.kits.accent | string | null | |
| team.kits.heading_color | string | null | |
| team.kits.page_text | string | null | |
| team.kits.link_url | string | null | |
| team.kits.link_label | string | null | |
| team.kits.thank_you_text | string | null | |
| team.kits.appearance | "auto" | "light" | "dark" | null | |
| team.kits.receipt | boolean | |
| team.kits.sender_name | string | null | |
| team.kits.reply_to | string | null | |
| team.kits.reply_to_verified | boolean | Replies go to `reply_to` only once it was confirmed with the code mailed to it. |
| team.kits.redirect_url | string | null | |
| team.kits.favicon_key | string | null | |
| team.kits.tab_title | string | null | |
| team.kits.og_image_key | string | null | |
| team.kits.og_title | string | null | |
| team.kits.og_description | string | null | |
| team.kits.font | string | null | |
| team.kits.extra_wallpaper_keys | array of string | |
| team.kits.video_key | string | null | |
| team.kits.language | string | null | |
| team.kits.texts | object | null | |
| team.kits.logo_url | string | null | |
| team.kits.wallpaper_url | string | null | |
| team.kits.previews | object | |
| team.kits.created_at | date-time | |
| team.kits.updated_at | date-time | |
| team.policy | object | |
| team.policy.locked | boolean | Members 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_id | string | null | |
| team.can_manage | boolean | The 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"
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? | string | The kit's name ("Brand N" when blank). |
| logo_key? | string | null | A key from POST /v1/branding/assets; null or "" clears it. |
| wallpaper_key? | string | null | Like `logo_key`. |
| accent? | string | null | A colour, `#RRGGBB`. |
| heading_color? | string | null | A colour, `#RRGGBB`. |
| page_text? | string | null | Text shown on your pages (500 characters at most). |
| link_url? | string | null | An `https:` link shown on your pages. |
| link_label? | string | null | The link's label (80 characters at most). |
| thank_you_text? | string | null | Shown 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? | boolean | Mail 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 | null | Where replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit. |
| redirect_url? | string | null | An `https:` address uploaders are sent to after their files arrive. Plus and up. |
| favicon_key? | string | null | A key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up. |
| tab_title? | string | null | The browser tab's title (70 characters). Plus and up. |
| og_image_key? | string | null | A key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up. |
| og_title? | string | null | The link preview's title (90 characters). Plus and up. |
| og_description? | string | null | The 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 | null | Up to 4 more wallpaper keys: the page rotates through all of them. Pro and up. |
| video_key? | string | null | A 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 | null | The upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up. |
| texts? | map of string | null | Your own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up. |
A name ending in ? is optional.
Response 201
| id | string | |
| name | string | |
| is_default | boolean | |
| logo_key | string | null | |
| wallpaper_key | string | null | |
| accent | string | null | |
| heading_color | string | null | |
| page_text | string | null | |
| link_url | string | null | |
| link_label | string | null | |
| thank_you_text | string | null | |
| appearance | "auto" | "light" | "dark" | null | |
| receipt | boolean | |
| sender_name | string | null | |
| reply_to | string | null | |
| reply_to_verified | boolean | Replies go to `reply_to` only once it was confirmed with the code mailed to it. |
| redirect_url | string | null | |
| favicon_key | string | null | |
| tab_title | string | null | |
| og_image_key | string | null | |
| og_title | string | null | |
| og_description | string | null | |
| font | string | null | |
| extra_wallpaper_keys | array of string | |
| video_key | string | null | |
| language | string | null | |
| texts | object | null | |
| texts.heading? | string | |
| texts.intro? | string | |
| texts.button? | string | |
| logo_url | string | null | |
| wallpaper_url | string | null | |
| previews | object | |
| previews.favicon_url | string | null | |
| previews.og_image_url | string | null | |
| previews.video_url | string | null | |
| previews.extra_wallpaper_urls | array of string | null | |
| created_at | date-time | |
| updated_at | date-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
| id | string | The kit's id. |
Request body (JSON)
| name? | string | The kit's name. |
| logo_key? | string | null | A key from POST /v1/branding/assets; null or "" clears it. |
| wallpaper_key? | string | null | Like `logo_key`. |
| accent? | string | null | A colour, `#RRGGBB`. |
| heading_color? | string | null | A colour, `#RRGGBB`. |
| page_text? | string | null | Text shown on your pages (500 characters at most). |
| link_url? | string | null | An `https:` link shown on your pages. |
| link_label? | string | null | The link's label (80 characters at most). |
| thank_you_text? | string | null | Shown 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? | boolean | Mail 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 | null | Where replies to those emails go; used only once confirmed with POST /v1/brands/:id/reply-to/send and /confirm. Every kit. |
| redirect_url? | string | null | An `https:` address uploaders are sent to after their files arrive. Plus and up. |
| favicon_key? | string | null | A key from POST /v1/branding/assets (`kind: favicon`): the tab icon. Plus and up. |
| tab_title? | string | null | The browser tab's title (70 characters). Plus and up. |
| og_image_key? | string | null | A key (`kind: og`, PNG, JPEG, WebP or GIF): the image chat apps show for the page's link. Plus and up. |
| og_title? | string | null | The link preview's title (90 characters). Plus and up. |
| og_description? | string | null | The 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 | null | Up to 4 more wallpaper keys: the page rotates through all of them. Pro and up. |
| video_key? | string | null | A 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 | null | The upload page's language: `en`, `de`, `es`, `fr`, `it`, `nl` or `pt`. Pro and up. |
| texts? | map of string | null | Your own words for the upload page: `heading` (60), `intro` (200), `button` (40). Pro and up. |
A name ending in ? is optional.
Response 200
| id | string | |
| name | string | |
| is_default | boolean | |
| logo_key | string | null | |
| wallpaper_key | string | null | |
| accent | string | null | |
| heading_color | string | null | |
| page_text | string | null | |
| link_url | string | null | |
| link_label | string | null | |
| thank_you_text | string | null | |
| appearance | "auto" | "light" | "dark" | null | |
| receipt | boolean | |
| sender_name | string | null | |
| reply_to | string | null | |
| reply_to_verified | boolean | Replies go to `reply_to` only once it was confirmed with the code mailed to it. |
| redirect_url | string | null | |
| favicon_key | string | null | |
| tab_title | string | null | |
| og_image_key | string | null | |
| og_title | string | null | |
| og_description | string | null | |
| font | string | null | |
| extra_wallpaper_keys | array of string | |
| video_key | string | null | |
| language | string | null | |
| texts | object | null | |
| texts.heading? | string | |
| texts.intro? | string | |
| texts.button? | string | |
| logo_url | string | null | |
| wallpaper_url | string | null | |
| previews | object | |
| previews.favicon_url | string | null | |
| previews.og_image_url | string | null | |
| previews.video_url | string | null | |
| previews.extra_wallpaper_urls | array of string | null | |
| created_at | date-time | |
| updated_at | date-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
| id | string | The kit's id. |
Response 200
| ok | true |
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
| id | string | The kit's id. |
Response 200
| ok | true |
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
| id | string | The kit's id. |
Request body (JSON)
| code | string | The 6-digit code from the email. |
A name ending in ? is optional.
Response 200
| ok | true | |
| verified | true |
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
| id | string | The kit's id. |
Response 200
| ok | true | |
| verified | boolean | |
| 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
| rosters | array of AgentRoster | |
| rosters.device_id | string | |
| rosters.device_name | string | |
| rosters.platform | string | |
| rosters.online | boolean | |
| rosters.agents | array of RosterAgent | |
| rosters.agents.id | string | |
| rosters.agents.name | string | |
| rosters.agents.runner | "folder" | "command" | "acp" | "channel" | "session" | |
| rosters.agents.product? | string | |
| rosters.agents.enabled | boolean | |
| 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.e2e | boolean | The computer published a key, so instructions can be sealed to it. |
| rosters.public_key | AgentPublicKey | null | |
| rosters.public_key.kid | string | |
| rosters.public_key.jwk | object | |
| rosters.connector | object | null | |
| rosters.connector.version? | string | |
| rosters.connector.platform? | string | |
| rosters.updated_at | date-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/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
| targets | array of AgentTarget | |
| targets.kind | "project" | |
| targets.id | string | project:<id> |
| targets.project_id | string | |
| targets.name | string | |
| targets.destination | string | |
| targets.device | object | |
| targets.device.id | string | |
| targets.device.name | string | null | |
| targets.device.platform | string | null | |
| targets.device.online | boolean | |
| targets.agent | object | |
| targets.agent.ref | string | |
| targets.agent.name | string | null | |
| targets.agent.runner | string | null | |
| targets.agent.product | string | null | |
| targets.agent.status | "ready" | "busy" | "paused" | null | |
| targets.agent.presets | array of AgentPreset | |
| targets.agent.sessions | array of object | |
| targets.agent.accept | AgentAccept | null | |
| targets.agent.policy | object | null | |
| targets.binding | object | |
| targets.binding.agent_ref | string | |
| 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.available | boolean | |
| targets.unavailable | "device_gone" | "no_connector" | "agent_missing" | "agent_paused" | null | |
| targets.public_key | AgentPublicKey | null | |
| targets.public_key.kid | string | |
| targets.public_key.jwk | object |
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/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? | string | Search words (`box=out` or `in`). |
| page? | string | With `q`: the page of results, 1–8. |
| box? | "out" | "in" | "device" | "pull" | `out`, `in` or `pull` (`device` is for the SendFlo connector). |
| agent? | string | For `box=pull`: only this cloud agent's tasks. |
| state? | string | For `box=out`: only tasks in this stored state. |
| before? | string | For `box=out`: tasks older than this id. |
| limit? | string | Tasks per page, 1–100 (50 by default). |
| active? | string | For the SendFlo connector. |
Response 200
| tasks | array of AgentTask | |
| tasks.id | string | |
| tasks.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| tasks.label | string | The state in plain words ("Done", "Needs your answer"…). |
| tasks.target_kind | "device" | "cloud" | |
| tasks.agent | object | |
| tasks.agent.ref | string | |
| tasks.agent.name | string | |
| tasks.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| tasks.device | object | null | |
| tasks.device.id | string | |
| tasks.device.name | string | null | |
| tasks.project_id | string | null | |
| tasks.cloud_agent_id | string | null | |
| tasks.session_ref | string | null | |
| tasks.from | object | |
| tasks.from.name | string | |
| tasks.from.user_id | string | |
| tasks.from.device_id | string | null | |
| tasks.instruction | string | null | |
| tasks.instruction_encrypted | boolean | |
| tasks.preset | string | null | |
| tasks.voice_file | string | null | |
| tasks.files | array of object | |
| tasks.files.name | string | |
| tasks.files.size | string | integer | |
| tasks.files.sha256? | string | |
| tasks.files.mime? | string | |
| tasks.total_bytes | string | Bytes, as a decimal string. |
| tasks.reason | string | null | |
| tasks.detail | string | null | |
| tasks.question | AgentTaskQuestion | null | |
| tasks.question.id | string | |
| tasks.question.kind | "question" | "permission" | "approval" | |
| tasks.question.text | string | |
| tasks.question.options? | array of object | |
| tasks.question.free_text? | boolean | |
| tasks.question.asked_at | date-time | |
| tasks.question.answer? | object | |
| tasks.result | AgentTaskResult | null | |
| tasks.result.summary | string | null | |
| tasks.result.files | array of object | |
| tasks.result.kept_at | string | null | |
| tasks.result.expires_at | date-time | null | |
| tasks.result.data? | any | Free-form JSON the agent returned. |
| tasks.policy | object | null | |
| tasks.policy.ask_first? | boolean | |
| tasks.policy.direct_only? | boolean | |
| tasks.policy.results? | "send" | "keep" | |
| tasks.device_transfer_id | string | null | |
| tasks.transfer_state | string | null | |
| tasks.cancel_requested | boolean | |
| tasks.retry_of | string | null | |
| tasks.role | "sender" | "owner" | "both" | null | |
| tasks.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| tasks.attempts | string | integer | |
| tasks.created_at | date-time | |
| tasks.updated_at | date-time | |
| tasks.delivered_at | date-time | null | |
| tasks.started_at | date-time | null | |
| tasks.finished_at | date-time | null | |
| tasks.expires_at | date-time | |
| search? | SearchMeta | Present when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first. |
| search.engine | "meilisearch" | "postgres" | |
| search.page | string | integer | This page (1–8). |
| search.total | string | integer | About how many match (the search engine's estimate). |
| search.next_page | string | 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"
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_id | string | One of this workspace's cloud agents. |
| file_ids | array of string | 1–50 stored file ids. |
| instruction? | string | What the agent should do (8000 characters at most). |
| preset? | string | A preset id of the agent. |
| voice_file? | string | The name of one of the files that holds a spoken instruction. |
| instruction_enc? | any | Not for cloud agents (422 `e2e_not_for_cloud`). |
| from_name? | string | The sender name the agent sees (the key creator's name by default). |
A name ending in ? is optional.
Response 200
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| id | string | The task's id. |
Response 200
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-time | |
| events | array of AgentTaskEvent | |
| events.id | string | |
| events.at | date-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.message | string | null | |
| events.data | any | Free-form JSON, or null. |
| retries? | array of AgentTaskRetryRef | |
| retries.id | string | |
| retries.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| retries.created_at | date-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
| id | string | The task's id. |
Request body (JSON)
| question_id | string | The open question's id. |
| option_id? | string | null | One of the question's options. |
| text? | string | null | A free-text answer. |
A name ending in ? is optional.
Response 200
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-time | |
| reached? | boolean | Whether 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
| id | string | The task's id. |
Response 200
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-time | |
| reached? | boolean | Whether 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
| id | string | The task's id (from GET /v1/agent-tasks?box=pull). |
Response 200
| type | "agent_task" | |
| api | 1 | |
| task | object | |
| task.id | string | |
| task.instruction | string | |
| task.preset | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.mime | string | null | |
| task.files.url | string | A download URL valid for 7 days. |
| task.from | object | |
| task.from.name | string | |
| task.created_at | date-time | |
| task.expires_at | date-time | |
| callback | object | |
| callback.base | string | |
| callback.token | string | |
| callback.task | string | |
| callback.events | string | |
| callback.results | string | |
| callback.complete | string | |
| 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
| id | string | The task's id. |
Response 200
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| agents | array of CloudAgent | |
| agents.id | string | |
| agents.name | string | |
| agents.kind | "webhook" | "a2a" | "routine" | "pull" | |
| agents.enabled | boolean | |
| agents.config | object | |
| agents.config.url | string | null | |
| agents.config.endpoint | string | null | |
| agents.config.protocol_version | string | null | |
| agents.config.routine_id | string | null | |
| agents.config.max_minutes | string | integer | null | |
| agents.has_secret | boolean | |
| agents.presets | array of AgentPreset | |
| agents.presets.id | string | |
| agents.presets.label | string | |
| agents.presets.instruction | string | |
| agents.accept | AgentAccept | null | |
| agents.accept.types? | array of string | |
| agents.accept.max_mb? | number | |
| agents.status | CloudAgentHealth | null | |
| agents.status.ok | boolean | |
| agents.status.at | date-time | |
| agents.status.error? | string | |
| agents.status.http? | string | integer | Webhook agents: the HTTP status the test got. |
| agents.status.endpoint? | string | A2A 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_at | date-time | |
| agents.updated_at | date-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"
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)
| name | string | The 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? | string | Webhook agents: where tasks are posted. A2A agents: the agent card, site or endpoint. https and public. |
| config.routine_id? | string | Routine agents: the routine's id. |
| config.max_minutes? | number | A2A agents: how long a task may run, 1–1440 minutes (120 by default). |
| secret? | string | null | A2A: a bearer token sent to the agent. Routine: its trigger token. Webhook: ignored (SendFlo makes the signing secret). |
| enabled? | boolean | Pause (false) or resume (true); a new agent is always on. |
| presets? | array of object | Up to 12 ready-made instructions `{ id, label, instruction }` senders can pick. |
| presets.id | string | |
| presets.label | string | |
| presets.instruction | string | |
| 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
| agent | CloudAgent | A cloud agent (never its secret). |
| agent.id | string | |
| agent.name | string | |
| agent.kind | "webhook" | "a2a" | "routine" | "pull" | |
| agent.enabled | boolean | |
| agent.config | object | |
| agent.config.url | string | null | |
| agent.config.endpoint | string | null | |
| agent.config.protocol_version | string | null | |
| agent.config.routine_id | string | null | |
| agent.config.max_minutes | string | integer | null | |
| agent.has_secret | boolean | |
| agent.presets | array of AgentPreset | |
| agent.presets.id | string | |
| agent.presets.label | string | |
| agent.presets.instruction | string | |
| agent.accept | AgentAccept | null | |
| agent.accept.types? | array of string | |
| agent.accept.max_mb? | number | |
| agent.status | CloudAgentHealth | null | |
| agent.status.ok | boolean | |
| agent.status.at | date-time | |
| agent.status.error? | string | |
| agent.status.http? | string | integer | Webhook agents: the HTTP status the test got. |
| agent.status.endpoint? | string | A2A 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_at | date-time | |
| agent.updated_at | date-time | |
| secret? | string | Webhook 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
| id | string | The cloud agent's id. |
Request body (JSON)
| name? | string | The 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? | string | Webhook agents: where tasks are posted. A2A agents: the agent card, site or endpoint. https and public. |
| config.routine_id? | string | Routine agents: the routine's id. |
| config.max_minutes? | number | A2A agents: how long a task may run, 1–1440 minutes (120 by default). |
| secret? | string | null | A2A: a bearer token sent to the agent. Routine: its trigger token. Webhook: ignored (SendFlo makes the signing secret). |
| enabled? | boolean | Pause (false) or resume (true); a new agent is always on. |
| presets? | array of object | Up to 12 ready-made instructions `{ id, label, instruction }` senders can pick. |
| presets.id | string | |
| presets.label | string | |
| presets.instruction | string | |
| 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
| agent | CloudAgent | A cloud agent (never its secret). |
| agent.id | string | |
| agent.name | string | |
| agent.kind | "webhook" | "a2a" | "routine" | "pull" | |
| agent.enabled | boolean | |
| agent.config | object | |
| agent.config.url | string | null | |
| agent.config.endpoint | string | null | |
| agent.config.protocol_version | string | null | |
| agent.config.routine_id | string | null | |
| agent.config.max_minutes | string | integer | null | |
| agent.has_secret | boolean | |
| agent.presets | array of AgentPreset | |
| agent.presets.id | string | |
| agent.presets.label | string | |
| agent.presets.instruction | string | |
| agent.accept | AgentAccept | null | |
| agent.accept.types? | array of string | |
| agent.accept.max_mb? | number | |
| agent.status | CloudAgentHealth | null | |
| agent.status.ok | boolean | |
| agent.status.at | date-time | |
| agent.status.error? | string | |
| agent.status.http? | string | integer | Webhook agents: the HTTP status the test got. |
| agent.status.endpoint? | string | A2A 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_at | date-time | |
| agent.updated_at | date-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
| id | string | The cloud agent's id. |
Response 200
| ok | true |
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
| id | string | The cloud agent's id. |
Response 200
| status | CloudAgentHealth | The agent's last health check. |
| status.ok | boolean | |
| status.at | date-time | |
| status.error? | string | |
| status.http? | string | integer | Webhook agents: the HTTP status the test got. |
| status.endpoint? | string | A2A 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.id | string | |
| status.skills.name | string | |
| 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
| balance | string | |
| prices | object | |
| prices.egressPerGb | string | |
| prices.storagePerGbMonth | string | |
| prices.relayPerGb | string | |
| prices.turnPerGb | string | |
| prices.fileRequest | string | |
| prices.webhookDelivery | string |
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/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_start | date-time | |
| period_end | date-time | |
| direct_bytes | string | |
| relay_bytes | string | |
| turn_bytes | string | |
| link_egress_bytes | string | |
| storage_resident_bytes | string | |
| storage_received_bytes | string | |
| storage_own_bytes | string | |
| staging_resident_bytes | string | |
| storage_quota_bytes | string | |
| egress_cap_bytes | string | |
| egress_pooled | boolean | |
| egress_used_bytes | string | |
| downloads | string | integer | |
| plan_class | "free" | "starter" | "plus" | "pro" | "enterprise" | |
| max_transfer_bytes | string | |
| max_file_bytes | string | The same as max_transfer_bytes (kept for older clients). |
| max_claims_per_code | number | |
| max_files_per_transfer | number | |
| max_downloads_per_file | number | |
| account_egress_multiple | number | |
| account_egress_cap_bytes | string | |
| turn_projects | boolean | |
| billing_cycle | "monthly" | "annual" | null | |
| direct_is_free | true | |
| preview | object | |
| preview.images | object | |
| preview.images.used | string | integer | |
| preview.images.limit | string | integer | |
| preview.pdf_pages | object | |
| preview.pdf_pages.used | string | integer | |
| preview.pdf_pages.limit | string | integer | |
| preview.video_bytes | object | |
| preview.video_bytes.used | string | |
| preview.video_bytes.limit | string | |
| preview.pooled? | boolean | A team seat: these are the team's pooled allowances and its usage, shared by every member. |
| marked_views? | object | Team 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.used | string | integer | |
| marked_views.included | string | integer | |
| marked_views.packs_left | string | 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"
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? | string | A team's id: read the whole team's log (owners and admins). Without it: the key's own workspace. |
| since? | date-time | Only events at or after this time (ISO 8601). |
| until? | date-time | Only events before this time. |
| action? | string | One or more actions, comma-separated (`FILE_DOWNLOAD,FILE_PREVIEWED`) — the list is the catalogue in the docs. |
| category? | string | One or more of ACCOUNT, SECURITY, LINK, ACCESS, REQUEST, COLLECTION, TEAM, DEVICE, BILLING, API, STAFF. |
| actor? | string | Who did it: an email address or a user id. |
| ip? | string | One IPv4 / IPv6 address, or a CIDR block (`203.0.113.0/24`). |
| link_id? | string | Only events on this link. |
| outcome? | "success" | "failure" | "denied" | `success`, `failure` or `denied`. |
| cursor? | string | `next_cursor` from the previous page. |
| limit? | string | integer | Events per page, 1–200 (50 by default). |
Response 200
| data | array of AuditEvent | |
| data.event_id | string | UUIDv7 — unique and time-ordered. |
| data.occurred_at | date-time | |
| data.category | string | |
| data.action | string | e.g. FILE_DOWNLOAD — see the catalogue. |
| data.label | string | The sentence a person reads after the actor's name: "downloaded a file". |
| data.outcome | "success" | "failure" | "denied" | |
| data.actor | object | |
| data.actor.kind | "user" | "recipient" | "guest" | "api_key" | "device" | "staff" | "system" | |
| data.actor.user_id | string | null | |
| data.actor.email | string | null | |
| data.actor.verified | boolean | The address was proved (a code by email, or the account's own). |
| data.actor.name | string | null | |
| data.on_behalf_of | string | null | |
| data.ip | string | null | |
| data.ip_source | "cloudflare" | "claimed" | "direct" | "internal" | "none" | null | |
| data.network | object | null | |
| data.network.asn | string | integer | null | |
| data.network.org | string | null | |
| data.network.class | string | null | |
| data.country | string | null | |
| data.target | object | |
| data.target.kind | string | null | |
| data.target.id | string | null | |
| data.link_id | string | null | |
| data.file_id | string | null | |
| data.file_name | string | null | |
| data.owner_user_id | string | null | |
| data.device | object | |
| data.device.label | string | null | |
| data.device.id | string | null | |
| data.user_agent | string | null | |
| data.session_id | string | null | |
| data.api_key_id | string | null | |
| data.request_id | string | null | |
| data.workspace_id | string | |
| data.subscription_id | string | null | |
| data.details | map of any | Bytes, reasons, the fields an edit changed … |
| next_cursor | string | 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/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? | string | A team's id: read the whole team's log (owners and admins). Without it: the key's own workspace. |
| since? | date-time | Only events at or after this time (ISO 8601). |
| until? | date-time | Only events before this time. |
| action? | string | One or more actions, comma-separated (`FILE_DOWNLOAD,FILE_PREVIEWED`) — the list is the catalogue in the docs. |
| category? | string | One or more of ACCOUNT, SECURITY, LINK, ACCESS, REQUEST, COLLECTION, TEAM, DEVICE, BILLING, API, STAFF. |
| actor? | string | Who did it: an email address or a user id. |
| ip? | string | One IPv4 / IPv6 address, or a CIDR block (`203.0.113.0/24`). |
| link_id? | string | Only 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"
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
| ws | string | The inbox's id (GET /v1/plans/{id}/inboxes in the web app; the id in its address). |
Response 200
| id | string | |
| workspace_id | string | |
| subscription_id | string | |
| name | string | |
| restricted | boolean | |
| seat_ids | array of string | |
| group_ids | array of string | |
| members | string | integer | |
| created_at | date-time | |
| data | array of TeamInboxMember | |
| data.user_id | string | |
| data.email | string | null | |
| data.name | string | 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
| ws | string | The inbox's id (GET /v1/plans/{id}/inboxes in the web app; the id in its address). |
Request body (JSON)
| seat_ids? | array of string | Seats on the team. |
| group_ids? | array of string | The team's groups (Team settings → Groups): their seats as they are when someone opens the inbox. |
| emails? | array of string | People seated on the team; one not seated is refused (422 `unknown_member`). |
| base? | object | Optional: 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
| id | string | |
| workspace_id | string | |
| subscription_id | string | |
| name | string | |
| restricted | boolean | |
| seat_ids | array of string | |
| group_ids | array of string | |
| members | string | integer | |
| created_at | date-time | |
| added | string | integer | |
| removed | string | 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? | string | What to upload, shown on the page. |
| name? | string | Your label for it (never shown to the uploader). |
| requested_by? | string | Who is asking, as the page says it: "Requested by Acme support in your chat". |
| send_to? | string | SendFlo emails this person the link; only they can upload. |
| for_email? | string | Bind the page to this person without emailing them. |
| expires_in_minutes? | string | integer | 15 to 10,080 (7 days); default 1,440. |
| max_files? | string | integer | Files per upload (1–50; default 10). |
| close_after_uploads? | string | integer | Uploads before it closes (default 1). |
| allowed_types? | array of string | Accepted file types: category ids or extensions. |
| max_bytes_per_file? | string | integer | A per-file size limit under the plan's. |
| questions? | array of object | Up to 10 questions for the uploader (the upload page's question types). |
| questions.id | string | |
| questions.label | string | |
| questions.type | string | |
| questions.required? | boolean | |
| questions.options? | array of string | |
| locked_answers? | map of string | Answers you set, by question id: shown to the uploader, never editable. |
| agent_fields? | object | What 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? | object | Where it came from: {provider, kind, id, url?, label?}, carried on every item and event. |
| external_refs.provider | string | |
| external_refs.kind | string | |
| external_refs.id | string | |
| external_refs.url? | string | |
| external_refs.label? | string | |
| handoff? | object | When it's ready, hand the files to these people or groups of the team: {to: [{email | user_id | group_id, permission}], expires_days}. |
| handoff.to | array 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? | string | A brand kit for the page. |
| widget_origin? | string | The chat page's origin, for the widget's pop-up to answer to. |
A name ending in ? is optional.
Response 201
| id | string | |
| status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| external_refs | map of any | null | |
| count | string | integer | How many files arrived. |
| files | array of object | |
| files.item_id | string | |
| files.type | string | null | |
| files.size | string | Bytes, as a decimal string. |
| files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| uploader_verified | boolean | The uploader proved their email (never the address itself). |
| answers | array of object | Only the answers the intake shares with the bot. |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| handed_to | array of object | |
| handed_to.id | string | |
| handed_to.kind | "user" | "group" | |
| handed_to.grantee_id | string | |
| handed_to.email | string | null | |
| handed_to.permission | "view" | "manage" | |
| handed_to.expires_at | date-time | null | |
| expires_at | date-time | null | |
| submitted_at | date-time | null | |
| ready_at | date-time | null | |
| url | string | The intake's files in SendFlo, for the people they're handed to (sign-in needed). |
| upload_url | string | The page the person uploads on. |
| emailed | boolean | |
| client_token | string | For SendFlo.requestUpload: single use, 15 minutes. |
| client_token_expires_at | date-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
| data | array of Intake | |
| data.id | string | |
| data.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| data.external_refs | map of any | null | |
| data.count | string | integer | How many files arrived. |
| data.files | array of object | |
| data.files.item_id | string | |
| data.files.type | string | null | |
| data.files.size | string | Bytes, as a decimal string. |
| data.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| data.files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| data.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| data.answers | array of object | Only the answers the intake shares with the bot. |
| data.answers.id | string | |
| data.answers.label | string | |
| data.answers.value | string | |
| data.handed_to | array of object | |
| data.handed_to.id | string | |
| data.handed_to.kind | "user" | "group" | |
| data.handed_to.grantee_id | string | |
| data.handed_to.email | string | null | |
| data.handed_to.permission | "view" | "manage" | |
| data.handed_to.expires_at | date-time | null | |
| data.expires_at | date-time | null | |
| data.submitted_at | date-time | null | |
| data.ready_at | date-time | null | |
| data.url | string | The 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/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
| id | string | The intake's id. |
Response 200
| id | string | |
| status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| external_refs | map of any | null | |
| count | string | integer | How many files arrived. |
| files | array of object | |
| files.item_id | string | |
| files.type | string | null | |
| files.size | string | Bytes, as a decimal string. |
| files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| uploader_verified | boolean | The uploader proved their email (never the address itself). |
| answers | array of object | Only the answers the intake shares with the bot. |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| handed_to | array of object | |
| handed_to.id | string | |
| handed_to.kind | "user" | "group" | |
| handed_to.grantee_id | string | |
| handed_to.email | string | null | |
| handed_to.permission | "view" | "manage" | |
| handed_to.expires_at | date-time | null | |
| expires_at | date-time | null | |
| submitted_at | date-time | null | |
| ready_at | date-time | null | |
| url | string | The 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
| id | string | The intake's id. |
Response 200
| id | string | |
| status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| external_refs | map of any | null | |
| count | string | integer | How many files arrived. |
| files | array of object | |
| files.item_id | string | |
| files.type | string | null | |
| files.size | string | Bytes, as a decimal string. |
| files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| uploader_verified | boolean | The uploader proved their email (never the address itself). |
| answers | array of object | Only the answers the intake shares with the bot. |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| handed_to | array of object | |
| handed_to.id | string | |
| handed_to.kind | "user" | "group" | |
| handed_to.grantee_id | string | |
| handed_to.email | string | null | |
| handed_to.permission | "view" | "manage" | |
| handed_to.expires_at | date-time | null | |
| expires_at | date-time | null | |
| submitted_at | date-time | null | |
| ready_at | date-time | null | |
| url | string | The 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
| id | string | The intake's id. |
Request body (JSON)
| to | object | {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? | string | When the access ends (ISO time). |
A name ending in ? is optional.
Response 200
| id | string | |
| status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| external_refs | map of any | null | |
| count | string | integer | How many files arrived. |
| files | array of object | |
| files.item_id | string | |
| files.type | string | null | |
| files.size | string | Bytes, as a decimal string. |
| files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| uploader_verified | boolean | The uploader proved their email (never the address itself). |
| answers | array of object | Only the answers the intake shares with the bot. |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| handed_to | array of object | |
| handed_to.id | string | |
| handed_to.kind | "user" | "group" | |
| handed_to.grantee_id | string | |
| handed_to.email | string | null | |
| handed_to.permission | "view" | "manage" | |
| handed_to.expires_at | date-time | null | |
| expires_at | date-time | null | |
| submitted_at | date-time | null | |
| ready_at | date-time | null | |
| url | string | The 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
| id | string | The intake's id. |
Request body (JSON)
| kind | "user" | "group" | user or group. |
| id | string | The person's or group's id (as in `handed_to`). |
A name ending in ? is optional.
Response 200
| id | string | |
| status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| external_refs | map of any | null | |
| count | string | integer | How many files arrived. |
| files | array of object | |
| files.item_id | string | |
| files.type | string | null | |
| files.size | string | Bytes, as a decimal string. |
| files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| files.name | string | "Collected file.<ext>" unless the intake shares names (agent_fields.names). |
| uploader_verified | boolean | The uploader proved their email (never the address itself). |
| answers | array of object | Only the answers the intake shares with the bot. |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| handed_to | array of object | |
| handed_to.id | string | |
| handed_to.kind | "user" | "group" | |
| handed_to.grantee_id | string | |
| handed_to.email | string | null | |
| handed_to.permission | "view" | "manage" | |
| handed_to.expires_at | date-time | null | |
| expires_at | date-time | null | |
| submitted_at | date-time | null | |
| ready_at | date-time | null | |
| url | string | The 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
| space | DriveSpace | null | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean | |
| plan | DrivePlan | null | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How long earlier versions and trashed items are kept. |
| folders? | array of DriveNode | An app allowed only some folders: those folders. |
| folders.id | string | |
| folders.parent_id | string | null | |
| folders.kind | "folder" | "file" | |
| folders.name | string | |
| folders.size | string | The current version's size (0 for a folder). |
| folders.mime | string | null | |
| folders.content_hash | string | null | |
| folders.versions | string | integer | Versions kept (the current one included). |
| folders.modified_at | date-time | When the content last changed. |
| folders.updated_at | date-time | When anything last changed (a rename, a move…). |
| folders.created_at | date-time | |
| folders.trashed_at | date-time | null | |
| folders.purge_after | date-time | null | |
| folders.starred? | boolean | |
| folders.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| folders.thumb_url? | string | null | |
| folders.inside? | string | integer | Trash: how many items a trashed folder holds. |
| folders.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| folders.version_id | string | null | |
| folders.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| folders.lock.user_id | string | |
| folders.lock.name | string | |
| folders.lock.until | date-time | |
| folders.lock.mine | boolean | |
| folders.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| folders.conflict.id | string | |
| folders.conflict.file_id | string |
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"
PATCH/drive/automations/{id}Change, pause or resume an automation
Any of its fields, or enabled.
Key scope: drive.write
Path parameters
| id | string | The automation's id. |
Request body (JSON)
| name? | string | Its name. |
| trigger? | string | Its trigger. |
| subfolders? | boolean | Files in subfolders too. |
| conditions? | map of any | Its conditions. |
| actions? | array of map of any | Its actions. |
| enabled? | boolean | false pauses it. |
A name ending in ? is optional.
Response 200
| automation | DriveAutomation | |
| automation.id | string | |
| automation.folder_id | string | |
| automation.name | string | |
| automation.trigger | "added" | "changed" | "labeled" | |
| automation.subfolders | boolean | |
| automation.conditions | map of any | |
| automation.actions | array of map of any | |
| automation.enabled | boolean | |
| automation.created_by | string | |
| automation.runs | string | integer | |
| automation.last_run_at | date-time | null | |
| automation.created_at | date-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
| id | string | The automation's id. |
Response 200
| removed | boolean |
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
| id | string | The automation's id. |
Response 200
| automation | DriveAutomation | |
| automation.id | string | |
| automation.folder_id | string | |
| automation.name | string | |
| automation.trigger | "added" | "changed" | "labeled" | |
| automation.subfolders | boolean | |
| automation.conditions | map of any | |
| automation.actions | array of map of any | |
| automation.enabled | boolean | |
| automation.created_by | string | |
| automation.runs | string | integer | |
| automation.last_run_at | date-time | null | |
| automation.created_at | date-time | |
| runs | array of object | |
| runs.id | string | |
| runs.node_id | string | |
| runs.name | string | |
| runs.trigger | string | |
| runs.status | "done" | "failed" | "skipped" | "loop_stopped" | |
| runs.steps | array of string | |
| runs.error | string | null | |
| runs.at | date-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? | string | The last `seq` you have (0: from the start). |
| limit? | string | 1–5,000 (1,000 by default). |
Response 200
| space_id | string | |
| cursor | string | |
| more | boolean | |
| changes | array of object | |
| changes.seq | string | |
| changes.op | "create" | "update" | "rename" | "move" | "trash" | "restore" | "delete" | |
| changes.node_id | string | |
| changes.parent_id | string | null | |
| changes.kind | "folder" | "file" | |
| changes.name | string | null | |
| changes.version_id | string | null | |
| changes.at | date-time | |
| changes.meta | any |
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/drive/clientsList your computers syncing Drive
Key scope: drive.read
Response 200
| clients | array of DriveSyncClient | |
| clients.id | string | |
| clients.name | string | |
| clients.os | string | null | |
| clients.client_version | string | null | |
| clients.root_kind | "cfapi" | "fileprovider" | "fuse" | "folder" | |
| clients.state | "ok" | "syncing" | "paused" | "problems" | "offline" | |
| clients.problems | string | integer | |
| clients.problem_reasons | map of string | integer | |
| clients.pending | string | integer | |
| clients.cache_bytes | string | null | |
| clients.cache_budget | string | null | |
| clients.hidden | string | integer | |
| clients.spaces | array of object | |
| clients.spaces.space_id | string | |
| clients.spaces.cursor | string | |
| clients.managed | object | 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_at | date-time | null | |
| clients.created_at | date-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"
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)
| name | string | The computer's name. |
| os? | string | null | e.g. `windows 10.0.19045`. |
| client_version? | string | null | The client's version. |
| root_kind | string | `cfapi` (Windows), `fileprovider` (macOS), `fuse` (Linux) or `folder` (a synced folder). |
A name ending in ? is optional.
Response 200
| client | DriveSyncClient | |
| client.id | string | |
| client.name | string | |
| client.os | string | null | |
| client.client_version | string | null | |
| client.root_kind | "cfapi" | "fileprovider" | "fuse" | "folder" | |
| client.state | "ok" | "syncing" | "paused" | "problems" | "offline" | |
| client.problems | string | integer | |
| client.problem_reasons | map of string | integer | |
| client.pending | string | integer | |
| client.cache_bytes | string | null | |
| client.cache_budget | string | null | |
| client.hidden | string | integer | |
| client.spaces | array of object | |
| client.spaces.space_id | string | |
| client.spaces.cursor | string | |
| client.managed | object | 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_at | date-time | null | |
| client.created_at | date-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
| id | string | The client's id. |
Response 200
| removed | true |
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
| id | string | The client's id. |
Request body (JSON)
| state? | string | `ok`, `syncing`, `paused`, `problems` or `offline`. |
| problems? | string | integer | Items that failed. |
| problem_reasons? | map of string | integer | null | {reason: count}. |
| pending? | string | integer | Items waiting. |
| cache_bytes? | string | string | integer | null | Bytes kept locally. |
| cache_budget? | string | string | integer | null | The cache's limit. |
| hidden? | string | integer | Folders hidden on this computer. |
| spaces? | array of object | null | [{space_id, cursor}]. |
| spaces.space_id | string | |
| spaces.cursor | string | |
| client_version? | string | null | If 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.addr | string | |
| peer.port | string | integer | |
| peer.token | string | |
| peer_in? | string | integer | Bytes fetched from nearby computers since the last heartbeat. |
| peer_out? | string | integer | Bytes served to them since the last heartbeat. |
| shared? | string | null | Why 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
| client | DriveSyncClient | |
| client.id | string | |
| client.name | string | |
| client.os | string | null | |
| client.client_version | string | null | |
| client.root_kind | "cfapi" | "fileprovider" | "fuse" | "folder" | |
| client.state | "ok" | "syncing" | "paused" | "problems" | "offline" | |
| client.problems | string | integer | |
| client.problem_reasons | map of string | integer | |
| client.pending | string | integer | |
| client.cache_bytes | string | null | |
| client.cache_budget | string | null | |
| client.hidden | string | integer | |
| client.spaces | array of object | |
| client.spaces.space_id | string | |
| client.spaces.cursor | string | |
| client.managed | object | 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_at | date-time | null | |
| client.created_at | date-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
| id | string | The comment's id. |
Request body (JSON)
| body? | string | The new text. |
| resolved? | boolean | true resolves, false reopens. |
A name ending in ? is optional.
Response 200
| updated | boolean |
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
| id | string | The comment's id. |
Response 200
| deleted | boolean |
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
| id | string | The comment's id. |
Request body (JSON)
| assignee_id | string | Their user id. |
| due_at? | date-time | null | When it's due. |
A name ending in ? is optional.
Response 200
| task | object | |
| task.id | string | |
| task.status | "open" | "done" | |
| task.due_at | date-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
| conflicts | array of DriveConflict | |
| conflicts.id | string | |
| conflicts.space_id | string | |
| conflicts.space_name | string | |
| conflicts.file | any | null | |
| conflicts.copy | any | null | |
| conflicts.path | array of object | |
| conflicts.path.id | string | |
| conflicts.path.name | string | |
| conflicts.by | object | |
| conflicts.by.id | string | |
| conflicts.by.name | string | |
| conflicts.client_name | string | null | |
| conflicts.state | "open" | "resolved" | |
| conflicts.resolution | "kept_file" | "kept_copy" | "kept_both" | null | |
| conflicts.created_at | date-time | |
| conflicts.resolved_at | date-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"
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
| id | string | The conflict's id. |
Request body (JSON)
| keep | string | `file`, `copy` or `both`. |
A name ending in ? is optional.
Response 200
| conflict | DriveConflict | |
| conflict.id | string | |
| conflict.space_id | string | |
| conflict.space_name | string | |
| conflict.file | any | null | |
| conflict.copy | any | null | |
| conflict.path | array of object | |
| conflict.path.id | string | |
| conflict.path.name | string | |
| conflict.by | object | |
| conflict.by.id | string | |
| conflict.by.name | string | |
| conflict.client_name | string | null | |
| conflict.state | "open" | "resolved" | |
| conflict.resolution | "kept_file" | "kept_copy" | "kept_both" | null | |
| conflict.created_at | date-time | |
| conflict.resolved_at | date-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? | string | The folder to make it in. |
| name | string | Its name (no `/`, 255 bytes at most). In a private space, the name sealed on your device. |
| name_key? | string | In 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
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string |
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
| id | string | The 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
| folder | DriveNode | |
| folder.id | string | |
| folder.parent_id | string | null | |
| folder.kind | "folder" | "file" | |
| folder.name | string | |
| folder.size | string | The current version's size (0 for a folder). |
| folder.mime | string | null | |
| folder.content_hash | string | null | |
| folder.versions | string | integer | Versions kept (the current one included). |
| folder.modified_at | date-time | When the content last changed. |
| folder.updated_at | date-time | When anything last changed (a rename, a move…). |
| folder.created_at | date-time | |
| folder.trashed_at | date-time | null | |
| folder.purge_after | date-time | null | |
| folder.starred? | boolean | |
| folder.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| folder.thumb_url? | string | null | |
| folder.inside? | string | integer | Trash: how many items a trashed folder holds. |
| folder.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| folder.version_id | string | null | |
| folder.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| folder.lock.user_id | string | |
| folder.lock.name | string | |
| folder.lock.until | date-time | |
| folder.lock.mine | boolean | |
| folder.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| folder.conflict.id | string | |
| folder.conflict.file_id | string | |
| path | array of object | From the top folder down to this one. |
| path.id | string | |
| path.name | string | |
| items | array of DriveNode | |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean | |
| plan | DrivePlan | null | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How long earlier versions and trashed items are kept. |
| role | "owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| shared | object | null | |
| shared.role | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| shared.top_id | string | null | |
| shared.owner | DriveOwner | null | |
| shared.owner.id | string | |
| shared.owner.name | string | |
| shared.owner.avatar_url | string | null | |
| access | object | null | |
| access.shares | string | integer | Shares that reach it (its own and from folders above). |
| access.team | boolean | |
| team | object | null | |
| team.id | string | |
| team.name | string | |
| team.pool_used | string | What every space of the team holds. |
| team.pool_bytes | string | The team's pool. |
| team.manages | boolean |
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"GET/drive/linksList your Drive links
Every live link you made, newest first, with the file or folder it opens.
Key scope: drive.read
Response 200
| links | array of object | |
| links.id | string | |
| links.node_id | string | The file or folder it opens. |
| links.slug | string | |
| links.url | string | The page anyone it lets in opens: https://sendflo.app/d/<slug>. |
| links.mode | "latest" | "version" | |
| links.version_id | string | null | |
| links.audience | "public" | "verified" | "restricted" | |
| links.emails | array of string | restricted: the addresses that may open it (each confirms with a code). |
| links.password | boolean | |
| links.delivery | "preview_download" | "download_only" | "preview_only" | |
| links.expires_at | date-time | null | |
| links.download_limit | string | integer | null | |
| links.downloads | string | integer | |
| links.share_id | string | null | |
| links.last_opened_at | date-time | null | |
| links.created_at | date-time | |
| links.node | object | |
| links.node.id | string | |
| links.node.kind | "folder" | "file" | |
| links.node.name | string | |
| links.node.trashed | boolean |
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/links" \ -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
| id | string | The link's id. |
Request body (JSON)
| expires_at? | date-time | null | When it ends (null: never, not for a public link). |
| download_limit? | string | integer | null | 1–100,000, or null. |
| delivery? | string | preview_download, download_only or preview_only. |
| password? | string | null | 4–128 characters, or null to remove it. |
A name ending in ? is optional.
Response 200
| link | DriveLink | null | |
| link.id | string | |
| link.node_id | string | The file or folder it opens. |
| link.slug | string | |
| link.url | string | The page anyone it lets in opens: https://sendflo.app/d/<slug>. |
| link.mode | "latest" | "version" | |
| link.version_id | string | null | |
| link.audience | "public" | "verified" | "restricted" | |
| link.emails | array of string | restricted: the addresses that may open it (each confirms with a code). |
| link.password | boolean | |
| link.delivery | "preview_download" | "download_only" | "preview_only" | |
| link.expires_at | date-time | null | |
| link.download_limit | string | integer | null | |
| link.downloads | string | integer | |
| link.share_id | string | null | |
| link.last_opened_at | date-time | null | |
| link.created_at | date-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
| id | string | The link's id. |
Response 200
| revoked | 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/links/{id}" \
-H "authorization: Bearer $SENDFLO_API_KEY"GET/drive/nodes/{id}Get a file or folder
Key scope: drive.read
Path parameters
| id | string | The file's or folder's id. |
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string | |
| path | array of object | From the top folder down to this one. |
| path.id | string | |
| path.name | string | |
| role | "owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| shared | object | null | |
| shared.role | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| shared.top_id | string | null | |
| shared.owner | DriveOwner | null | |
| shared.owner.id | string | |
| shared.owner.name | string | |
| shared.owner.avatar_url | string | null | |
| access | object | null | |
| access.shares | string | integer | Shares that reach it (its own and from folders above). |
| access.team | boolean |
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
| id | string | The file's or folder's id. |
Request body (JSON)
| name? | string | The new name (in a private space, sealed on your device). |
| name_key? | string | In a private space: the new name's index (64 hex), with the sealed `name`. |
| parent_id? | string | The folder to move it to. |
| on_conflict? | string | When moving: `fail` (the default) or `rename`. |
A name ending in ? is optional.
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string |
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
| id | string | The file's or folder's id. |
Response 200
| trashed | true | |
| id | string | |
| count | string | integer | Items moved (the folder and everything in it). |
| bytes | string | |
| purge_after | date-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
| id | string | The folder's id. |
Response 200
| automations | array of DriveAutomation | |
| automations.id | string | |
| automations.folder_id | string | |
| automations.name | string | |
| automations.trigger | "added" | "changed" | "labeled" | |
| automations.subfolders | boolean | |
| automations.conditions | map of any | |
| automations.actions | array of map of any | |
| automations.enabled | boolean | |
| automations.created_by | string | |
| automations.runs | string | integer | |
| automations.last_run_at | date-time | null | |
| automations.created_at | date-time | |
| can_manage | boolean |
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
| id | string | The folder's id. |
Request body (JSON)
| name? | string | The rule's name. |
| trigger? | string | `added`, `changed` or `labeled` (which needs conditions.label_ids). |
| subfolders? | boolean | Files in subfolders too (default true). |
| conditions? | map of any | All must hold. |
| actions? | array of map of any | 1 to 6, done in order. |
| enabled? | boolean | false: made paused. |
A name ending in ? is optional.
Response 200
| automation | DriveAutomation | |
| automation.id | string | |
| automation.folder_id | string | |
| automation.name | string | |
| automation.trigger | "added" | "changed" | "labeled" | |
| automation.subfolders | boolean | |
| automation.conditions | map of any | |
| automation.actions | array of map of any | |
| automation.enabled | boolean | |
| automation.created_by | string | |
| automation.runs | string | integer | |
| automation.last_run_at | date-time | null | |
| automation.created_at | date-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
| id | string | The file's or folder's id. |
Response 200
| comments | array of any | |
| can_task | boolean |
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
| id | string | The file's or folder's id. |
Request body (JSON)
| body | string | 1–2,000 characters. |
| parent_id? | string | null | Reply to this comment. |
| anchor? | any | null | Where it points. |
| mentions? | array of string | Up to 20 user ids. |
A name ending in ? is optional.
Response 200
| comment | object | |
| comment.id | string |
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
| id | string | The file's or folder's id. |
Request body (JSON)
| parent_id? | string | The folder to copy it into. |
A name ending in ? is optional.
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string |
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
| id | string | The file's id. |
Query parameters
| version? | string | An earlier version's id (GET …/versions). |
| inline? | "1" | `1`: for viewing, not saving. |
Response 200
| url | string | |
| name | string | |
| size | string | |
| mime | string | null | |
| version_id | string |
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"GET/drive/nodes/{id}/linksList a file's or folder's links
Its live links (owners and co-owners).
Key scope: drive.read
Path parameters
| id | string | The file's or folder's id. |
Response 200
| links | array of DriveLink | |
| links.id | string | |
| links.node_id | string | The file or folder it opens. |
| links.slug | string | |
| links.url | string | The page anyone it lets in opens: https://sendflo.app/d/<slug>. |
| links.mode | "latest" | "version" | |
| links.version_id | string | null | |
| links.audience | "public" | "verified" | "restricted" | |
| links.emails | array of string | restricted: the addresses that may open it (each confirms with a code). |
| links.password | boolean | |
| links.delivery | "preview_download" | "download_only" | "preview_only" | |
| links.expires_at | date-time | null | |
| links.download_limit | string | integer | null | |
| links.downloads | string | integer | |
| links.share_id | string | null | |
| links.last_opened_at | date-time | null | |
| links.created_at | date-time |
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 "https://api.sendflo.app/v1/drive/nodes/{id}/links" \
-H "authorization: Bearer $SENDFLO_API_KEY"POST/drive/nodes/{id}/linksMake a link to a file or folder
A page at url that opens it: public (anyone with the link), verified (anyone who confirms their email with a code) or restricted (only emails). A file link serves the latest version (mode: latest, the default) or the current one for good (mode: version); a folder link lists the folder's current contents. Optional password, download_limit and delivery (preview_download, download_only, preview_only). Public links end after 30 days unless expires_at says otherwise (a year at most). Bytes go out only once the malware scan cleared them. Owners and co-owners.
Key scope: drive.write
Path parameters
| id | string | The file's or folder's id. |
Request body (JSON)
| audience? | string | public (default), verified or restricted. |
| emails? | array of string | restricted: up to 50 addresses. |
| password? | string | null | 4–128 characters. |
| delivery? | string | preview_download (default), download_only or preview_only. |
| expires_at? | date-time | null | When it ends. |
| download_limit? | string | integer | null | 1–100,000 downloads. |
| mode? | string | Files: latest (default) or version. |
| version_id? | string | null | mode version: which version (the current one when absent). |
A name ending in ? is optional.
Response 200
| link | DriveLink | |
| link.id | string | |
| link.node_id | string | The file or folder it opens. |
| link.slug | string | |
| link.url | string | The page anyone it lets in opens: https://sendflo.app/d/<slug>. |
| link.mode | "latest" | "version" | |
| link.version_id | string | null | |
| link.audience | "public" | "verified" | "restricted" | |
| link.emails | array of string | restricted: the addresses that may open it (each confirms with a code). |
| link.password | boolean | |
| link.delivery | "preview_download" | "download_only" | "preview_only" | |
| link.expires_at | date-time | null | |
| link.download_limit | string | integer | null | |
| link.downloads | string | integer | |
| link.share_id | string | null | |
| link.last_opened_at | date-time | null | |
| link.created_at | date-time |
A name ending in ? is optional.
An error: no_drive, not_found, drive_role, read_only, link_root, file_flagged, bad_audience, bad_delivery, bad_email, no_emails, too_many_emails, bad_expiry, bad_limit, bad_password, no_version, public_links_paused, free_plan_restricted, 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}/links" \
-H "authorization: Bearer $SENDFLO_API_KEY" \
-H "content-type: application/json" \
-d '{}'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
| id | string | The file's id. |
Request body (JSON)
| minutes? | string | integer | 1–1,440. |
| client_id? | string | null | Your sync client, if a computer locked it. |
A name ending in ? is optional.
Response 200
| lock | object | |
| lock.user_id | string | |
| lock.until | date-time | |
| lock.mine | true |
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
| id | string | The file's id. |
Response 200
| unlocked | 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/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
| id | string | The file's id. |
Query parameters
| version_id? | string | A version of the file (the current one by default). |
Response 200
| version_id | string | |
| size | string | |
| part_size | string | integer | |
| hashes | array 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
| id | string | The file's id. |
Request body (JSON)
| client_id | string | This computer's sync-client id (POST /v1/drive/clients): the key binds it. |
| version_id? | string | null | The version wanted (the current one by default). |
A name ending in ? is optional.
Response 200
| hash? | string | The version's BLAKE3. |
| size? | string | |
| version_id? | string | |
| off? | true | |
| peers | array of object | |
| peers.client_id | string | |
| peers.addr | string | |
| peers.port | string | integer | |
| peers.exp | string | integer | Unix seconds. |
| peers.key | string | Hex, 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
| id | string | The file's or folder's id. |
Response 200
| people | array of object | |
| people.id | string | |
| people.name | string | |
| people.email | string | |
| people.avatar_url | string | null | |
| people.me | boolean |
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
| id | string | The trashed item's id (as listed by GET /v1/drive/trash). |
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string | |
| path | array of object | From the top folder down to this one. |
| path.id | string | |
| path.name | string |
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"PUT/drive/nodes/{id}/starStar a file or folder
Key scope: drive.write
Path parameters
| id | string | The file's or folder's id. |
Response 200
| starred | true |
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
| id | string | The file's or folder's id. |
Response 200
| starred | false |
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
| id | string | The file's id. |
Response 200
| text_in_image | object | null | |
| text_in_image.state | "read" | "partly_read" | "unreadable" | "damaged" | "no_text" | "not_read_yet" | "off" | |
| text_in_image.label | string | The words to show, e.g. "Partly read". |
| text_in_image.detail | string | 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
| id | string | The file's id. |
Response 200
| current_version_id | string | null | |
| version_days | string | integer | |
| versions | array of object | |
| versions.id | string | |
| versions.size | string | |
| versions.content_hash | string | null | |
| versions.source | "web" | "desktop" | "phone" | "api" | "restore" | "copy" | |
| versions.created_at | date-time | |
| versions.created_by | string | null | |
| versions.purge_after | date-time | null | |
| versions.current | boolean | |
| versions.editors? | array of string | A 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
| id | string | The file's id. |
| vid | string | The version's id. |
Response 200
| deleted | true |
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
| id | string | The file's id. |
| vid | string | The version's id. |
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string |
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 | null | The folder (your Drive's top when absent). |
| name? | string | null | The note's name. |
A name ending in ? is optional.
Response 200
| note | DriveNote | |
| note.id | string | |
| note.name | string | |
| note.parent_id | string | null | |
| note.size | string | |
| note.version_id | string | null | |
| note.modified_at | date-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
| id | string | The note's id. |
Response 200
| note | DriveNote | |
| note.id | string | |
| note.name | string | |
| note.parent_id | string | null | |
| note.size | string | |
| note.version_id | string | null | |
| note.modified_at | date-time | |
| epoch | string | |
| state | string | |
| seq | string | |
| saved | string | The last `seq` already saved as a version. |
| text | string | |
| presence | array of DriveNotePresence | |
| presence.client | string | |
| presence.user_id | string | |
| presence.name | string | |
| presence.colour | string | |
| presence.anchor | string | null | |
| presence.head | string | null | |
| presence.at | string | integer | |
| you | string | |
| can_edit | boolean |
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
| id | string | The note's id. |
Request body (JSON)
| text | string | The note's Markdown (up to 1 MB). |
A name ending in ? is optional.
Response 200
| note | DriveNote | |
| note.id | string | |
| note.name | string | |
| note.parent_id | string | null | |
| note.size | string | |
| note.version_id | string | null | |
| note.modified_at | date-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
| id | string | The note's id. |
Request body (JSON)
| client | string | Your client's id. |
| anchor? | string | null | Your selection's anchor. |
| head? | string | null | Your selection's head. |
| leave? | boolean | You closed the note. |
A name ending in ? is optional.
Response 200
| ok | boolean |
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
| id | string | The note's id. |
Query parameters
| epoch | string | The epoch the note opened with. |
| since? | string | The last `seq` you have. |
Response 200
| epoch | string | |
| seq | string | |
| saved | string | The last `seq` already saved as a version. |
| updates | array of object | |
| updates.seq | string | |
| updates.client | string | |
| updates.update | string | |
| presence | array of DriveNotePresence | |
| presence.client | string | |
| presence.user_id | string | |
| presence.name | string | |
| presence.colour | string | |
| presence.anchor | string | null | |
| presence.head | string | null | |
| presence.at | string | integer | |
| state? | string | When 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
| id | string | The note's id. |
Request body (JSON)
| epoch | string | The note's epoch. |
| client | string | Your client's id (4–40 letters, digits, - or _). |
| update | string | The Yjs update, base64. |
| anchor? | string | null | Your selection's anchor. |
| head? | string | null | Your selection's head. |
A name ending in ? is optional.
Response 200
| seq | string |
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? | string | 1–500 (200 by default). |
Response 200
| items | array of DriveNode | |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| plan | DrivePlan | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How long earlier versions and trashed items are kept. |
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean |
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/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
| q | string | Search words (200 characters at most). |
| in? | string | A folder: search only inside it. |
| trashed? | "1" | `1`: search the trash instead. |
| kind? | "file" | "folder" | `file` or `folder` only. |
| page? | string | The 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
| items | array of object | |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| items.path | array of object | From the top folder down to this one. |
| items.path.id | string | |
| items.path.name | string | |
| items.space | DriveSpace | |
| items.space.id | string | |
| items.space.kind | "personal" | "team" | |
| items.space.name | string | |
| items.space.root_id | string | The top folder. |
| items.space.seq | string | The last change. |
| items.space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| items.space.files | string | integer | |
| items.space.e2e | boolean | |
| items.shared | object | null | |
| items.shared.role | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| items.shared.top_id | string | null | |
| items.shared.owner | DriveOwner | null | |
| items.match? | "name" | "contents" | With `contents=1`: how it was found — its name, or the words inside it (its best match). |
| items.snippet? | string | The words around the match inside the file, as plain text (with `contents=1`, when its text matched). |
| items.marks? | array of array of any | Where the matched words are in `snippet`: [start, end) string indices in UTF-16 code units, in order, never overlapping. |
| search | SearchMeta | Present when `q` was given. `engine` is `postgres` while the search engine is unavailable: a plain name match, newest first. |
| search.engine | "meilisearch" | "postgres" | |
| search.page | string | integer | This page (1–8). |
| search.total | string | integer | About how many match (the search engine's estimate). |
| search.next_page | string | 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/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
| spaces | array of object | |
| spaces.id | string | |
| spaces.kind | "personal" | "team" | |
| spaces.name | string | |
| spaces.root_id | string | The top folder. |
| spaces.seq | string | The last change. |
| spaces.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| spaces.files | string | integer | |
| spaces.e2e | boolean | |
| spaces.team | object | |
| spaces.team.id | string | |
| spaces.team.name | string | |
| spaces.role | "owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| spaces.members | string | integer | |
| teams | array of object | |
| teams.id | string | |
| teams.name | string | |
| teams.can_create | boolean | |
| teams.manages | boolean |
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"
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_id | string | The team (subscription) id. |
| name | string | Its name (unique in the team). |
| private? | object | Make 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_key | string | |
| private.seals | array of object | |
| private.seals.user_id | string | |
| private.seals.identity_version | string | integer | |
| private.seals.sealed | string |
A name ending in ? is optional.
Response 200
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean |
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
| id | string | The space's id. |
Request body (JSON)
| name | string | Its new name. |
A name ending in ? is optional.
Response 200
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean |
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
| id | string | The space's id. |
Response 200
| archived | boolean |
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
| id | string | The space's id. |
Query parameters
| since? | string | The last `seq` you applied. |
| limit? | string | 1–5,000 (1,000 by default). |
Response 200
| space_id | string | |
| cursor | string | |
| more | boolean | |
| changes | array of object | |
| changes.seq | string | |
| changes.op | "create" | "update" | "rename" | "move" | "trash" | "restore" | "delete" | |
| changes.node_id | string | |
| changes.node | DriveNode | null | |
| changes.node.id | string | |
| changes.node.parent_id | string | null | |
| changes.node.kind | "folder" | "file" | |
| changes.node.name | string | |
| changes.node.size | string | The current version's size (0 for a folder). |
| changes.node.mime | string | null | |
| changes.node.content_hash | string | null | |
| changes.node.versions | string | integer | Versions kept (the current one included). |
| changes.node.modified_at | date-time | When the content last changed. |
| changes.node.updated_at | date-time | When anything last changed (a rename, a move…). |
| changes.node.created_at | date-time | |
| changes.node.trashed_at | date-time | null | |
| changes.node.purge_after | date-time | null | |
| changes.node.starred? | boolean | |
| changes.node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| changes.node.thumb_url? | string | null | |
| changes.node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| changes.node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| changes.node.version_id | string | null | |
| changes.node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| changes.node.conflict? | object | This 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
| id | string | The space's id. |
Query parameters
| after? | string | The last node id you have (0: from the start). |
| limit? | string | 1–5,000 (1,000 by default). |
Response 200
| space_id | string | |
| seq | string | |
| nodes | array of DriveNode | |
| nodes.id | string | |
| nodes.parent_id | string | null | |
| nodes.kind | "folder" | "file" | |
| nodes.name | string | |
| nodes.size | string | The current version's size (0 for a folder). |
| nodes.mime | string | null | |
| nodes.content_hash | string | null | |
| nodes.versions | string | integer | Versions kept (the current one included). |
| nodes.modified_at | date-time | When the content last changed. |
| nodes.updated_at | date-time | When anything last changed (a rename, a move…). |
| nodes.created_at | date-time | |
| nodes.trashed_at | date-time | null | |
| nodes.purge_after | date-time | null | |
| nodes.starred? | boolean | |
| nodes.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| nodes.thumb_url? | string | null | |
| nodes.inside? | string | integer | Trash: how many items a trashed folder holds. |
| nodes.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| nodes.version_id | string | null | |
| nodes.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| nodes.lock.user_id | string | |
| nodes.lock.name | string | |
| nodes.lock.until | date-time | |
| nodes.lock.mine | boolean | |
| nodes.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| nodes.conflict.id | string | |
| nodes.conflict.file_id | string | |
| next | string | 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
| items | array of DriveNode | |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| plan | DrivePlan | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How long earlier versions and trashed items are kept. |
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean |
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/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
| roots | array of object | |
| roots.space_id | string | |
| roots.kind | "personal" | "team" | |
| roots.name | string | |
| roots.team | string | null | |
| roots.root_id | string | |
| roots.role | "owner" | "co_owner" | "editor" | "uploader" | "viewer" | "previewer" | |
| roots.read_only | boolean | |
| roots.seq | string |
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/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
| tasks | array 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"
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
| id | string | The task's id. |
Request body (JSON)
| status? | string | open or done. |
| due_at? | date-time | null | When it's due, or null. |
A name ending in ? is optional.
Response 200
| task | object | |
| task.id | string | |
| task.status | "open" | "done" | |
| task.due_at | date-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
| id | string | The team plan's id. |
Response 200
| conflicts | array of DriveConflict | |
| conflicts.id | string | |
| conflicts.space_id | string | |
| conflicts.space_name | string | |
| conflicts.file | any | null | |
| conflicts.copy | any | null | |
| conflicts.path | array of object | |
| conflicts.path.id | string | |
| conflicts.path.name | string | |
| conflicts.by | object | |
| conflicts.by.id | string | |
| conflicts.by.name | string | |
| conflicts.client_name | string | null | |
| conflicts.state | "open" | "resolved" | |
| conflicts.resolution | "kept_file" | "kept_copy" | "kept_both" | null | |
| conflicts.created_at | date-time | |
| conflicts.resolved_at | date-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
| id | string | The team plan's id. |
Request body (JSON)
| rule | string | `newest`, `file` or `both`. |
| ids? | array of string | null | Conflict ids (all when absent). |
A name ending in ? is optional.
Response 200
| resolved | string | integer | |
| skipped | string | 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
| id | string | The team (subscription) id. |
Response 200
| policy | object | |
| policy.create | "admins" | "managers" | "members" | |
| policy.outside | "allow" | "off" | |
| policy.peers | "on" | "off" | |
| outside_controls | boolean |
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
| id | string | The team (subscription) id. |
Request body (JSON)
| create? | string | admins, managers or members. |
| outside? | string | allow or off. |
| peers? | string | on (the default) or off: the team's computers on one network fetch its spaces' files from each other. |
| automations? | string | Who 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
| policy | object | |
| 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
| items | array of DriveNode | |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| version_days | string | integer | |
| plan | DrivePlan | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How long earlier versions and trashed items are kept. |
| space | DriveSpace | |
| space.id | string | |
| space.kind | "personal" | "team" | |
| space.name | string | |
| space.root_id | string | The top folder. |
| space.seq | string | The last change. |
| space.bytes_used | string | What the plan counts: current versions of files not in the trash. |
| space.files | string | integer | |
| space.e2e | boolean |
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"
DELETE/drive/trashEmpty the trash
Key scope: drive.write
Response 200
| deleted | string | 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
| id | string | The trashed item's id. |
Response 200
| deleted | string | integer | Items 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? | string | The folder. |
| name | string | The file's name. |
| size | number | Bytes. |
| mime? | string | null | Its media type. |
| hash? | string | null | BLAKE3 of the content (64 hex characters), optional. |
| on_conflict? | string | `rename` (the default), `replace` or `fail`. |
| node_id? | string | null | A file to add a version to. |
| source? | string | `api` (the default for keys), `desktop` or `phone`. |
| modified_at? | date-time | null | When the content last changed on the device. |
| base_version_id? | string | null | With `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 | null | A sync client's id (POST /v1/drive/clients): a conflict copy is named after that computer. |
| parts? | object | With `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_size | string | integer | |
| parts.hashes | array of string | |
| name_key? | string | In a private space (end-to-end): the name's index (64 hex) your device computed, with the sealed `name`. |
| sealed? | object | In 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_id | string | |
| sealed.sealed_key | string |
A name ending in ? is optional.
Response 200
| state | "done" | |
| deduplicated | true | |
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string | |
| 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
| id | string | The upload's id. |
Response 200
| aborted | true |
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
| id | string | The upload's id. |
Response 200
| node | DriveNode | |
| node.id | string | |
| node.parent_id | string | null | |
| node.kind | "folder" | "file" | |
| node.name | string | |
| node.size | string | The current version's size (0 for a folder). |
| node.mime | string | null | |
| node.content_hash | string | null | |
| node.versions | string | integer | Versions kept (the current one included). |
| node.modified_at | date-time | When the content last changed. |
| node.updated_at | date-time | When anything last changed (a rename, a move…). |
| node.created_at | date-time | |
| node.trashed_at | date-time | null | |
| node.purge_after | date-time | null | |
| node.starred? | boolean | |
| node.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| node.thumb_url? | string | null | |
| node.inside? | string | integer | Trash: how many items a trashed folder holds. |
| node.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| node.version_id | string | null | |
| node.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| node.lock.user_id | string | |
| node.lock.name | string | |
| node.lock.until | date-time | |
| node.lock.mine | boolean | |
| node.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| node.conflict.id | string | |
| node.conflict.file_id | string | |
| 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
| id | string | The upload's id. |
Request body (JSON)
| parts | array of string | integer | Part numbers. |
A name ending in ? is optional.
Response 200
| part_urls | array 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_used | string | |
| files | string | integer | |
| bytes | string | The plan's room. |
| history_bytes | string | Earlier versions and the trash (not counted). |
| plan | DrivePlan | |
| plan.plan | string | |
| plan.name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: files can be read and downloaded until deleted, nothing new added. |
| plan.bytes | string | The room. |
| plan.version_days | string | integer | How 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/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
| items | array of object | |
| items.workspace | string | The Drive's workspace. |
| items.kind | "personal" | "team" | |
| items.team | object | null | |
| items.team.id | string | The team plan. |
| items.team.name | string | null | |
| items.plan | string | null | |
| items.enabled | boolean | The plan searches inside files now. |
| items.trial | boolean | |
| items.cap | string | integer | null | |
| items.until | date-time | null | |
| items.documents | string | integer | Documents searched inside now. |
| items.backfill | SearchBackfill | null | |
| items.backfill.id | string | |
| items.backfill.status | "queued" | "running" | "paused" | "done" | "cancelled" | "failed" | |
| items.backfill.trigger | string | Why 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_seen | string | integer | Files it looked at (end-to-end files included: their contents are never read). |
| items.backfill.queued | string | integer | Files it sent to be read. |
| items.backfill.done | string | integer | Files read. |
| items.backfill.failed | string | integer | Files that couldn't be read (a password-protected or broken document, or the reader gave up). |
| items.backfill.skipped | map of string | integer | Files 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.processed | string | integer | Files back from the reader: `done`, `failed` and those skipped there. The progress bar's numerator. |
| items.backfill.total | string | integer | Files it expects to read: an estimate while it walks, exact once the walk is over. |
| items.backfill.started_at | date-time | null | |
| items.backfill.finished_at | date-time | null | |
| items.ocr | object | null | |
| items.ocr.allowed | boolean | The plan offers text in images (Drive 5 TB, Team Drive Pro and Enterprise). |
| items.ocr.on | boolean | Turned on for this Drive (PUT /v1/search/settings). |
| items.ocr.since | date-time | null | |
| items.ocr.until | date-time | null | |
| items.ocr.cap_pages | string | integer | null | |
| items.ocr.pages | string | integer | Pages read for text in images so far. |
| items.ocr.files | string | integer | Files whose text in images was read and is searchable. |
| items.ocr.backfill | SearchBackfill | 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/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
| items | array of SearchSettingsItem | |
| items.workspace | string | The Drive's workspace: pass it back as `workspace` to change this Drive. |
| items.kind | "personal" | "team" | |
| items.team | object | null | |
| items.team.id | string | The team plan. |
| items.team.name | string | null | |
| items.plan | string | null | |
| items.ocr | object | |
| items.ocr.allowed | boolean | The plan offers text in images: Drive 5 TB, Team Drive Pro and Team Drive Enterprise. |
| items.ocr.on | boolean | |
| items.ocr.since | date-time | null | |
| items.ocr.until | date-time | null | |
| items.ocr.cap_pages | string | 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"
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)
| ocr | boolean | On (true) or off (false). |
| workspace? | string | The Drive to change (from `GET /v1/search/settings`); default your own Drive. |
A name ending in ? is optional.
Response 200
| item | SearchSettingsItem | |
| item.workspace | string | The Drive's workspace: pass it back as `workspace` to change this Drive. |
| item.kind | "personal" | "team" | |
| item.team | object | null | |
| item.team.id | string | The team plan. |
| item.team.name | string | null | |
| item.plan | string | null | |
| item.ocr | object | |
| item.ocr.allowed | boolean | The plan offers text in images: Drive 5 TB, Team Drive Pro and Team Drive Enterprise. |
| item.ocr.on | boolean | |
| item.ocr.since | date-time | null | |
| item.ocr.until | date-time | null | |
| item.ocr.cap_pages | string | integer | null | |
| changed | boolean | False when it already was that way. |
| backfill | string | 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
| nodeId | string | The picture's id (a Drive file). |
Query parameters
| limit? | string | How many, 1–24 (default 12). |
Response 200
| items | array of object | Most alike first. Only pictures you can open now, in the Drives you can search (each one checked again). |
| items.id | string | |
| items.parent_id | string | null | |
| items.kind | "folder" | "file" | |
| items.name | string | |
| items.size | string | The current version's size (0 for a folder). |
| items.mime | string | null | |
| items.content_hash | string | null | |
| items.versions | string | integer | Versions kept (the current one included). |
| items.modified_at | date-time | When the content last changed. |
| items.updated_at | date-time | When anything last changed (a rename, a move…). |
| items.created_at | date-time | |
| items.trashed_at | date-time | null | |
| items.purge_after | date-time | null | |
| items.starred? | boolean | |
| items.seq | string | The change that last touched it (see GET /v1/drive/changes). |
| items.thumb_url? | string | null | |
| items.inside? | string | integer | Trash: how many items a trashed folder holds. |
| items.scan_state? | "pending" | "clean" | "flagged" | "failed" | "released" | "not_scannable" | "error" | |
| items.version_id | string | null | |
| items.lock? | object | Locked for editing: others' saves are refused until then (P6). |
| items.lock.user_id | string | |
| items.lock.name | string | |
| items.lock.until | date-time | |
| items.lock.mine | boolean | |
| items.conflict? | object | This file is a sync conflict's copy of `file_id` (P6). |
| items.conflict.id | string | |
| items.conflict.file_id | string | |
| items.path | array of object | The folders above it, from the top you can see. |
| items.path.id | string | |
| items.path.name | string | |
| items.space | any | Its space (as in `GET /v1/drive/search`). |
| items.score | number | How 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
| plans | array of BackupPlan | |
| plans.scope | "personal" | "team" | |
| plans.team_id | string | null | |
| plans.team_name | string | null | |
| plans.plan | string | |
| plans.plan_name | string | |
| plans.status | "active" | "trialing" | "past_due" | "read_only" | |
| plans.writable | boolean | False on a plan that ended: backups can be restored until deleted, nothing new is backed up. |
| plans.bytes | string | The room. |
| plans.stored | string | What the plan's packs take. |
| plans.devices_limit | string | integer | null | |
| plans.devices_used | string | integer | |
| plans.version_days | string | integer | |
| plans.private_key | boolean | The plan offers a private key (only you can read the backup). |
| plans.manages | boolean | The caller sees and manages every device on the plan (a team's owner or admins). |
| plans.team_policy | map of any | null | |
| devices | array of object | |
| devices.id | string | |
| devices.name | string | |
| devices.kind | "computer" | "server" | "phone" | "volume" | |
| devices.os | string | null | |
| devices.client | "cli" | "desktop" | "phone" | |
| devices.client_version | string | null | |
| devices.volume_serial | string | null | |
| devices.status | "ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious" | |
| devices.state | "idle" | "running" | "error" | |
| devices.progress | map of any | null | |
| devices.paused | boolean | |
| devices.key_mode | "sendflo" | "private" | |
| devices.suspicious | object | null | |
| devices.suspicious.at | date-time | |
| devices.suspicious.reason? | string | |
| devices.suspicious.changed? | string | integer | |
| devices.suspicious.sample? | array of string | |
| devices.changes_ok_until | date-time | null | |
| devices.policy | map of any | null | |
| devices.bytes | string | What the last complete snapshot covers. |
| devices.files | string | integer | |
| devices.last_snapshot_id | string | null | |
| devices.last_snapshot_at | date-time | null | |
| devices.last_seen_at | date-time | null | |
| devices.last_error | string | null | |
| devices.last_error_at | date-time | null | |
| devices.created_at | date-time | |
| devices.mine | boolean | It's the caller's own device (false: a team member's, seen by a team admin). |
| devices.user | object | |
| devices.user.id | string | |
| devices.user.name? | string | |
| devices.user.email? | string | |
| devices.team_id | string | 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"
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)
| name | string | What to call it ("Dana's laptop"). |
| kind? | string | `computer` (the default), `server`, `phone` or `volume`. |
| os? | string | null | Its operating system and version. |
| client? | string | `cli` (the default), `desktop` or `phone`. |
| client_version? | string | null | The backup client's version. |
| volume_serial? | string | null | An external drive's volume serial. |
| policy? | map of any | null | What 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 | null | The team plan to back up to. |
A name ending in ? is optional.
Response 200
| device | BackupDevice | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.client | "cli" | "desktop" | "phone" | |
| device.client_version | string | null | |
| device.volume_serial | string | null | |
| device.status | "ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious" | |
| device.state | "idle" | "running" | "error" | |
| device.progress | map of any | null | |
| device.paused | boolean | |
| device.key_mode | "sendflo" | "private" | |
| device.suspicious | object | null | |
| device.suspicious.at | date-time | |
| device.suspicious.reason? | string | |
| device.suspicious.changed? | string | integer | |
| device.suspicious.sample? | array of string | |
| device.changes_ok_until | date-time | null | |
| device.policy | map of any | null | |
| device.bytes | string | What the last complete snapshot covers. |
| device.files | string | integer | |
| device.last_snapshot_id | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.last_seen_at | date-time | null | |
| device.last_error | string | null | |
| device.last_error_at | date-time | null | |
| device.created_at | date-time | |
| device.mine | boolean | It's the caller's own device (false: a team member's, seen by a team admin). |
| device.user | object | |
| device.user.id | string | |
| device.user.name? | string | |
| device.user.email? | string | |
| reattached | true | |
| plan | BackupPlan | |
| plan.scope | "personal" | "team" | |
| plan.team_id | string | null | |
| plan.team_name | string | null | |
| plan.plan | string | |
| plan.plan_name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: backups can be restored until deleted, nothing new is backed up. |
| plan.bytes | string | The room. |
| plan.stored | string | What the plan's packs take. |
| plan.devices_limit | string | integer | null | |
| plan.devices_used | string | integer | |
| plan.version_days | string | integer | |
| plan.private_key | boolean | The plan offers a private key (only you can read the backup). |
| plan.manages | boolean | The caller sees and manages every device on the plan (a team's owner or admins). |
| plan.team_policy | map 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
| id | string | The device's id. |
Response 200
| device | BackupDevice | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.client | "cli" | "desktop" | "phone" | |
| device.client_version | string | null | |
| device.volume_serial | string | null | |
| device.status | "ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious" | |
| device.state | "idle" | "running" | "error" | |
| device.progress | map of any | null | |
| device.paused | boolean | |
| device.key_mode | "sendflo" | "private" | |
| device.suspicious | object | null | |
| device.suspicious.at | date-time | |
| device.suspicious.reason? | string | |
| device.suspicious.changed? | string | integer | |
| device.suspicious.sample? | array of string | |
| device.changes_ok_until | date-time | null | |
| device.policy | map of any | null | |
| device.bytes | string | What the last complete snapshot covers. |
| device.files | string | integer | |
| device.last_snapshot_id | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.last_seen_at | date-time | null | |
| device.last_error | string | null | |
| device.last_error_at | date-time | null | |
| device.created_at | date-time | |
| device.mine | boolean | It's the caller's own device (false: a team member's, seen by a team admin). |
| device.user | object | |
| device.user.id | string | |
| device.user.name? | string | |
| device.user.email? | string | |
| plan | BackupPlan | |
| plan.scope | "personal" | "team" | |
| plan.team_id | string | null | |
| plan.team_name | string | null | |
| plan.plan | string | |
| plan.plan_name | string | |
| plan.status | "active" | "trialing" | "past_due" | "read_only" | |
| plan.writable | boolean | False on a plan that ended: backups can be restored until deleted, nothing new is backed up. |
| plan.bytes | string | The room. |
| plan.stored | string | What the plan's packs take. |
| plan.devices_limit | string | integer | null | |
| plan.devices_used | string | integer | |
| plan.version_days | string | integer | |
| plan.private_key | boolean | The plan offers a private key (only you can read the backup). |
| plan.manages | boolean | The caller sees and manages every device on the plan (a team's owner or admins). |
| plan.team_policy | map 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
| id | string | The device's id. |
Request body (JSON)
| name? | string | A new name. |
| paused? | boolean | Pause or resume its backup. |
| policy? | map of any | null | What the client backs up. |
A name ending in ? is optional.
Response 200
| device | BackupDevice | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.client | "cli" | "desktop" | "phone" | |
| device.client_version | string | null | |
| device.volume_serial | string | null | |
| device.status | "ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious" | |
| device.state | "idle" | "running" | "error" | |
| device.progress | map of any | null | |
| device.paused | boolean | |
| device.key_mode | "sendflo" | "private" | |
| device.suspicious | object | null | |
| device.suspicious.at | date-time | |
| device.suspicious.reason? | string | |
| device.suspicious.changed? | string | integer | |
| device.suspicious.sample? | array of string | |
| device.changes_ok_until | date-time | null | |
| device.policy | map of any | null | |
| device.bytes | string | What the last complete snapshot covers. |
| device.files | string | integer | |
| device.last_snapshot_id | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.last_seen_at | date-time | null | |
| device.last_error | string | null | |
| device.last_error_at | date-time | null | |
| device.created_at | date-time | |
| device.mine | boolean | It's the caller's own device (false: a team member's, seen by a team admin). |
| device.user | object | |
| device.user.id | string | |
| 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
| id | string | The device's id. |
Response 200
| removed | true | |
| kept_until | date-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
| id | string | The device's id. |
Request body (JSON)
| ids | array of string | Blob ids (64 hex characters). |
A name ending in ? is optional.
Response 200
| have | array 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
| id | string | The device's id. |
Request body (JSON)
| ids | array of string | Blob ids. |
A name ending in ? is optional.
Response 200
| blobs | array of object | |
| blobs.id | string | |
| blobs.kind | "data" | "tree" | |
| blobs.url | string | |
| blobs.offset | string | |
| blobs.length | string | integer | |
| blobs.compression | string | 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
| id | string | The device's id. |
Request body (JSON)
| state | string | `running`, `idle` or `error`. |
| progress? | map of any | null | phase, files_done, files_total, bytes_done, bytes_total. |
| error? | string | null | What went wrong. |
| client_version? | string | null | The 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
| paused | boolean | |
| policy | map 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
| id | string | The device's id (a private-key device, its own computer). |
Response 200
| report | object | |
| report.id | string | |
| report.state | string | |
| snapshots | array of object | |
| snapshots.id | string | |
| snapshots.root_tree | string |
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
| id | string | The device's id. |
| rid | string | The report's id. |
Request body (JSON)
| ids | array of string | Blob ids (64 hex characters). |
A name ending in ? is optional.
Response 200
| added | string | 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
| id | string | The device's id. |
| rid | string | The report's id. |
Request body (JSON)
| snapshots | array of string | The ids of the snapshots walked. |
A name ending in ? is optional.
Response 200
| report | object | |
| report.id | string | |
| report.state | string | |
| report.blobs | string | 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
| id | string | The device's id. |
Response 200
| on | boolean | |
| share_team | boolean | |
| since | date-time | null | |
| mine | boolean | |
| team | object | null | |
| team.id | string | |
| team.name | string | null | |
| team.shows | boolean | |
| last | object | null | |
| last.at | date-time | |
| last.lat | number | |
| last.lon | number | |
| last.accuracy_km | string | integer | |
| last.city | string | null | |
| last.region | string | null | |
| last.country | string | null | |
| last.source | "ip" | "country" | |
| history | array of object | |
| history.at | date-time | |
| history.lat | number | |
| history.lon | number | |
| history.accuracy_km | string | integer | |
| history.city | string | null | |
| history.region | string | null | |
| history.country | string | 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
| id | string | The device's id. |
Request body (JSON)
| on | boolean | true to keep where it is, false to stop and delete the history. |
| share_team? | boolean | Also let the team's owner and admins see it (only while the team shows locations). |
A name ending in ? is optional.
Response 200
| on | boolean | |
| share_team | boolean | |
| since | date-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
| id | string | The device's id. |
Request body (JSON)
| hash | string | BLAKE3 of the whole pack (64 hex characters). |
| size | string | integer | The pack's bytes (up to 1 GiB). |
| blobs | array of object | Each blob's id, kind (`data` or `tree`), offset, length and `z: df` when raw-deflate compressed. |
| blobs.id | string | |
| blobs.kind | string | |
| blobs.offset | string | integer | |
| blobs.length | string | integer | |
| blobs.z? | string |
A name ending in ? is optional.
Response 200
| state | "done" | |
| pack_id | string |
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
| id | string | The device's id. |
Request body (JSON)
| snapshot_id | string | The backup to restore from. |
| paths | array of array of string | The files and folders, each a JSON list of names. |
A name ending in ? is optional.
Response 201
| restore | BackupRestore | |
| restore.id | string | |
| restore.device_id | string | |
| restore.snapshot_id | string | |
| restore.name | string | The ZIP's name as downloaded. |
| restore.state | "queued" | "running" | "ready" | "failed" | "canceled" | "expired" | |
| restore.files | string | integer | |
| restore.bytes | string | What the files hold. |
| restore.zip_bytes | string | The ZIP's exact size. |
| restore.done_bytes | string | How much of the ZIP is put together. |
| restore.paths | array of array of string | |
| restore.error | string | null | |
| restore.created_at | date-time | |
| restore.finished_at | date-time | null | |
| restore.expires_at | date-time | null | |
| restore.downloaded_at | date-time | null | |
| restore.user_id | string | Who 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
| id | string | The device's id. |
Request body (JSON)
| root_tree | string | The root tree blob's id. |
| started_at? | string | null | When the run began. |
| files? | string | integer | Files it covers. |
| dirs? | string | integer | Folders. |
| bytes? | string | integer | string | Bytes it covers. |
| new_bytes? | string | integer | string | Bytes this run stored. |
| new_blobs? | string | integer | Blobs this run stored. |
| paths? | array of string | The backed-up paths. |
| skipped? | array of any | Files not read, each {path, reason}. |
| trigger? | string | `first`, `scheduled` or `manual`. |
| client_version? | string | null | The client's version. |
A name ending in ? is optional.
Response 201
| snapshot | BackupSnapshot | |
| snapshot.id | string | |
| snapshot.device_id | string | |
| snapshot.root_tree | string | The 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_at | date-time | |
| snapshot.finished_at | date-time | |
| snapshot.files | string | integer | |
| snapshot.dirs | string | integer | |
| snapshot.bytes | string | Everything the snapshot covers. |
| snapshot.new_bytes | string | What this run newly stored. |
| snapshot.new_blobs | string | integer | |
| snapshot.paths | array of string | |
| snapshot.skipped | array of any | Files the client couldn't read (path and reason). |
| snapshot.client_version | string | 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
| id | string | The device's id. |
Query parameters
| limit? | string | number | At most this many (100 by default, up to 500). |
Response 200
| data | array of BackupSnapshot | |
| data.id | string | |
| data.device_id | string | |
| data.root_tree | string | The 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_at | date-time | |
| data.finished_at | date-time | |
| data.files | string | integer | |
| data.dirs | string | integer | |
| data.bytes | string | Everything the snapshot covers. |
| data.new_bytes | string | What this run newly stored. |
| data.new_blobs | string | integer | |
| data.paths | array of string | |
| data.skipped | array of any | Files the client couldn't read (path and reason). |
| data.client_version | string | 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
| id | string | The device's id. |
| sid | string | The snapshot's id. |
Query parameters
| path | string | The file, as a JSON list of names. |
Response 200
| name | string | |
| size | string | integer | |
| mtime | date-time | |
| chunks | array of object | |
| chunks.id | string | |
| chunks.url | string | |
| chunks.offset | string | integer | |
| chunks.length | string | integer | |
| chunks.compression | string | 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
| id | string | The device's id. |
| sid | string | The snapshot's id. |
Query parameters
| path? | string | The folder, as a JSON list of names. |
Response 200
| snapshot | BackupSnapshot | |
| snapshot.id | string | |
| snapshot.device_id | string | |
| snapshot.root_tree | string | The 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_at | date-time | |
| snapshot.finished_at | date-time | |
| snapshot.files | string | integer | |
| snapshot.dirs | string | integer | |
| snapshot.bytes | string | Everything the snapshot covers. |
| snapshot.new_bytes | string | What this run newly stored. |
| snapshot.new_blobs | string | integer | |
| snapshot.paths | array of string | |
| snapshot.skipped | array of any | Files the client couldn't read (path and reason). |
| snapshot.client_version | string | null | |
| path | array of string | |
| entries | array of object | |
| entries.name | string | |
| entries.type | "file" | "dir" | "symlink" | |
| entries.size | string | integer | null | |
| entries.mtime | date-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
| id | string | The device's id. |
Request body (JSON)
| reason | string | `extensions`, `ransom_note` or `entropy`. |
| changed | string | integer | How many files changed. |
| sample? | array of string | Some of their paths. |
| extensions? | map of string | integer | Counts per extension. |
A name ending in ? is optional.
Response 200
| device | BackupDevice | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.client | "cli" | "desktop" | "phone" | |
| device.client_version | string | null | |
| device.volume_serial | string | null | |
| device.status | "ok" | "running" | "overdue" | "failing" | "paused" | "pending" | "suspicious" | |
| device.state | "idle" | "running" | "error" | |
| device.progress | map of any | null | |
| device.paused | boolean | |
| device.key_mode | "sendflo" | "private" | |
| device.suspicious | object | null | |
| device.suspicious.at | date-time | |
| device.suspicious.reason? | string | |
| device.suspicious.changed? | string | integer | |
| device.suspicious.sample? | array of string | |
| device.changes_ok_until | date-time | null | |
| device.policy | map of any | null | |
| device.bytes | string | What the last complete snapshot covers. |
| device.files | string | integer | |
| device.last_snapshot_id | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.last_seen_at | date-time | null | |
| device.last_error | string | null | |
| device.last_error_at | date-time | null | |
| device.created_at | date-time | |
| device.mine | boolean | It's the caller's own device (false: a team member's, seen by a team admin). |
| device.user | object | |
| device.user.id | string | |
| 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? | string | A team plan (your own plan when absent). |
Response 200
| key_id | string | null | |
| keys | array of object | |
| keys.kind | "passphrase" | "recovery" | |
| keys.wrapped | string | |
| keys.kdf | map of any | |
| keys.key_id | string | |
| keys.created_at | date-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"
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 | null | A team plan. |
| key_id | string | BLAKE3 derive_key("sendflo backup 2026-10-29 key id", key), hex. |
| passphrase | object | {wrapped, kdf: {alg: pbkdf2-sha256, iterations ≥ 600000, salt}}. |
| passphrase.wrapped | string | |
| passphrase.kdf | map of any | |
| recovery | object | {wrapped, kdf: {alg: raw}}. |
| recovery.wrapped | string | |
| recovery.kdf | map of any |
A name ending in ? is optional.
Response 201
| key_id | string | |
| keys | array of object | |
| keys.kind | string | |
| keys.created_at | date-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 | null | A team plan. |
| key_id | string | The key's id. |
| wrapped | string | The key wrapped by the new passphrase. |
| kdf | map of any | Its PBKDF2 parameters. |
A name ending in ? is optional.
Response 200
| key_id | string | |
| created_at | date-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
| upload | string | The upload's id. |
Response 200
| cancelled | true |
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
| upload | string | The upload's id. |
Response 200
| pack_id | string | |
| size | string | |
| blobs | string | 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
| upload | string | The upload's id. |
Request body (JSON)
| parts | array of string | integer | Part numbers (from 1). |
A name ending in ? is optional.
Response 200
| part_urls | array 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
| data | array of object | |
| data.id | string | |
| data.device_id | string | |
| data.snapshot_id | string | |
| data.name | string | The ZIP's name as downloaded. |
| data.state | "queued" | "running" | "ready" | "failed" | "canceled" | "expired" | |
| data.files | string | integer | |
| data.bytes | string | What the files hold. |
| data.zip_bytes | string | The ZIP's exact size. |
| data.done_bytes | string | How much of the ZIP is put together. |
| data.paths | array of array of string | |
| data.error | string | null | |
| data.created_at | date-time | |
| data.finished_at | date-time | null | |
| data.expires_at | date-time | null | |
| data.downloaded_at | date-time | null | |
| data.user_id | string | Who asked for it. |
| data.device_name | string | null | |
| data.mine | boolean |
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/backup/restores/{rid}Get a restore
Key scope: backup.read
Path parameters
| rid | string | The restore's id. |
Response 200
| restore | BackupRestore | |
| restore.id | string | |
| restore.device_id | string | |
| restore.snapshot_id | string | |
| restore.name | string | The ZIP's name as downloaded. |
| restore.state | "queued" | "running" | "ready" | "failed" | "canceled" | "expired" | |
| restore.files | string | integer | |
| restore.bytes | string | What the files hold. |
| restore.zip_bytes | string | The ZIP's exact size. |
| restore.done_bytes | string | How much of the ZIP is put together. |
| restore.paths | array of array of string | |
| restore.error | string | null | |
| restore.created_at | date-time | |
| restore.finished_at | date-time | null | |
| restore.expires_at | date-time | null | |
| restore.downloaded_at | date-time | null | |
| restore.user_id | string | Who 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
| rid | string | The restore's id. |
Response 200
| restore | BackupRestore | |
| restore.id | string | |
| restore.device_id | string | |
| restore.snapshot_id | string | |
| restore.name | string | The ZIP's name as downloaded. |
| restore.state | "queued" | "running" | "ready" | "failed" | "canceled" | "expired" | |
| restore.files | string | integer | |
| restore.bytes | string | What the files hold. |
| restore.zip_bytes | string | The ZIP's exact size. |
| restore.done_bytes | string | How much of the ZIP is put together. |
| restore.paths | array of array of string | |
| restore.error | string | null | |
| restore.created_at | date-time | |
| restore.finished_at | date-time | null | |
| restore.expires_at | date-time | null | |
| restore.downloaded_at | date-time | null | |
| restore.user_id | string | Who 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
| rid | string | The restore's id. |
Response 200
| url | string | |
| name | string | |
| size | string |
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
| id | string | The team plan's id. |
Response 200
| shows | boolean | |
| manages | boolean | |
| devices | array of object | |
| devices.device_id | string | |
| devices.last | object | |
| devices.last.at | date-time | |
| devices.last.lat | number | |
| devices.last.lon | number | |
| devices.last.accuracy_km | string | integer | |
| devices.last.city | string | null | |
| devices.last.region | string | null | |
| devices.last.country | string | 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
| id | string | The team plan's id. |
Response 200
| policy | object | |
| policy.required | array of "desktop" | "documents" | "pictures" | |
| policy.exclude | array of string | |
| policy.max_mbps | number | null | |
| policy.may_pause | boolean | |
| policy.may_remove | boolean | |
| 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)
| label | string | What the person sees in Settings → Encryption ("CLI on build-box"). |
| platform | "web" | "desktop" | "ios" | "android" | "cli" | `cli` for the SDK and the CLI. |
| public_key | string | The device's X-Wing public key, base64url (1216 bytes). |
A name ending in ? is optional.
Response 201
| id | string | |
| label | string | |
| platform | "web" | "desktop" | "ios" | "android" | "cli" | |
| status | "pending" | "active" | "revoked" | |
| public_key | string | base64url |
| fingerprint | string | |
| code | string | The six digits the person compares on the device that approves this one. |
| unlocked_via | "setup" | "device" | "recovery" | "passkey" | null | |
| approved_by | string | null | |
| created_at | date-time | |
| approved_at | date-time | null | |
| revoked_at | date-time | null | |
| enrolled_by_key | boolean | An 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
| data | array of E2eDevice | |
| data.id | string | |
| data.label | string | |
| data.platform | "web" | "desktop" | "ios" | "android" | "cli" | |
| data.status | "pending" | "active" | "revoked" | |
| data.public_key | string | base64url |
| data.fingerprint | string | |
| data.code | string | The six digits the person compares on the device that approves this one. |
| data.unlocked_via | "setup" | "device" | "recovery" | "passkey" | null | |
| data.approved_by | string | null | |
| data.created_at | date-time | |
| data.approved_at | date-time | null | |
| data.revoked_at | date-time | null | |
| data.enrolled_by_key | boolean | An 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"
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
| id | string | The device's id. |
Response 200
| ok | true | |
| rotate_recommended | boolean |
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
| id | string | The waiting device's id. |
Request body (JSON)
| via | "recovery" | "passkey" | `recovery`. |
| credential? | string | Unused for `recovery`. |
| sealed | string | The current identity seed sealed to the device's own key (HPKE, `SEAL_INFO.identity`). |
A name ending in ? is optional.
Response 200
| id | string | |
| label | string | |
| platform | "web" | "desktop" | "ios" | "android" | "cli" | |
| status | "pending" | "active" | "revoked" | |
| public_key | string | base64url |
| fingerprint | string | |
| code | string | The six digits the person compares on the device that approves this one. |
| unlocked_via | "setup" | "device" | "recovery" | "passkey" | null | |
| approved_by | string | null | |
| created_at | date-time | |
| approved_at | date-time | null | |
| revoked_at | date-time | null | |
| enrolled_by_key | boolean | An 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
| data | array of object | |
| data.seq | string | integer | |
| data.kind | string | |
| data.payload | map of any | |
| data.device_id | string | null | |
| data.at | date-time | |
| data.prev_hash | string | |
| data.hash | string |
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/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
| suite | string | |
| user_id | string | The person the keys belong to (the key's maker). |
| identity | E2eIdentity | null | |
| identity.version | string | integer | |
| identity.suite | string | `xwing-hpke1`: X-Wing (X25519 + ML-KEM-768) with HPKE (RFC 9180). |
| identity.public_key | string | base64url |
| identity.fingerprint | string | |
| identity.created_at | date-time | |
| devices | array of E2eDevice | |
| devices.id | string | |
| devices.label | string | |
| devices.platform | "web" | "desktop" | "ios" | "android" | "cli" | |
| devices.status | "pending" | "active" | "revoked" | |
| devices.public_key | string | base64url |
| devices.fingerprint | string | |
| devices.code | string | The six digits the person compares on the device that approves this one. |
| devices.unlocked_via | "setup" | "device" | "recovery" | "passkey" | null | |
| devices.approved_by | string | null | |
| devices.created_at | date-time | |
| devices.approved_at | date-time | null | |
| devices.revoked_at | date-time | null | |
| devices.enrolled_by_key | boolean | An API key or an app asked for it (the SDK or the CLI). |
| recovery | boolean | |
| passkeys | array of string | |
| chain | object | null | |
| chain.seq | string | integer | |
| chain.hash | string |
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/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? | string | An unlocked device's id. |
| kind? | "recovery" | "passkey" | `recovery` (a key can't read passkey copies). |
| credential? | string | With `kind=passkey`, from a browser only. |
Response 200
| identities | array of E2eIdentity | |
| identities.version | string | integer | |
| identities.suite | string | `xwing-hpke1`: X-Wing (X25519 + ML-KEM-768) with HPKE (RFC 9180). |
| identities.public_key | string | base64url |
| identities.fingerprint | string | |
| identities.created_at | date-time | |
| seals | array of object | |
| seals.version | string | integer | |
| seals.kind | string | |
| seals.target | string | |
| seals.sealed | string | base64url: HPKE enc ‖ ciphertext of a 32-byte seed. |
| history | array of object | |
| history.version | string | integer | |
| history.kind | string | |
| history.target | string | |
| history.sealed | string | base64url: 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/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
| data | array of object | |
| data.team_key_id | string | |
| data.plan_id | string | |
| data.kind | "recovery" | "inbox" | |
| data.ref_id | string | null | |
| data.version | string | integer | |
| data.public_key | string | |
| data.fingerprint | string | |
| data.identity_version | string | integer | |
| data.sealed | string |
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"
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_id | string | The audit log's event id (UUIDv7) — the same row GET /v1/audit returns. |
| occurred_at | date-time | |
| action | "LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD" | |
| outcome | "success" | "failure" | "denied" | |
| link_id | string | null | |
| file_id | string | null | |
| file_name | string | null | |
| sender_user_id | string | null | |
| recipient | object | |
| recipient.kind | "user" | "recipient" | "guest" | |
| recipient.email | string | null | |
| recipient.verified | boolean | The address was proved with a code (or is a signed-in account's own). |
| recipient.user_id | string | null | |
| ip | string | null | |
| ip_source | "cloudflare" | "claimed" | "direct" | "internal" | "none" | null | |
| network | object | null | |
| network.asn | string | integer | null | |
| network.org | string | null | |
| network.class | string | null | |
| country | string | null | |
| device | string | null | |
| session_id | string | null | |
| details | map of any | Bytes 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_id | string | The audit log's event id (UUIDv7) — the same row GET /v1/audit returns. |
| occurred_at | date-time | |
| action | "LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD" | |
| outcome | "success" | "failure" | "denied" | |
| link_id | string | null | |
| file_id | string | null | |
| file_name | string | null | |
| sender_user_id | string | null | |
| recipient | object | |
| recipient.kind | "user" | "recipient" | "guest" | |
| recipient.email | string | null | |
| recipient.verified | boolean | The address was proved with a code (or is a signed-in account's own). |
| recipient.user_id | string | null | |
| ip | string | null | |
| ip_source | "cloudflare" | "claimed" | "direct" | "internal" | "none" | null | |
| network | object | null | |
| network.asn | string | integer | null | |
| network.org | string | null | |
| network.class | string | null | |
| country | string | null | |
| device | string | null | |
| session_id | string | null | |
| details | map of any | Bytes 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_id | string | The audit log's event id (UUIDv7) — the same row GET /v1/audit returns. |
| occurred_at | date-time | |
| action | "LINK_OPENED" | "FILE_PREVIEWED" | "FILE_DOWNLOAD" | |
| outcome | "success" | "failure" | "denied" | |
| link_id | string | null | |
| file_id | string | null | |
| file_name | string | null | |
| sender_user_id | string | null | |
| recipient | object | |
| recipient.kind | "user" | "recipient" | "guest" | |
| recipient.email | string | null | |
| recipient.verified | boolean | The address was proved with a code (or is a signed-in account's own). |
| recipient.user_id | string | null | |
| ip | string | null | |
| ip_source | "cloudflare" | "claimed" | "direct" | "internal" | "none" | null | |
| network | object | null | |
| network.asn | string | integer | null | |
| network.org | string | null | |
| network.class | string | null | |
| country | string | null | |
| device | string | null | |
| session_id | string | null | |
| details | map of any | Bytes 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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | 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
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | null |
A name ending in ? is optional.
drive.item.renamedA Drive file or folder was renamed
Its name changed.
data
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | null |
A name ending in ? is optional.
drive.item.movedA Drive file or folder was moved
It went to another folder.
data
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | 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
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | null |
A name ending in ? is optional.
drive.item.restoredA Drive file or folder came back from the trash
Its owner restored it.
data
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | 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
| item | DriveItemV2 | |
| item.id | string | |
| item.kind | "file" | "folder" | |
| item.name | string | null | |
| item.parent_id | string | null | |
| item.version_id | string | null | |
| item.space | object | |
| item.space.id | string | |
| item.space.kind | "personal" | "team" | |
| item.seq | string | The change's place in the space's journal (GET /v1/drive/changes). |
| actor_user_id | string | 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
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | 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
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | integer |
A name ending in ? is optional.
backup.snapshot.createdA backup finished
A device's backup run made its snapshot.
data
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | integer | |
| snapshot | object | |
| snapshot.id | string | |
| snapshot.status | "complete" | "partial" | |
| snapshot.trigger | string | |
| snapshot.files | string | integer | |
| snapshot.bytes | string | |
| snapshot.new_bytes | string | |
| snapshot.started_at | date-time | |
| snapshot.finished_at | date-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
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | integer | |
| error | string | 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
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | integer | |
| error | string | 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
| restore | object | |
| restore.id | string | |
| restore.device_id | string | |
| restore.name | string | |
| restore.files | string | integer | |
| restore.zip_bytes | string | |
| restore.expires_at | date-time | null | |
| restore.user_id | string |
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
| device | BackupDeviceV2 | |
| device.id | string | |
| device.name | string | |
| device.kind | "computer" | "server" | "phone" | "volume" | |
| device.os | string | null | |
| device.user_id | string | Whose device it is. |
| device.status | string | null | |
| device.last_snapshot_at | date-time | null | |
| device.bytes | string | What its last backup covers. |
| device.files | string | integer | |
| report | object | |
| report.reason | string | |
| report.changed | string | integer | |
| report.sample | array of string | |
| report.extensions | map of string | integer |
A name ending in ? is optional.
agent_task.completedA task completed
The task finished (completed).
data
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-time |
A name ending in ? is optional.
agent_task.expiredA task expired
Nobody took the task in time (expired).
data
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| task | AgentTask | A task handed to an AI agent on a computer or in the cloud. |
| task.id | string | |
| task.state | "submitted" | "delivered" | "awaiting_approval" | "queued" | "handed_off" | "working" | "input_required" | "completed" | "failed" | "canceled" | "rejected" | "expired" | |
| task.label | string | The state in plain words ("Done", "Needs your answer"…). |
| task.target_kind | "device" | "cloud" | |
| task.agent | object | |
| task.agent.ref | string | |
| task.agent.name | string | |
| task.agent.runner | string | folder, command, acp, channel, session, webhook, a2a, routine or pull. |
| task.device | object | null | |
| task.device.id | string | |
| task.device.name | string | null | |
| task.project_id | string | null | |
| task.cloud_agent_id | string | null | |
| task.session_ref | string | null | |
| task.from | object | |
| task.from.name | string | |
| task.from.user_id | string | |
| task.from.device_id | string | null | |
| task.instruction | string | null | |
| task.instruction_encrypted | boolean | |
| task.preset | string | null | |
| task.voice_file | string | null | |
| task.files | array of object | |
| task.files.name | string | |
| task.files.size | string | integer | |
| task.files.sha256? | string | |
| task.files.mime? | string | |
| task.total_bytes | string | Bytes, as a decimal string. |
| task.reason | string | null | |
| task.detail | string | null | |
| task.question | AgentTaskQuestion | null | |
| task.question.id | string | |
| task.question.kind | "question" | "permission" | "approval" | |
| task.question.text | string | |
| task.question.options? | array of object | |
| task.question.free_text? | boolean | |
| task.question.asked_at | date-time | |
| task.question.answer? | object | |
| task.result | AgentTaskResult | null | |
| task.result.summary | string | null | |
| task.result.files | array of object | |
| task.result.kept_at | string | null | |
| task.result.expires_at | date-time | null | |
| task.result.data? | any | Free-form JSON the agent returned. |
| task.policy | object | null | |
| task.policy.ask_first? | boolean | |
| task.policy.direct_only? | boolean | |
| task.policy.results? | "send" | "keep" | |
| task.device_transfer_id | string | null | |
| task.transfer_state | string | null | |
| task.cancel_requested | boolean | |
| task.retry_of | string | null | |
| task.role | "sender" | "owner" | "both" | null | |
| task.actions | array of "approve" | "reject" | "answer" | "cancel" | "retry" | What you may do now. |
| task.attempts | string | integer | |
| task.created_at | date-time | |
| task.updated_at | date-time | |
| task.delivered_at | date-time | null | |
| task.started_at | date-time | null | |
| task.finished_at | date-time | null | |
| task.expires_at | date-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
| balance | string | Credits, as a decimal string (can be negative). |
| threshold | string |
A name ending in ? is optional.
credits.topped_upCredits were added
A credit pack bought for the API & Agents workspace is paid.
data
| amount | string | Credits added. |
| balance | string | |
| cents | string | integer | The pack's price in US cents, before tax. |
| transaction_id | string | Paddle'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
| destination | string | The destination's slug. |
| file_id | string | |
| file_name | string |
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
| transfer | object | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| file | object | |
| file.link_id | string | |
| file.slug | string | null | |
| file.name | string | null | |
| approval | "approved" | "changes" | |
| author | object | |
| author.name | string | |
| author.email | string | |
| 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
| transfer | object | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| file | object | |
| file.link_id | string | |
| file.slug | string | null | |
| file.name | string | null | |
| comment | object | |
| comment.id | string | |
| comment.parent_id | string | null | |
| comment.body | string | null | |
| comment.encrypted | boolean | |
| comment.anchor | FeedbackAnchor | null | |
| comment.created_at | date-time | |
| author | object | |
| author.name | string | |
| author.email | string | |
| 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
| transfer | object | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| file | object | |
| file.link_id | string | |
| file.slug | string | null | |
| file.name | string | null | |
| rating | object | |
| rating.value | string | integer | |
| rating.scale | "stars5" | "score10" | |
| author | object | |
| author.name | string | |
| author.email | string | |
| 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
| transfer | object | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| files | string | integer | The 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_id | string | The file's id (for `subject: agent`, the task's id). |
| subject | "file" | "asset" | "agent" | "drive" | |
| name | string | |
| severity | "low" | "medium" | "high" | "critical" | |
| signature | string | |
| whole_send_blocked | boolean |
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_id | string | |
| subject | "file" | "asset" | "agent" | "drive" | |
| name | string | |
| by | "staff" | "owner" | "team_admin" | |
| reason | string |
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_id | string | |
| slug | string | |
| link | object | |
| link.id | string | |
| link.name | string | null | |
| link.kind | "request" | "reception" | |
| file | object | |
| file.id | string | |
| file.name | string | |
| file.size | string | Bytes, as a decimal string. |
| inbox_item_id | string | |
| from | string | null | |
| from_email | string | null | |
| answers | array of object | |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| answers_v2 | array of object | The same answers typed (DZ4). `answers` keeps them as text. |
| answers_v2.id | string | |
| answers_v2.label | string | |
| answers_v2.type | string | short_text, long_text, email, number, date, multiple_choice, opinion_scale, phone, yes_no or consent. |
| answers_v2.value | any | The 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). |
| contact | object | null | |
| contact.name | string | |
| contact.email | string | |
| contact.email_verified | boolean | |
| submission_id | string | null | |
| retention_days | string | integer | |
| expires_at | date-time | |
| ad_supported | boolean | |
| url | string | The received file in SendFlo. |
| scan | object | The 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.severity | string | null | |
| scan.signature | string | 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_id | string | |
| slug | string | |
| link | object | |
| link.id | string | |
| link.name | string | null | |
| link.kind | "request" | "reception" | |
| file | object | |
| file.id | string | null | |
| file.name | string | |
| file.size | string | |
| inbox_item_id | string | |
| from | string | null | |
| from_email | string | null | |
| submission_id | string | null | |
| url | string | |
| scan | object | The 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.severity | string | null | |
| scan.signature | string | 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_id | string | |
| file_request_id | string | |
| slug | string | |
| link | object | |
| link.id | string | |
| link.name | string | null | |
| link.kind | "request" | "reception" | |
| from | string | null | |
| from_email | string | null | |
| contact | object | null | |
| contact.name | string | |
| contact.email | string | |
| contact.email_verified | boolean | |
| answers | array of object | |
| answers.id | string | |
| answers.label | string | |
| answers.value | string | |
| answers_v2 | array of object | |
| answers_v2.id | string | |
| answers_v2.label | string | |
| answers_v2.type | string | |
| answers_v2.value | any | |
| files | array of object | |
| files.inbox_item_id | string | |
| files.file_id | string | null | |
| files.name | string | |
| files.size | string | |
| files.scan | object | The 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.severity | string | null | |
| files.scan.signature | string | null | |
| files.url | string | |
| counts | object | |
| counts.files | string | integer | |
| counts.rejected | string | integer | |
| counts.checking | string | integer | Files still being checked. |
| url | string |
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_id | string | The key's id, or for an app its grant's (Connected apps). |
| kind | "spend_velocity" | "error_rate" | "content_probe" | |
| detail | string | |
| suspended | boolean | |
| subject | "api_key" | "app" | |
| app_name | string | 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The 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
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The 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
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The link in My Links (sign-in needed). |
| who | WebhookWho | |
| who.email | string | null | |
| who.via | "verified" | "emailed" | "sender" | "anonymous" | |
| who.country | string | 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
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The link in My Links (sign-in needed). |
| who | WebhookWho | |
| who.email | string | null | |
| who.via | "verified" | "emailed" | "sender" | "anonymous" | |
| who.country | string | null | |
| bytes | string | |
| complete | boolean |
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
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The link in My Links (sign-in needed). |
| max_downloads | string | 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
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The link in My Links (sign-in needed). |
A name ending in ? is optional.
link.expiredA link expired
The link passed its expiry.
data
| link | WebhookLink | |
| link.id | string | |
| link.slug | string | |
| link.transfer_id | string | The send's first link. |
| link.file_id | string | |
| link.file_name | string | "Encrypted file.<ext>" when the name is sealed. |
| link.size | string | Bytes, as a decimal string. |
| link.encrypted | boolean | |
| link.access | string | |
| link.expires_at | date-time | null | |
| link.url | string | The 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The send in SendFlo (sign-in needed); never a download. |
| string | ||
| replaced | string | 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
| transfer | WebhookTransfer | |
| transfer.id | string | The send's first link. |
| transfer.slug | string | |
| transfer.file_count | string | integer | |
| transfer.subject | string | null | |
| transfer.encrypted | boolean | |
| transfer.access | string | public, tracked or restricted. |
| transfer.expires_at | date-time | null | |
| transfer.url | string | The send in SendFlo (sign-in needed); never a download. |
| string | ||
| link_ids | array 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_request | WebhookRequest | |
| file_request.id | string | |
| file_request.slug | string | |
| file_request.name | string | null | |
| file_request.kind | "request" | "reception" | |
| file_request.url | string | The 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_request | WebhookRequest | |
| file_request.id | string | |
| file_request.slug | string | |
| file_request.name | string | null | |
| file_request.kind | "request" | "reception" | |
| file_request.url | string | The 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_request | WebhookRequest | |
| file_request.id | string | |
| file_request.slug | string | |
| file_request.name | string | null | |
| file_request.kind | "request" | "reception" | |
| file_request.url | string | The 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, in SendFlo (sign-in needed). |
| severity | string | null | |
| signature | string | null |
A name ending in ? is optional.
inbox_item.releasedA flagged file was released
Its owner (or a team admin) marked it safe.
data
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| item | WebhookReceivedItem | |
| item.id | string | |
| item.file_id | string | |
| item.file_name | string | |
| item.size | string | |
| item.from_email | string | null | |
| item.file_request_id | string | null | |
| item.received_at | date-time | |
| item.url | string | Received, 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
| intake | WebhookIntake | |
| intake.id | string | |
| intake.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| intake.external_refs | map of any | null | |
| intake.count | string | integer | How many files arrived. |
| intake.files | array of object | |
| intake.files.item_id | string | |
| intake.files.type | string | null | |
| intake.files.size | string | Bytes, as a decimal string. |
| intake.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| intake.files.name | string | "Collected file.<ext>" unless the intake shares names. |
| intake.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| intake.answers | array of object | Only the answers the intake shares with the bot. |
| intake.answers.id | string | |
| intake.answers.label | string | |
| intake.answers.value | string | |
| intake.handed_to | array of object | |
| intake.handed_to.id | string | |
| intake.handed_to.kind | "user" | "group" | |
| intake.handed_to.grantee_id | string | |
| intake.handed_to.email | string | null | |
| intake.handed_to.permission | "view" | "manage" | |
| intake.handed_to.expires_at | date-time | null | |
| intake.expires_at | date-time | null | |
| intake.submitted_at | date-time | null | |
| intake.ready_at | date-time | null | |
| intake.url | string | The 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
| intake | WebhookIntake | |
| intake.id | string | |
| intake.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| intake.external_refs | map of any | null | |
| intake.count | string | integer | How many files arrived. |
| intake.files | array of object | |
| intake.files.item_id | string | |
| intake.files.type | string | null | |
| intake.files.size | string | Bytes, as a decimal string. |
| intake.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| intake.files.name | string | "Collected file.<ext>" unless the intake shares names. |
| intake.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| intake.answers | array of object | Only the answers the intake shares with the bot. |
| intake.answers.id | string | |
| intake.answers.label | string | |
| intake.answers.value | string | |
| intake.handed_to | array of object | |
| intake.handed_to.id | string | |
| intake.handed_to.kind | "user" | "group" | |
| intake.handed_to.grantee_id | string | |
| intake.handed_to.email | string | null | |
| intake.handed_to.permission | "view" | "manage" | |
| intake.handed_to.expires_at | date-time | null | |
| intake.expires_at | date-time | null | |
| intake.submitted_at | date-time | null | |
| intake.ready_at | date-time | null | |
| intake.url | string | The 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
| intake | WebhookIntake | |
| intake.id | string | |
| intake.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| intake.external_refs | map of any | null | |
| intake.count | string | integer | How many files arrived. |
| intake.files | array of object | |
| intake.files.item_id | string | |
| intake.files.type | string | null | |
| intake.files.size | string | Bytes, as a decimal string. |
| intake.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| intake.files.name | string | "Collected file.<ext>" unless the intake shares names. |
| intake.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| intake.answers | array of object | Only the answers the intake shares with the bot. |
| intake.answers.id | string | |
| intake.answers.label | string | |
| intake.answers.value | string | |
| intake.handed_to | array of object | |
| intake.handed_to.id | string | |
| intake.handed_to.kind | "user" | "group" | |
| intake.handed_to.grantee_id | string | |
| intake.handed_to.email | string | null | |
| intake.handed_to.permission | "view" | "manage" | |
| intake.handed_to.expires_at | date-time | null | |
| intake.expires_at | date-time | null | |
| intake.submitted_at | date-time | null | |
| intake.ready_at | date-time | null | |
| intake.url | string | The 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
| intake | WebhookIntake | |
| intake.id | string | |
| intake.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| intake.external_refs | map of any | null | |
| intake.count | string | integer | How many files arrived. |
| intake.files | array of object | |
| intake.files.item_id | string | |
| intake.files.type | string | null | |
| intake.files.size | string | Bytes, as a decimal string. |
| intake.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| intake.files.name | string | "Collected file.<ext>" unless the intake shares names. |
| intake.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| intake.answers | array of object | Only the answers the intake shares with the bot. |
| intake.answers.id | string | |
| intake.answers.label | string | |
| intake.answers.value | string | |
| intake.handed_to | array of object | |
| intake.handed_to.id | string | |
| intake.handed_to.kind | "user" | "group" | |
| intake.handed_to.grantee_id | string | |
| intake.handed_to.email | string | null | |
| intake.handed_to.permission | "view" | "manage" | |
| intake.handed_to.expires_at | date-time | null | |
| intake.expires_at | date-time | null | |
| intake.submitted_at | date-time | null | |
| intake.ready_at | date-time | null | |
| intake.url | string | The 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
| intake | WebhookIntake | |
| intake.id | string | |
| intake.status | "open" | "opened" | "submitted" | "ready" | "rejected" | "expired" | "closed" | |
| intake.external_refs | map of any | null | |
| intake.count | string | integer | How many files arrived. |
| intake.files | array of object | |
| intake.files.item_id | string | |
| intake.files.type | string | null | |
| intake.files.size | string | Bytes, as a decimal string. |
| intake.files.scan | string | pending, clean, flagged, released, not_scannable or rejected. |
| intake.files.name | string | "Collected file.<ext>" unless the intake shares names. |
| intake.uploader_verified | boolean | The uploader proved their email (never the address itself). |
| intake.answers | array of object | Only the answers the intake shares with the bot. |
| intake.answers.id | string | |
| intake.answers.label | string | |
| intake.answers.value | string | |
| intake.handed_to | array of object | |
| intake.handed_to.id | string | |
| intake.handed_to.kind | "user" | "group" | |
| intake.handed_to.grantee_id | string | |
| intake.handed_to.email | string | null | |
| intake.handed_to.permission | "view" | "manage" | |
| intake.handed_to.expires_at | date-time | null | |
| intake.expires_at | date-time | null | |
| intake.submitted_at | date-time | null | |
| intake.ready_at | date-time | null | |
| intake.url | string | The intake's files in SendFlo, for the people they're handed to (sign-in needed). |
| to | object | |
| to.kind | "user" | "group" | |
| to.id | string | |
| to.permission | "view" | "manage" |
A name ending in ? is optional.