Developers
File transfer inside your workflow.
Create Transfer sessions from your backend and receive signed events as people share files. The sender chooses files in their browser; your servers never handle the file bytes.
API access requires an eligible plan.
How the integration works
1. Backend creates a Transfer
Your backend creates a transfer with an API key.
2. Sender opens hostUrl
You send the person who is sending to the transfer’s
hostUrl, a sending page hosted by Xferio. They choose files there.3. Recipients use shareUrl
Recipients open the share link. Files stream from the sender’s browser to theirs while the sender’s tab stays open.
4. Backend receives events
Your webhook endpoint receives events such as a recipient connecting or a download finishing.
The TypeScript SDK (@xferio/sdk, in this repository under packages/sdk-ts) wraps every step, including webhook verification. It is not yet published to a package registry; any HTTP client works too.
Keys and scopes
Keep keys on the backend
Never expose a key in browser code or client apps.Shown once
Store the key as a secret when it is created; it is not shown again.Existing scopes only
Tick only the scopes the integration needs.
API keys are workspace-scoped bearer keys. Create them under API & webhooks in your workspace. A key is shown once; store it as a secret and use it only on your backend. Never expose it in browser code or client apps.
Authorization: Bearer <your-api-key>Each key works only in its workspace and only for the scopes you tick. A key sees and stops the transfers it created; give it transfers:read_all to see the whole workspace. Rotating a key keeps the old one working for 24 hours.
Create a transfer
Use your API base URL and workspace id.
curl -X POST https://xferio.io/v1/tenants/$WORKSPACE_ID/transfers \
-H "Authorization: Bearer $XFERIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Signed contract", "expiresInHours": 24, "password": "optional"}'The response includes:
transfer.shareUrl: the link recipients open.hostUrl: the sending page. Redirect the sender there and never log it; whoever has it can send as this transfer.hostandsignaling: for advanced integrations that run the sender inside their own page with the SDK.
hostUrl as a secret. Share shareUrl with recipients.import { XferioApi } from '@xferio/sdk';
const xferio = new XferioApi({ baseUrl: 'https://xferio.io', apiKey: process.env.XFERIO_API_KEY });
const created = await xferio.createTransfer(workspaceId, { title: 'Signed contract' });
return Response.redirect(created.hostUrl, 303);Signed webhooks
Verify the raw body
Check the signature before parsing JSON.Check the signature
HMAC-SHA256 with the endpoint’s secret.Deduplicate event IDs
Retries reuse the same id.Browser reports are signals
source: "client" events come from a browser.
Add an HTTPS endpoint under API & webhooks and pick the events you want. Each delivery is a JSON event signed with the endpoint’s secret. Verify X-Xferio-Signature against the raw request body:
X-Xferio-Signature: t=1791547200,v1=5f2b…
# v1 = hex HMAC-SHA256(secret, "<t>.<raw request body>")import { verifyWebhook } from '@xferio/sdk';
const event = await verifyWebhook({
payload: rawBody, // raw bytes, before JSON parsing
signature: req.headers['x-xferio-signature'],
secret: process.env.XFERIO_WEBHOOK_SECRET,
}); // throws if forged, altered or older than 5 minutes
if (await alreadyHandled(event.id)) return; // retries reuse the same idReply with any 2xx within 10 seconds. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.
Common events:transfer.createdpeer.joinedtransfer.completed
Events marked source: "client", such as transfer.completed, were reported by a browser; treat them as a signal, not as an independent verification.
Errors, limits and reference
Rate limits and retries
Each API key may make 300 requests a minute; creating transfers is limited to 60 a minute. After a 429, wait for the time in the message.Reference and SDK
OpenAPI reference. The SDK lives in this repository and is not yet published.
Errors share one shape, and their codes never change meaning:
{ "error": { "code": "QUOTA_EXCEEDED", "message": "…", "requestId": "…" } }| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | The request body or query is invalid; details list each field. |
| 401 | UNAUTHENTICATED | Missing, revoked or expired key or token. |
| 402 | ENTITLEMENT_MISSING | Your plan does not include this feature. |
| 402 | QUOTA_EXCEEDED | A monthly limit or the seat limit is reached. |
| 403 | FORBIDDEN | The key lacks the scope, or the transfer belongs to someone else. |
| 409 | TRANSFER_NOT_JOINABLE | The transfer has ended or expired. |
| 409 | TRANSFER_FULL | Every recipient place on the link is taken. |
| 429 | RATE_LIMITED | Too many requests; wait for the time in the message. |
Each API key may make 300 requests a minute; creating transfers is limited to 60 a minute.
Reference
- OpenAPI 3.1 document: https://xferio.io/v1/openapi.json
- Example service with webhook verification:
examples/backendin the Xferio repository. - In a Direct Transfer, files are encrypted between the browsers by WebRTC (DTLS), also when they pass through the relay. There is no additional application-level encryption, and Xferio cannot scan Direct Transfer files for malware because it never receives them. Xferio Drive stores files and checks them separately; see Security.
Product scope
API keys are workspace credentials for your backend, not a way for people to sign in to an app.
Get started