ive sent it
Developers · beta

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.

AUTHENTICATION
Bearer ivst_live_… · ivst_test_…

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.

CONCEPTS
the six ideas that carry everything

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.

QUICKSTART
four calls · one delivery
01

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.

02

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.

03

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.

04

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.

DELIVERY OPTIONS
every block, one create call

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

Pro

Recipients 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

Pro

Name 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

Pro

Set 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

Pro

The 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

Pro

The 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

Team

Every 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

Team

The 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

Pro

Attach 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:

optiontypewhat it addsplan
recipientsstring[]Email addresses or @handles. Omit with linkOnly: true to share the link yourself. Ignored on a test key (delivery goes to your own email).
messagestringA note that rides with the delivery.
passwordstringRecipients must enter it before anything opens.Pro
allowedCountriesstring[]GeoWall — two-letter codes; the delivery refuses collection everywhere else.Pro
downloadCapintegerTotal downloads allowed before the delivery seals itself.Pro
viewOncebooleanBurns after first collection.Pro
releaseAtTimeunix secondsScheduled release — files stay sealed until the embargo lifts.Pro
retentionSecondsintegerHow long the delivery lives (clamped to your plan's maximum).
region"eu" | "us"Where the files physically live.Team for us
watermark (+ watermarkLabel)booleanPer-recipient watermarking — every recipient gets their own marked copy.Team
idVerify (+ idVerifyConfirm)booleanID-verified delivery — files unlock after an identity check; confirm mode lets the sender release each verified person by hand.Team
priceCents (+ priceCurrency)integerPay to Download — files unlock on card payment straight to your Stripe account. 0% commission.Pro
notifyOnOpenbooleanEmail the sender on first open.
idempotencyKeystringHold one key across retries — an ambiguous timeout can never create a second delivery.
ENDPOINTS
12 routes
POST/v1/deliveriesAPI key · send

Create 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.

GET/v1/deliveries/:idAPI key · read

The delivery now: status (draft → scanning → ready), per-file scan verdicts, per-recipient opens and downloads, the delivery certificate id, testMode.

POST/v1/transfers/:id/files/:fileId/partsX-Transfer-Token

Multipart part URLs, up to 1,000 per request — page through for terabyte files.

POST/v1/transfers/:id/files/:fileId/completeX-Transfer-Token

Mark a file uploaded (multipart: include the part ETags).

POST/v1/transfers/:id/finalizeX-Transfer-Token

All files in — start scanning. Everything after is automatic.

GET/v1/eventsAPI key · events

Your workspace’s event stream: delivery.created, .sent, .opened, .downloaded, .receipt_issued. Cursor-paged with after=<id>; limit ≤ 100.

POST/v1/webhooksAPI key · events

Register an https URL to be POSTed each new event, signed. Returns a signing secret (shown once). Prefer this over polling for a permanent integration.

GET/v1/webhooksAPI key · events

List your endpoints with status, failure count and last response code. Never returns the secret.

DELETE/v1/webhooks/:idAPI key · events

Remove an endpoint.

GET/v1/certificates/:idnone

Verify a delivery certificate — the recipient-side proof of origin. The full SHA-256 unlocks the file list and signed PDF/JSON.

GET/v1/receipts/:idnone

Verify a proof-of-delivery receipt against the public register.

GET/v1/receipts/keysnone

The public signing key — verify receipts and certificates entirely offline.

WEBHOOKS
push, don’t poll

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.

ERRORS & LIMITS
stable reasons, human messages

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.

THE GUARANTEES
same engine as the app
  • 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 on files.ivesentit.com — allowlist *.ivesentit.com and 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.