Send files from your code.
The same engine as the app — every file malware-scanned and content-checked before delivery, branded links on our domain, signed proof both ways.
The developer API is available on Pro and above. Create keys in your account — shown once, stored only as a fingerprint. Scopes: send, read, events. Up to 5 active keys (10 in a team workspace); keys created whilst you are in a team workspace will send as the team.
Test mode: a delivery from an ivst_test_ key is sent to your account’s email address, so you see the real thing land in your inbox and no one else can be reached. Test deliveries are clearly marked testMode. A live key (ivst_live_) sends for real, to whoever you address.
Deliveries, not uploads
The unit is a delivery: files + recipients + protections, moving through draft → scanning → ready. You never manage storage; you describe a send and follow its story.
A key, and a per-delivery token
Your API key creates and reads deliveries. Each new delivery also hands back a manageToken that authorizes one thing only: uploading that delivery's files. So the token you put near the upload can't touch anything else — leak it and you lose one delivery, never your account.
The scanning guarantee
Between finalize and ready, every file passes malware scanning, and media passes content checks. There is no scope, plan or flag that skips this.
Test mode
ivst_test_ keys run the entire pipeline — scans, events, certificate — but deliver only to your own account email, and stamp testMode on everything they touch.
Events, and webhooks
Poll GET /v1/events with a cursor, or register a webhook and we push each event to your URL — signed, retried on failure, auto-disabled if your endpoint stays down. Same exactly-once stream either way; you will never see a duplicate delivery.sent.
You declare sizes; we verify them
Give each file's exact size in bytes when you create a delivery — it shapes the upload and counts against your plan. Once the file lands we check the real size: the delivery always shows true numbers, and a file that turns out to be over your plan's limit is rejected and removed.
Proof as a first-class object
Senders can pull a signed receipt of who opened and downloaded what; every delivery mints a certificate the recipient can verify — both check out offline against our published key.
Create a delivery
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_test_..." \
-H "Content-Type: application/json" \
-d '{
"files": [{ "name": "final-cut.mp4", "size": 734003200 }],
"recipients": ["client@example.com"],
"message": "The final cut — enjoy."
}'{
"id": "d23583b7-05f1-4d7f-a6bd-a1e28af4e3b1",
"status": "draft",
"mode": "test",
"shortCode": "SSyNuWN",
"uploads": [{
"fileId": "a512b334-…",
"mode": "multipart", // or "single" under 100 MB, with a ready url
"uploadId": "…", "partSize": 16777216, "partCount": 44
}],
"manageToken": "b_mKzpcj…" // shown once — X-Transfer-Token from here on
}Give each file a name and its exact size in bytes — the size picks the upload shape (one PUT under 100 MB, multipart above) and is checked against your plan, then reconciled against the real bytes when you finalize. On a test key you can omit recipients entirely: every test delivery goes to your own account email.
Upload the bytes
# single mode (files under 100 MB): one PUT
curl -X PUT "<uploads[0].url>" \
-H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \
--data-binary @final-cut.mp4
# multipart (larger): fetch part URLs in batches, PUT each part,
# then complete with the ETags
curl -X POST https://api.ivesentit.com/v1/transfers/<id>/files/<fileId>/parts \
-H "X-Transfer-Token: <manageToken>" \
-d '{ "partNumbers": [1, 2, 3] }'Every upload URL is signed, single-purpose and expiring — and lives on our domain, never a raw cloud hostname.
Finalize — then everything is automatic
curl -X POST https://api.ivesentit.com/v1/transfers/<id>/finalize \
-H "X-Transfer-Token: <manageToken>"Scanning starts. The delivery email goes out when everything is clean and packaged — with a signed delivery certificate your recipient can verify.
Follow the story
curl https://api.ivesentit.com/v1/deliveries/<id> \
-H "Authorization: Bearer ivst_live_..."
curl "https://api.ivesentit.com/v1/events?after=<last-id>" \
-H "Authorization: Bearer ivst_live_..."{
"id": "d23583b7-…", "status": "ready", "testMode": true,
"certificateId": "DLV-D23583B7-TLPJLB", // every delivery carries signed proof
"files": [{ "name": "final-cut.mp4", "uploaded": true, "scan": "clean" }],
"recipients": [{ "to": "client@example.com", "opens": 2, "downloads": 1 }]
}
{ "events": [
{ "type": "delivery.created", "data": { "transferId": "d23583b7-…" } },
{ "type": "delivery.sent", "data": { "certificateId": "DLV-…" } },
{ "type": "delivery.opened", "data": { "recipient": "client@…", "country": "GB" } },
{ "type": "delivery.downloaded", "data": { "bytes": 734003200 } }
], "nextAfter": null }Status shows per-file scan state and per-recipient opens and downloads. Events are cursor-paged, oldest first, kept 30 days.
Everything a delivery can carry goes in the one POST /v1/deliveries body. All optional — a bare send is complete on its own; plan-gated blocks answer 403 UPGRADE_REQUIRED if your plan doesn’t include them. Each block below shows the exact call that enables it.
Password
ProRecipients must enter the password before anything opens. Share it through a different channel than the delivery email.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "accounts-2026.xlsx", "size": 48210 }],
"recipients": ["fd@client.com"],
"password": "correct-horse-battery"
}'The password is never stored in plain text and never appears in any email.
GeoWall
ProName the countries the delivery may be collected from — it refuses everywhere else, at the edge.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "master-audio.wav", "size": 812349440 }],
"recipients": ["label@partner.co.uk"],
"allowedCountries": ["GB", "IE"]
}'Two-letter ISO codes. Attempts from outside the wall are refused before any bytes move.
Download cap
ProSet the total number of downloads the delivery allows before it seals itself.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "press-kit.pdf", "size": 9210331 }],
"recipients": ["press@outlet.com"],
"downloadCap": 3
}'The cap counts completed downloads across all recipients. It cannot be combined with viewOnce (they contradict — the API tells you so).
View once
ProThe delivery burns after its first collection — one look, then gone.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "credentials.pdf", "size": 102400 }],
"recipients": ["new-hire@company.com"],
"viewOnce": true
}'After the first successful collection the delivery is sealed for everyone, sender included.
Scheduled release
ProThe delivery email goes out now; the files stay sealed until the embargo lifts.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "earnings-q3.pdf", "size": 421889 }],
"recipients": ["press@wire.com"],
"releaseAtTime": 1790244000
}'Unix seconds. Recipients see a countdown, not the files, until the moment arrives.
Per-recipient watermarking
TeamEvery recipient gets their own marked copy — a leaked screener points back to exactly one person.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "episode-1-screener.mp4", "size": 2147483648 }],
"recipients": ["critic-a@outlet.com", "critic-b@paper.co"],
"watermark": true,
"watermarkLabel": "CONFIDENTIAL SCREENER"
}'Needs at least one named recipient (the mark is per person). watermarkLabel is optional extra text alongside the recipient identity.
ID-verified delivery
TeamThe files stay locked until the recipient passes an identity check. Add confirm mode and you release each verified person by hand.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "settlement-agreement.pdf", "size": 220144 }],
"recipients": ["counterparty@firm.law"],
"idVerify": true,
"idVerifyConfirm": true
}'Needs named recipients. With idVerifyConfirm the sender reviews each verification and releases the files per person; without it, passing the check unlocks automatically.
Pay to Download
ProAttach a price. Your client pays by card, the files unlock, and the money lands in your own Stripe account — we take 0%.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "wedding-highlights.mp4", "size": 5368709120 }],
"recipients": ["couple@example.com"],
"priceCents": 45000,
"priceCurrency": "gbp"
}'Needs Stripe payouts set up on your account first (Account → Get paid), and at least one named recipient — each personal link is one purchase.
Link-only & open alerts
Two small switches: linkOnly makes a send with no recipients (you share the link yourself); notifyOnOpen emails you on first open.
curl -X POST https://api.ivesentit.com/v1/deliveries \
-H "Authorization: Bearer ivst_live_..." -H "Content-Type: application/json" \
-d '{
"files": [{ "name": "portfolio.pdf", "size": 18320441 }],
"linkOnly": true,
"notifyOnOpen": true
}'A recipient-less send must say linkOnly: true explicitly — silently losing recipients should never look like success.
Quick reference — the full option surface at a glance:
| option | type | what it adds | plan |
|---|---|---|---|
recipients | string[] | Email addresses or @handles. Omit with linkOnly: true to share the link yourself. Ignored on a test key (delivery goes to your own email). | |
message | string | A note that rides with the delivery. | |
password | string | Recipients must enter it before anything opens. | Pro |
allowedCountries | string[] | GeoWall — two-letter codes; the delivery refuses collection everywhere else. | Pro |
downloadCap | integer | Total downloads allowed before the delivery seals itself. | Pro |
viewOnce | boolean | Burns after first collection. | Pro |
releaseAtTime | unix seconds | Scheduled release — files stay sealed until the embargo lifts. | Pro |
retentionSeconds | integer | How long the delivery lives (clamped to your plan's maximum). | |
region | "eu" | "us" | Where the files physically live. | Team for us |
watermark (+ watermarkLabel) | boolean | Per-recipient watermarking — every recipient gets their own marked copy. | Team |
idVerify (+ idVerifyConfirm) | boolean | ID-verified delivery — files unlock after an identity check; confirm mode lets the sender release each verified person by hand. | Team |
priceCents (+ priceCurrency) | integer | Pay to Download — files unlock on card payment straight to your Stripe account. 0% commission. | Pro |
notifyOnOpen | boolean | Email the sender on first open. | |
idempotencyKey | string | Hold one key across retries — an ambiguous timeout can never create a second delivery. |
/v1/deliveriesAPI key · sendCreate a delivery: register files (name + exact byte size) and recipients, get branded upload targets and a manageToken. On a test key recipients is optional and ignored — delivery goes to your account email. Supports message, password, region (eu/us), retentionSeconds, allowedCountries, downloadCap, viewOnce, releaseAtTime and an idempotencyKey held across retries.
/v1/deliveries/:idAPI key · readThe delivery now: status (draft → scanning → ready), per-file scan verdicts, per-recipient opens and downloads, the delivery certificate id, testMode.
/v1/transfers/:id/files/:fileId/partsX-Transfer-TokenMultipart part URLs, up to 1,000 per request — page through for terabyte files.
/v1/transfers/:id/files/:fileId/completeX-Transfer-TokenMark a file uploaded (multipart: include the part ETags).
/v1/transfers/:id/finalizeX-Transfer-TokenAll files in — start scanning. Everything after is automatic.
/v1/eventsAPI key · eventsYour workspace’s event stream: delivery.created, .sent, .opened, .downloaded, .receipt_issued. Cursor-paged with after=<id>; limit ≤ 100.
/v1/webhooksAPI key · eventsRegister an https URL to be POSTed each new event, signed. Returns a signing secret (shown once). Prefer this over polling for a permanent integration.
/v1/webhooksAPI key · eventsList your endpoints with status, failure count and last response code. Never returns the secret.
/v1/webhooks/:idAPI key · eventsRemove an endpoint.
/v1/certificates/:idnoneVerify a delivery certificate — the recipient-side proof of origin. The full SHA-256 unlocks the file list and signed PDF/JSON.
/v1/receipts/:idnoneVerify a proof-of-delivery receipt against the public register.
/v1/receipts/keysnoneThe public signing key — verify receipts and certificates entirely offline.
Register an https URL and we POST every event to it as it happens — no polling loop. Each request carries an X-Ive-Signature header you verify with the signing secret we hand you once at registration:
// verify an incoming webhook (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // too old
const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest();
const given = Buffer.from(v1 ?? '', 'hex');
// constant-time compare — never === on signatures
return given.length === expected.length && timingSafeEqual(expected, given);
}The signature is HMAC-SHA256 over <timestamp>.<raw body>, formatted t=<ts>,v1=<hmac>. Reject anything older than five minutes, and compare signatures constant-time (as the snippet does) — never with ==. We retry failed deliveries with growing backoff and auto-disable an endpoint that stays down — re-enable it from the dashboard or POST /v1/webhooks/:id/enable. Delivery is at-least-once: dedupe on the event id.
Every error is JSON with a stable reason and a sentence a human can read. Branch on the reason, show the message.
401 API_KEY_INVALID- Missing or unknown key — send Authorization: Bearer ivst_…
401 API_KEY_REVOKED- The key was revoked from the dashboard.
403 API_KEY_SCOPE- The key lacks the needed scope (send / read / events).
403 UPGRADE_REQUIRED- The option needs a bigger plan — same gates as the app.
404 DELIVERY_NOT_FOUND- Not this key’s delivery (ownership is per workspace).
429 RATE_LIMITED- Per-workspace velocity budgets: 120 creates/hour, 600 reads/hour.
429 QUOTA_EXCEEDED- A daily anti-runaway guardrail, not a plan limit — contact us and we raise it. Enterprise ceilings are fully customisable.
Idempotency: hold one idempotencyKey across create retries — an ambiguous timeout can never mint a second delivery. Uploads retry safely per part; re-request URLs if they expire.
- Nothing ships unchecked. Every file is malware-scanned; media is content-checked. No key scope, no plan, no flag bypasses it.
- Our name on every URL. Uploads on
in.ivesentit.com, downloads onfiles.ivesentit.com— allowlist*.ivesentit.comand you're done. - Proof both ways. Senders get a signed receipt of who opened what; every delivery carries a certificate the recipient can verify — the endpoints are public and keyless.
- Your recipients see a delivery, not a link dump — the same branded pages the app sends, on your domain if you have one.