Enterprise pack
Programmatic API
Mint unique, branded upload links from your own server. Each upload is forwarded directly into your storage and announced via HMAC-signed webhook — agnostic to images, video, PDFs, or raw binary.
Overview
The programmatic API is a server-to-server surface for enterprise integrators — event platforms collecting photo uploads, legal firms ingesting PDFs, customer-portal vendors embedding upload links inside their own product. You call POST /api/v1/links with your custom metadata and branding; we hand back a public uploadUrlfor your end-user. When the file lands, it's piped straight into your S3 / R2 bucket and we POST your webhook with the same metadata you tagged the link with at creation time.
Base URL
https://fileaway.io/api/v1Response envelope
All responses follow the same wrapper: a top-level success flag, the payload under data, and a meta object with requestId for support traceability.
{
"success": true,
"data": { ... },
"meta": {
"timestamp": "2026-04-29T12:00:00.000Z",
"requestId": "req-7c3f...",
"version": "v1"
}
}Subscription gating
API access is part of the Business pack. Every /api/v1/* request re-checks entitlement at request time, so cancelling the pack revokes access on the next call without any key rotation.
| Quota | Effect |
|---|---|
| apiAccess | Required to call any /api/v1/* route |
| maxApiKeys | Cap on concurrent active keys |
| maxLinks | Cap on total links per account |
| apiRateLimitPerMinute | Sliding-window cap, per key |
| webhooksEnabled | Required to attach a webhook to a link |
| brandingEnabled | Required to attach branding to a link |
Issuing API keys
Issue keys from the dashboard or via the management endpoint below. The plaintext key is returned once in data.plaintextKey — store it immediately. After that only the public keyPrefix is retrievable.
curl -X POST https://fileaway.io/api/management/api-keys \
-H "Authorization: Bearer <your JWT>" \
-H "Content-Type: application/json" \
-d '{
"label": "Production server",
"scopes": ["links:read", "links:write", "uploads:read", "webhooks:write"]
}'{
"success": true,
"data": {
"apiKey": {
"_id": "65f0...",
"label": "Production server",
"keyPrefix": "fa_live_a1b2c3d4",
"scopes": ["links:read", "links:write", "uploads:read", "webhooks:write"],
"requestCount": 0,
"createdAt": "2026-04-29T12:00:00.000Z"
},
"plaintextKey": "fa_live_a1b2c3d4_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
}
}Available scopes: links:read, links:write, uploads:read, webhooks:write. Omit scopes to grant all four.
Revoke a key
Authentication
Send the plaintext key in the X-API-Keyheader. Bearer-token form also works for compatibility with HTTP clients that don't support custom headers.
X-API-Key: fa_live_a1b2c3d4_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0Key format
fa_<env>_<prefix>_<secret> where env is live or test, the prefix is 8 hex chars (safe to display), and the secret is 32 hex chars (only the SHA-256 is persisted on our side).
Create a branded link
The response includes uploadUrl — the public link to hand to your end-user. Anything you put in metadatais echoed back in every webhook payload so you don't need a database round-trip to reattribute uploads.
curl -X POST https://fileaway.io/api/v1/links \
-H "X-API-Key: fa_live_..." \
-H "Content-Type: application/json" \
-d '{
"integrationId": "65f0a1b2c3d4e5f6a7b8c9d0",
"label": "Acme Conf 2026 — Day 1 photo wall",
"allowedMimeTypes": ["image/jpeg", "image/png", "image/webp"],
"maxFileSizeBytes": 26214400,
"metadata": {
"eventId": "evt_2026_acme_d1",
"customerId": "cust_acme"
},
"branding": {
"title": "Upload your shots",
"primaryColor": "#0F172A",
"logoUrl": "https://acme.com/logo.png"
},
"webhook": {
"url": "https://acme.com/hooks/fileaway",
"secret": "whsec_xxxxxxxxxxxxxxxx",
"events": ["upload.created"]
}
}'curl -X POST https://fileaway.io/api/v1/links \
-H "X-API-Key: fa_live_..." \
-H "Content-Type: application/json" \
-d '{
"integrationId": "65f0a1b2c3d4e5f6a7b8c9d0",
"label": "Smith v. Jones — discovery intake",
"allowedMimeTypes": ["application/pdf"],
"maxFileSizeBytes": 104857600,
"password": "Verbal-Cipher-9",
"metadata": {
"caseNumber": "2026-CV-04412",
"clientId": "cli_smith"
},
"webhook": {
"url": "https://firm.example/api/intake",
"secret": "whsec_xxxxxxxxxxxxxxxx"
}
}'{
"success": true,
"data": {
"_id": "65f0c1...",
"slug": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"label": "Acme Conf 2026 — Day 1 photo wall",
"uploadUrl": "https://fileaway.io/u/a1b2c3d4...",
"infoUrl": "https://fileaway.io/api/u/a1b2c3d4...",
"active": true,
"uploadCount": 0,
"metadata": { "eventId": "evt_2026_acme_d1", "customerId": "cust_acme" },
"source": "API",
"createdAt": "2026-04-29T12:00:00.000Z"
}
}Required fields
integrationId(S3 or R2 destination you've already set up in the dashboard) and label. Everything else is optional.
Manage links
Paginated list of every link your account owns. Query params: page, limit (1–100, default 20), isActive, search (matches label), integrationId.
Pass null for any nullable field (e.g. "webhook": null) to clear it.
Flips the active flag without changing other fields.
Listing uploads
Every file that came through the link, newest first. Returns metadata only — file bytes are never stored on our side, they live in your bucket under storageKey.
{
"success": true,
"data": [
{
"_id": "65f0c2...",
"originalFilename": "IMG_8423.jpg",
"storedFilename": "IMG_8423.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 4823100,
"storageKey": "links/a1b2c3d4.../9f7e_IMG_8423.jpg",
"destinationType": "S3",
"createdAt": "2026-04-29T12:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1, "hasNext": false, "hasPrev": false }
}Webhooks
When a file lands on a link with a configured webhook, we POST a JSON envelope to that URL. The signing scheme follows Stripe / GitHub conventions: HMAC-SHA256 over {timestamp}.{rawBody} with the secret you supplied at link-creation time.
Headers
X-Webhook-Id: a4d2e8f0-...
X-Webhook-Event: upload.created
X-Webhook-Timestamp: 1709300000
X-Webhook-Signature: 8d0f1a3c4b...Payload
{
"id": "a4d2e8f0-...",
"event": "upload.created",
"deliveredAt": "2026-04-29T12:00:00.000Z",
"linkSlug": "a1b2c3d4...",
"linkId": "65f0c1...",
"metadata": { "eventId": "evt_2026_acme_d1", "customerId": "cust_acme" },
"data": {
"uploadId": "65f0c2...",
"originalFilename": "IMG_8423.jpg",
"storedFilename": "IMG_8423.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 4823100,
"storageKey": "links/a1b2c3d4.../9f7e_IMG_8423.jpg",
"destinationType": "S3",
"uploadedAt": "2026-04-29T12:00:00.000Z"
}
}Verifying the signature (Node.js)
import crypto from "node:crypto";
export function verifyWebhook(req, secret) {
const sig = req.headers["x-webhook-signature"];
const ts = req.headers["x-webhook-timestamp"];
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex")
);
}Retries
Failed deliveries are retried up to 3 times with exponential backoff (1s → 4s → 16s) and a 10s per-call timeout. Persistent failures are stored and visible in the dashboard's deliveries view.
Events
upload.created— file accepted and proxied to your bucket.upload.failed— upload was rejected (bad MIME, oversize, password failure).link.limit_reached— the link'smaxUploadsceiling was hit.
Rate limits
Per-key sliding-window throttle. The cap is read from your pack's apiRateLimitPerMinute at request time, so an upgrade lifts the ceiling on the very next call. Every response carries:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1709300060Hitting the cap returns 429 RATE_LIMITED; respect X-RateLimit-Reset (unix seconds) before retrying.
Error codes
Errors use the same envelope as success responses but with success: false and a top-level error object containing code, message, and optional details.
| Code | HTTP | Meaning |
|---|---|---|
| API_KEY_MISSING | 401 | No X-API-Key / Authorization header |
| API_KEY_INVALID | 401 | Key not found or wrong secret |
| API_KEY_REVOKED | 401 | Key has been revoked |
| API_KEY_EXPIRED | 401 | Key is past its expiresAt |
| API_KEY_INSUFFICIENT_SCOPE | 403 | Key lacks the scope this route requires |
| API_ACCESS_NOT_SUBSCRIBED | 403 | Owner has no pack with apiAccess |
| FEATURE_NOT_AVAILABLE | 403 | Pack does not enable the requested feature |
| QUOTA_EXCEEDED | 403 | Pack quota would be exceeded |
| VALIDATION_ERROR | 400 | Request body failed schema validation |
| NOT_FOUND | 404 | Link or upload doesn't exist |
| RATE_LIMITED | 429 | Per-key rate limit hit; respect X-RateLimit-Reset |
Ready to integrate?
Subscribe to the Business pack and issue your first key from the dashboard.