Developers
API Integrations

Trinsfer Developer Guide

Everything you need to integrate Trinsfer into your stack. The API is REST + JSON, authenticated with a Bearer API key, and respects standard HTTP semantics. All examples below were tested against the live https://trinsfer.com/api/v1 base URL.

Introduction

The Trinsfer API gives you programmatic access to two products:

  • Transfers โ€” create file-transfer envelopes, query metadata, update title/recipient/expiration, soft-delete.
  • Speed tests โ€” submit and retrieve internet-speed test results from your own infrastructure.

All endpoints live under https://trinsfer.com/api/v1 and return JSON with a consistent envelope:

{ "data": { โ€ฆ }, "pagination": { "limit": 20, "offset": 0, "total": 87 } }
{ "error": { "code": "expiry_too_long", "message": "Your 'free' plan allows up to 7 day(s) of retentionโ€ฆ" }, "plan": "free", "max_expiry_days": 7, "upgrade_url": "/pricing" }

Quickstart (60 seconds)

Fire your first request right away:

# Replace tk_xxxxxxxx_โ€ฆ with the key from /profile#api-keys curl https://trinsfer.com/api/v1/account \ -H "Authorization: Bearer tk_xxxxxxxx_โ€ฆ"
const r = await fetch('https://trinsfer.com/api/v1/account', { headers: { 'Authorization': 'Bearer tk_xxxxxxxx_โ€ฆ' } }); const { data } = await r.json(); console.log('plan:', data.plan, 'used:', data.usage.this_month);
import requests r = requests.get( 'https://trinsfer.com/api/v1/account', headers={'Authorization': 'Bearer tk_xxxxxxxx_โ€ฆ'}, ) print(r.json()['data'])
$ch = curl_init('https://trinsfer.com/api/v1/account'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer tk_xxxxxxxx_โ€ฆ'], ]); $data = json_decode(curl_exec($ch), true)['data']; print_r($data);

Authentication

Every request must include the header Authorization: Bearer <your-key>. Keys look like tk_a1b2c3d4_โ€ฆ โ€” the first 11 characters (tk_xxxxxxxx) are the prefix and identify the key. The full secret is stored only as SHA-256 on our side; we never see it again after creation.

Never embed keys in frontend code. Treat them as passwords โ€” store them in environment variables, secret managers, or server-side config.

Listing and rotating keys

From /profile#api-keys you can:

  • Generate new keys with custom scopes.
  • Regenerate a key โ€” old secret stops working immediately, you receive the new one once.
  • Revoke or permanently delete a key.

Error handling

Every non-2xx response carries error.code and error.message. The cheat-sheet below maps codes to HTTP statuses and what you should do.

HTTP
Error code
Action
401
missing_token / invalid_token / revoked
Verify the Bearer header and that the key still exists.
403
insufficient_scope
The key doesn't have the required scope โ€” add it or use another key.
404
not_found
Resource was deleted or the token belongs to another user.
422
expiry_too_long
Reduce expires_days below your plan's max_expiry_days.
429
rate_limited
Wait until the Retry-After seconds and retry. Consider exponential backoff.
402
quota_payment_required
Settle pending overage at pay_url (your /profile#api-keys).
500
server_error
Transient. Retry with backoff; if it persists, contact support.

Rate limit & quota headers

Every response (including errors) carries headers that describe your remaining quota โ€” no need to call /account for that:

  • X-RateLimit-Limit โ€” requests per minute allowed by your plan.
  • X-RateLimit-Remaining โ€” how many you have left in the current minute.
  • X-RateLimit-Reset โ€” seconds until the counter resets.
  • X-Quota-Limit-Month โ€” free requests included this month.
  • X-Quota-Used-Month โ€” how many you have already used.
  • X-Quota-Overage โ€” request count above your free quota.
  • X-Quota-Overage-USD โ€” money owed at current pricing.
  • X-Trinsfer-Plan โ€” your plan: free, monthly, annual.

Scopes & permissions

Scopes restrict what a key can do โ€” assign the minimum required:

  • transfers:read โ€” list and read transfers + files.
  • transfers:write โ€” create, update, delete transfers.
  • speed:read โ€” list speed test history.
  • speed:write โ€” submit and delete speed test results.

Requesting an endpoint without the matching scope returns HTTP 403 insufficient_scope.

Transfer expiration limits

The expires_days field on POST /transfers and PATCH /transfers/<token> is capped by your plan. Exceeding it returns HTTP 422 expiry_too_long.

Plan
Maximum retention
Per-request price after quota
Free
Up to 7 days
$0.0010
Pro Monthly
Up to 30 days
$0.0005
Pro Annual
Up to 395 days
$0.0002
If a Pro subscription expires, future transfers fall back to the Free 7-day cap. Existing transfers keep their original expiration date.

Pay-per-use billing

Each plan ships with a generous free monthly request quota. Past that, every request adds a tiny pay-per-use charge that you can settle on demand from /profile#api-keys.

  • Overage is computed monthly and reset every 1st of the month.
  • While you owe less than $20, the API keeps responding normally โ€” you only see the bill grow.
  • Above $20 unpaid, the API returns HTTP 402 quota_payment_required with a pay_url that opens PayPal at the exact owed amount.

Account

GET/api/v1/account

Returns the current user, active plan, rate-limit ceiling, monthly quota and current overage. Useful as a health-check / ping endpoint for your SDK.

{ "data": { "user": { "id": 1, "name": "Tester", "email": "u@example.com" }, "plan": "free", "limits": { "requests_per_minute": 60, "requests_per_month": 5000, "max_transfer_expiry_days": 7, "max_transfer_size_gb": 2 }, "usage": { "this_month": 217, "last_24_hours": 12 }, "overage": { "requests": 0, "usd": 0, "price_per_req": 0.001 } } }

Transfers

GET/api/v1/transfersscope: transfers:read

List your transfers, newest first. Supports ?limit=<1-100> (default 20) and ?offset=<0+>.

POST/api/v1/transfersscope: transfers:write

Create a transfer envelope. Returns token + upload_url โ€” the upload itself uses TUS resumable chunks (see "Recipes" below).

curl -X POST https://trinsfer.com/api/v1/transfers \ -H "Authorization: Bearer tk_xxxxxxxx_โ€ฆ" \ -H "Content-Type: application/json" \ -d '{ "title": "Q4 report", "expires_days": 7, "recipient_email": "cfo@example.com", "message": "Quarterly numbers" }'
const r = await fetch('https://trinsfer.com/api/v1/transfers', { method: 'POST', headers: { 'Authorization': 'Bearer tk_xxxxxxxx_โ€ฆ', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Q4 report', expires_days: 7, recipient_email: 'cfo@example.com' }) }); const { data } = await r.json(); console.log(data.upload_url, data.download_url);
import requests r = requests.post( 'https://trinsfer.com/api/v1/transfers', headers={'Authorization': 'Bearer tk_xxxxxxxx_โ€ฆ'}, json={ 'title': 'Q4 report', 'expires_days': 7, 'recipient_email':'cfo@example.com', }, ) print(r.json()['data']['token'])
GET/api/v1/transfers/<token>scope: transfers:read

Retrieve a single transfer with its file list and signed per-file download URLs.

PATCH/api/v1/transfers/<token>scope: transfers:write

Updatable fields: title, recipient_email, message, expires_days (capped by your plan).

DELETE/api/v1/transfers/<token>scope: transfers:write

Soft-deletes the transfer. Files are purged after the grace period.

Speed tests

GET/api/v1/speed-testsscope: speed:read

List your stored speed test history. Same pagination as transfers.

POST/api/v1/speed-testsscope: speed:write
curl -X POST https://trinsfer.com/api/v1/speed-tests \ -H "Authorization: Bearer tk_xxxxxxxx_โ€ฆ" \ -H "Content-Type: application/json" \ -d '{ "ping_ms": 12.5, "jitter_ms": 1.8, "download_mbps": 95.4, "upload_mbps": 48.3, "public": true, "display_name": "my-edge-node", "isp": "Acme ISP", "country": "US", "location": "New York" }'
GET/api/v1/speed-tests/<id>scope: speed:read
DELETE/api/v1/speed-tests/<id>scope: speed:write

Recipe: upload a file with chunks

Trinsfer uses its own lightweight chunk-upload endpoint at POST /upload_chunk.php. Each chunk is a raw binary POST; metadata travels in X-* headers. When the last chunk arrives the server auto-assembles the final file โ€” no separate "finalize" call needed.

Protocol

  1. Create the transfer envelope with POST /api/v1/transfers and keep the returned token.
  2. Pick a chunk size (5โ€“10 MB works great) and split the file.
  3. For each chunk, POST /upload_chunk.php with the raw bytes as the body and these headers:
    • X-Token โ€” the transfer token.
    • X-File-Id โ€” a stable identifier you generate per file (UUID).
    • X-Chunk-Index โ€” zero-based chunk number.
    • X-Total-Chunks โ€” how many chunks the file will be.
    • X-File-Name โ€” original file name.
    • X-File-Size โ€” total file size in bytes.
    • Content-Type: application/octet-stream.
  4. The transfer flips from uploading โ†’ ready automatically as soon as the last chunk completes.
Chunks are idempotent โ€” re-POST the same X-Chunk-Index to retry a failed upload, the server overwrites that chunk and recounts.
# Send chunk 0 of 4 (file already split into ./part0 โ€ฆ ./part3) curl -X POST https://trinsfer.com/upload_chunk.php \ -H "X-Token: dta1c9ir" \ -H "X-File-Id: 11111111-2222-3333-4444-555555555555" \ -H "X-Chunk-Index: 0" \ -H "X-Total-Chunks: 4" \ -H "X-File-Name: backup.zip" \ -H "X-File-Size: 52428800" \ -H "Content-Type: application/octet-stream" \ --data-binary @./part0
// Step 1 โ€” create the envelope const env = await fetch('https://trinsfer.com/api/v1/transfers', { method: 'POST', headers: { 'Authorization': 'Bearer tk_โ€ฆ', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'backup.zip', expires_days: 7 }) }).then(r => r.json()); // Step 2 โ€” chunk and upload const file = document.querySelector('input[type=file]').files[0]; const CHUNK_SIZE = 5 * 1024 * 1024; // 5 MB const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const fileId = crypto.randomUUID(); for (let i = 0; i < totalChunks; i++) { const blob = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE); await fetch('https://trinsfer.com/upload_chunk.php', { method: 'POST', headers: { 'X-Token': env.data.token, 'X-File-Id': fileId, 'X-Chunk-Index': i, 'X-Total-Chunks': totalChunks, 'X-File-Name': file.name, 'X-File-Size': file.size, 'Content-Type': 'application/octet-stream', }, body: blob, }); } console.log('done โ†’', env.data.download_url);
import os, uuid, requests API = 'https://trinsfer.com' KEY = 'tk_xxxxxxxx_โ€ฆ' FILE = './backup.zip' CHUNK = 5 * 1024 * 1024 # 1. envelope env = requests.post(f"{API}/api/v1/transfers", headers={'Authorization': f'Bearer {KEY}'}, json={'title': os.path.basename(FILE), 'expires_days': 7}, ).json()['data'] # 2. chunks size = os.path.getsize(FILE) total = (size + CHUNK - 1) // CHUNK fid = str(uuid.uuid4()) with open(FILE, 'rb') as fp: for i in range(total): requests.post(f"{API}/upload_chunk.php", headers={ 'X-Token': env['token'], 'X-File-Id': fid, 'X-Chunk-Index': str(i), 'X-Total-Chunks': str(total), 'X-File-Name': os.path.basename(FILE), 'X-File-Size': str(size), 'Content-Type': 'application/octet-stream', }, data=fp.read(CHUNK), ) print('done โ†’', env['download_url'])
// PHP 8+ $api = 'https://trinsfer.com'; $key = 'tk_xxxxxxxx_โ€ฆ'; $file = './backup.zip'; // 1. envelope $ch = curl_init("$api/api/v1/transfers"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['title' => basename($file), 'expires_days' => 7]), CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", 'Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, ]); $env = json_decode(curl_exec($ch), true)['data']; curl_close($ch); // 2. chunks $chunkSize = 5 * 1024 * 1024; $size = filesize($file); $total = (int) ceil($size / $chunkSize); $fid = bin2hex(random_bytes(16)); $fp = fopen($file, 'rb'); for ($i = 0; $i < $total; $i++) { $chunk = fread($fp, $chunkSize); $ch = curl_init("$api/upload_chunk.php"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $chunk, CURLOPT_HTTPHEADER => [ "X-Token: {$env['token']}", "X-File-Id: $fid", "X-Chunk-Index: $i", "X-Total-Chunks: $total", 'X-File-Name: ' . basename($file), "X-File-Size: $size", 'Content-Type: application/octet-stream', ], CURLOPT_RETURNTRANSFER => true, ]); curl_exec($ch); curl_close($ch); } fclose($fp); echo "done โ†’ {$env['download_url']}\n";

Webhooks

Subscribe to events that happen on your account from /profile#webhooks. Every delivery includes:

  • X-Trinsfer-Event โ€” event name.
  • X-Trinsfer-Delivery โ€” unique delivery id (for idempotency).
  • X-Trinsfer-Signature โ€” sha256=<hex> of the body using your webhook secret.

Events emitted

  • transfer.created โ€” fires when a transfer is created via the API or web app.
  • transfer.downloaded โ€” fires every time someone downloads from a transfer link.
  • transfer.expired โ€” fires when a transfer crosses its expiration date.
  • transfer.password_failed โ€” fires on a wrong password attempt.
  • speed_test.submitted โ€” fires when a speed test is stored.
  • api_key.used โ€” fires when an API key is used to authenticate a request (throttled to at most once per 60s per key).

Verify a delivery (PHP)

$secret = getenv('TRINSFER_WEBHOOK_SECRET'); $body = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_TRINSFER_SIGNATURE'] ?? ''; $expect = 'sha256=' . hash_hmac('sha256', $body, $secret); if (!hash_equals($expect, $sig)) { http_response_code(401); exit; } $payload = json_decode($body, true); // $payload->event, $payload->data, $payload->timestamp โ€ฆ

Libraries

Until we ship official SDKs you can use any HTTP client. We've validated the API works with:

  • JavaScript โ€” native fetch, axios, ky.
  • Python โ€” requests, httpx.
  • PHP โ€” Guzzle, Symfony HttpClient, raw cURL.
  • Go โ€” net/http with http.Header{Authorization: "Bearer โ€ฆ"}.
  • Postman โ€” import any cURL above with "Import โ†’ Raw text".

Want to publish a community SDK? Open a PR or email dev@trinsfer.com.