Skip to main content
VaultFerry
← Home

API reference

The VaultFerry public REST API, version 1. Create an API key under Settings → API keys to get started; the sections below cover authentication, conventions, every endpoint with examples, errors, and rate limits.

Overview

The VaultFerry API lets you create and manage share links and upload links (file requests) from your own tools and automation platforms such as Zapier. The API is available on the Business plan (monthly billing — cancel anytime).

All requests use the base URL below. Breaking changes will only ever ship as a new base path (/v2); within v1, changes are additive.

https://api.vaultferry.com/api/public/v1

Your file bytes transfer directly from your storage to your recipient and never pass through VaultFerry or your automation tool. Link metadata you route through an automation (filenames, labels, expiry) does pass through VaultFerry and that tool.

Authentication

Every request is authenticated with an API key sent in the X-Api-Key header. Create keys in VaultFerry under Settings → API keys (Business plan). API keys are shown once at creation and cannot be retrieved afterward — store yours securely. We store a one-way hash of your key, never the key itself.

A key grants read access to your live link inventory (including shareable URLs) and create/revoke access to links on your connected storage — store it like a password. Revoke a key any time in Settings; revoked keys stop working immediately and are erased after 30 days. Keys look like vf_live_…; treat the full value as an opaque secret.

curl -H "X-Api-Key: $VAULTFERRY_API_KEY" \
  https://api.vaultferry.com/api/public/v1/me

A missing, unknown, or revoked key receives the same response: a 401 with body {"code":"invalid_api_key","message":"…"} and no WWW-Authenticate header. Use GET /me as your connection test.

Conventions

The API speaks JSON only: send Content-Type: application/json on writes (anything else is a 415) and accept application/json responses (a 406 otherwise). Timestamps are ISO 8601 in UTC, e.g. 2026-07-16T12:00:00Z.

Connection ids are UUIDs. Share-link and upload-link ids are 22-character URL-safe strings — treat them as opaque. If you must validate an id shape, accept up to 32 characters of A–Z a–z 0–9 - _rather than pinning today's length.

Statuses are lowercase and differ per resource: share links are active | expired | revoked | exhausted (download cap reached); upload links are active | expired | revoked | filled (file cap reached). New fields and new enum values may appear within v1 — write integrations that tolerate unknowns.

Every field is always present in a response; a value that does not apply is serialised as an explicit null:

Nullable fields and their meaning
Fieldnull means
fileSizeBytesyour storage provider did not report a size for the file
maxDownloadsthe share link has no download cap
maxFilesthe upload link has no file-count cap
connectionIdthe connection this link belonged to was deleted — deleting a connection also revoked its links
nextCursoryou are on the last page of a list

Pagination

List endpoints are cursor-paginated, newest first. limitdefaults to 25 (maximum 100); the response's nextCursorfeeds the next request's cursor parameter and is null on the last page.

Cursors are opaque tokens: never construct or edit a cursor, and don't store them long-term. A tampered or expired cursor returns 400 invalid_cursor — restart from the first page.

curl -H "X-Api-Key: $VAULTFERRY_API_KEY" \
  "https://api.vaultferry.com/api/public/v1/share-links?limit=25&cursor=<nextCursor>"

Endpoints

Me

GET /me

Identifies the account a key belongs to — the auth test.

{ "email": "owner@example.com", "plan": "business" }

Connections

GET /connections?cursor=&limit=

Lists your storage connections for picking a connectionId. Only connections with supportsUploadRequests: true can receive upload links.

{
  "items": [
    {
      "id": "3f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
      "displayName": "Acme production bucket",
      "supportsUploadRequests": true
    }
  ],
  "nextCursor": null
}

Share links

GET /share-links?cursor=&limit=&status=&q=
GET /share-links/{id}
POST /share-links
DELETE /share-links/{id}

The list's q filters by a case-insensitive substring of the file name; status filters by one lowercase status value. GET /share-links/{id} reads one link back; unknown ids return 404 not_found. DELETE /share-links/{id} revokes immediately and returns 204.

Create a share link:

curl -X POST \
  -H "X-Api-Key: $VAULTFERRY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "3f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
    "filePath": "clients/acme/report.pdf",
    "expiresInHours": 72,
    "password": "optional-secret",
    "maxDownloads": 5
  }' \
  https://api.vaultferry.com/api/public/v1/share-links

Returns 201 with a Locationheader pointing at the API read-back resource; the body's url is the public download page your recipient uses:

HTTP/1.1 201 Created
Location: https://api.vaultferry.com/api/public/v1/share-links/Ab3dEf6hIj9kLm2nOp5qRs

{
  "id": "Ab3dEf6hIj9kLm2nOp5qRs",
  "url": "https://vaultferry.com/share/Ab3dEf6hIj9kLm2nOp5qRs",
  "fileName": "report.pdf",
  "filePath": "clients/acme/report.pdf",
  "fileSizeBytes": 4194304,
  "status": "active",
  "expiresAt": "2026-08-01T12:00:00Z",
  "downloadCount": 0,
  "maxDownloads": 5,
  "passwordProtected": true,
  "connectionId": "3f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "createdAt": "2026-07-16T12:00:00Z"
}

