Create is not send
POST /transfers always creates a draft. The link is not live and nobody is emailed until you call finalize. That is the only honest ordering: the alternative is either mailing a link to files that have not arrived, or holding a request open for the length of an upload.
A draft you never finalize is swept after 24 hours, so an abandoned script leaves nothing behind.
Endpoints
POST /transfers
Registers files and returns upload targets. title is for your dashboard and is never shown to recipients.
Transfers live 7 days unless you say otherwise. Pass expiresInDays to set a different lifetime — it is clamped to your plan's maximum (7 free, 90 paid) rather than rejected.
POST /transfers/{id}/finalize
Makes the link live and emails recipients. Omit them for a link-only transfer you share yourself — that is the default, and it spends none of your email budget.
Safe to retry: finalizing twice does not re-send, and recipients already notified are not notified again. If a response is lost, repeat the call.
GET /transfers/{id}
The transfer, its ready files with scan verdicts, and per-recipient status.
GET /transfers/{id}/downloads
Download receipts. See the note about counting below.
POST /transfers/{id}/files
Add more files, before finalizing. Once a transfer is sent its contents are fixed and this returns 410 not_editable — a delivery that changes shape after the recipient was told it was ready is a bad surprise for them and an invisible one for you.
DELETE /transfers/{id}/files/{fileId}
Remove one, also only before finalizing.
PATCH /transfers/{id}
Change expiresInDays or maxDownloads. Expiry is clamped to your plan's maximum rather than rejected, so asking for 365 gets you the longest you may have.
DELETE /transfers/{id}
Revoke. Immediate and irreversible — the per-file encryption keys are destroyed before the response returns, so the content is unrecoverable whether or not object storage has caught up. Links already sent stop working.
Counting downloads
sessionId. Count distinct sessions, or a single visitor looks like eight.totalDownloads on the response is the authoritative count and is what maxDownloads is enforced against. The event list is capped at the 200 most recent.
Full example
const auth = { Authorization: `Bearer ${process.env.YUNGLE_API_KEY}`,
'Content-Type': 'application/json' };
const created = await fetch('https://yungle.co/api/v1/transfers', {
method: 'POST',
headers: auth,
body: JSON.stringify({
title: 'Rushes — Tuesday',
files: [{ name: 'a001.mov', size: 4_200_000_000, path: 'Day 1' }],
}),
}).then((r) => r.json());
// …stream to created.tusEndpoint using created.files[0].uploadToken…
const sent = await fetch(
`https://yungle.co/api/v1/transfers/${created.transfer.id}/finalize`,
{
method: 'POST',
headers: auth,
body: JSON.stringify({ recipients: ['editor@example.com'] }),
},
).then((r) => r.json());
console.log(sent.transfer.url); // give this to anyone elseSize limits
One transfer may total up to your plan's storage quota. Transfer bytes are not metered against collection storage — the quota is used here as a per-transfer ceiling, not a balance you spend. Exceeding it returns 413 transfer_too_large with the limit in the response, before anything is uploaded.
Recipients are capped at 10 per transfer. See Rate & email limits.