filePath is the connection-relative path including the file name. expiresInHours takes 1–8760: a value outside 1–8760 returns 400 validation_error, while a value exceeding your plan's expiry cap returns 402 plan_limit with a message naming the bound (the same applies to POST /upload-links). password (optional; at least 8 characters and at most 72 bytes) and maxDownloads are optional.

Upload links (file requests)

GET /upload-links?cursor=&limit=&status=&q=
GET /upload-links/{id}
POST /upload-links
DELETE /upload-links/{id}

The list's q matches the link label only. Create accepts connectionId (required), destinationPath (required), label (optional, ≤ 200 characters — blank derives it from the destination folder), expiresInHours, password, maxFiles, maxBytesPerFile, and requireUploaderIdentity (boolean). Creating against a connection whose supportsUploadRequests is false returns 422 provider_not_supported.

HTTP/1.1 201 Created
Location: https://api.vaultferry.com/api/public/v1/upload-links/Zx8wVu5tSr2qPo9nMl6kJi

{
  "id": "Zx8wVu5tSr2qPo9nMl6kJi",
  "url": "https://vaultferry.com/upload/Zx8wVu5tSr2qPo9nMl6kJi",
  "label": "Acme onboarding documents",
  "destinationPath": "clients/acme/incoming",
  "status": "active",
  "expiresAt": "2026-08-01T12:00:00Z",
  "maxFiles": 10,
  "maxBytesPerFile": 1073741824,
  "receivedFileCount": 0,
  "requireUploaderIdentity": false,
  "passwordProtected": false,
  "connectionId": "3f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "createdAt": "2026-07-16T12:00:00Z"
}

Errors

Every failure returns a JSON envelope {"code": "…", "message": "…"}. The message is human-readable and safe to surface. A 402 adds an upgrade field; validation_error adds violations[] with per-field messages; daily_quota_exceeded adds retryAfterSeconds.

Error codes
codeHTTPMeaning
invalid_api_key401Missing, unknown, or revoked key.
plan_limit402The account is not on the Business plan, or the request exceeds a plan bound (e.g. the link-expiry cap) — see message.
storage_permission_denied403Your storage credentials rejected the request — check the connection in VaultFerry.
not_found404Unknown route, unknown link id, an unknown connectionId, or a filePath that does not resolve to a file on the connection.
malformed_json400The body is not valid JSON for the target shape.
validation_error400A field failed validation — see violations[].
bad_request400A malformed query parameter (e.g. a non-numeric limit, an unknown status), or a body value that breaks a domain rule (e.g. a password shorter than 8 characters or longer than 72 bytes, or a non-blank label that has no usable characters once slashes and control characters are stripped) — see message.
invalid_cursor400A tampered or expired pagination cursor — restart from the first page.
method_not_allowed405Wrong HTTP method for the route.
not_acceptable406The Accept header cannot be satisfied — responses are application/json only.
unsupported_media_type415The request Content-Type is not application/json.
provider_not_supported422Upload-link create on a connection without upload-request support.
rate_limited429A per-minute ceiling was hit — Retry-After says when to retry.
daily_quota_exceeded429The rolling 24-hour create quota was hit — Retry-After can be hours.
storage_unavailable502Your storage provider could not be reached.
storage_error502An unexpected storage-side error.

Rate limits and quotas

Per key: 60 read requests per minute (GET — a HEAD counts as a read) and 10 mutating requests per minute (POST and DELETE share this budget). Per account, across all keys: 120 requests per minute. All three per-minute ceilings are applied best-effort (they reset if the service restarts); the durable, contractual bound is the daily create quota below. Exceeding a ceiling returns 429 rate_limited with a Retry-After header — wait that long and retry.

The durable, contractual bound is the create quota: 1,000 link creations per rolling 24-hour window (share and upload links combined). Hitting it returns 429 daily_quota_exceeded with Retry-After set to when the oldest counted create ages out — potentially hours. If you run bulk imports or large backfills, spread creates across the day and honour Retry-After instead of hammering the endpoint; automation platforms like Zapier do this automatically.

Link lifecycle and retention

Links leave the active state by expiring, being revoked, or reaching their cap (exhausted for share links, filled for upload links). Terminal links remain readable in the API for 30 days after entering a terminal state and are then permanently deleted — if an id you stored starts returning 404, the link aged out; nothing deleted it on your behalf.

Retries and duplicates

The two create endpoints are not idempotent: a create that is retried after a timeout can mint a second live link. If your workflow retries, list with q to find the duplicate and DELETE the extra one. Reads and revokes are safe to retry (revoking an already-revoked link returns 204 again).

Server-side use only

Call the API from server-side code or an automation platform — never from a browser. Cross-origin browser calls are blocked by design: the X-Api-Key header is never allowed cross-origin, so browser requests fail the CORS preflight — and embedding a key in client-side code would hand your link inventory to anyone reading the page source.