API documentation
Create and manage syntransfer.ro transfers from your own applications, scripts and servers. Version 1.
Overview
- Base URL:
https://www.syntransfer.ro/api/v1(always use thewwwaddress; other addresses redirect and the request body is lost). - Available on the Business and Corporate plans, including team members (each member uses their own keys).
- Requests and responses are JSON (
Content-Type: application/json), except chunk uploads, which send the raw bytes. - Sizes are in bytes, dates are ISO 8601 (for example
2026-11-13T18:33:40+02:00). - Your plan limits (size per transfer, transfers and volume per month, expiry) are the same as on the website.
Authentication
Create a key in Account → API (up to 5 keys per account). The full key is shown only once; we store only its hash. Send it with every request:
Authorization: Bearer syt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx If your HTTP client cannot set the Authorization header, use X-API-Key: syt_... instead.
- Allowed IPs. When you create a key you can restrict it to a list of IP addresses or networks (IPv4 and IPv6, for example
203.0.113.5, 198.51.100.0/24, up to 20 entries). Requests from other addresses get403 ip_not_allowed. - Revoking. A revoked key stops working immediately.
- Read-only accounts. If the account is suspended or the subscription has ended, only
GETrequests work (403 account_read_onlyfor the rest).
Limits
| Limit | Value |
|---|---|
| Requests | 60 per minute per key |
| Chunk uploads | 600 per minute per key (counted separately) |
| Invalid keys | more than 30 requests with an invalid key in one minute block that IP for 15 minutes (429 ip_blocked) |
| Files per transfer | 100 |
| Chunk size | up to 64 MB (8 MB recommended) |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). A 429 response also has Retry-After.
Errors
Errors use the HTTP status code and a JSON body. code is stable and meant for programs; message is for people.
{"error": {"code": "transfer_not_found", "message": "Transfer not found."}} | Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not a JSON object. |
| 400 | invalid_chunk | The chunk body does not match Content-Length (retry). |
| 401 | unauthorized | Missing, invalid or revoked API key. |
| 403 | plan_required | The account is not on Business or Corporate. |
| 403 | ip_not_allowed | The key is restricted to other IP addresses. |
| 403 | account_read_only | Suspended account or ended subscription. |
| 403 | feature_not_in_plan | An option (password, custom expiry, ...) is not included in your plan. |
| 404 | transfer_not_found, file_not_found, not_found | Unknown transfer, file index or endpoint. |
| 409 | transfer_not_pending | The transfer is already finalized. |
| 409 | upload_incomplete | Finalize was called before every chunk arrived (the response lists the files). |
| 411 | length_required | Chunk upload without Content-Length. |
| 413 | transfer_too_large | The transfer exceeds the plan size limit. |
| 413 | chunk_too_large, upload_exceeds_declared_size | The chunk is over 64 MB, or the data exceeds the declared sizes. |
| 422 | invalid_parameter | A request field is missing or invalid (see message). |
| 422 | checksum_mismatch | X-Chunk-SHA256 does not match the received data (retry). |
| 422 | invalid_e2ee_format | In e2ee mode, chunk 0 of a file does not start with SYNE. |
| 429 | rate_limited, ip_blocked | Too many requests; wait Retry-After seconds. |
| 429 | monthly_transfer_limit, monthly_volume_limit | The monthly plan limit is reached. |
| 500 | storage_error, server_error | Server problem; retry later. |
Upload flow
POST /transfersdeclares the files (name, size, number of chunks). The transfer is created aspending.PUT /transfers/{id}/files/{index}/chunks/{n}uploads each chunk. Chunks can be sent in parallel and in any order; sending the same chunk again is safe.POST /transfers/{id}/finalizeactivates the transfer and sends the emails.- Share the
urlfrom the response. Ine2eemode, append#key=and your key (see End-to-end encryption).
Unfinished transfers are deleted automatically after 2 days.
Account
/meAccount, plan, limits (null means unlimited), usage in the current month and the options included in the plan.
curl -H "Authorization: Bearer $KEY" https://www.syntransfer.ro/api/v1/me {
"id": 42, "name": "Jane Doe", "email": "jane@company.com",
"plan": {"slug": "business", "name": "Business"},
"team_member": false, "read_only": false,
"limits": {"max_transfer_size": null, "monthly_transfers": null, "monthly_volume": null,
"expiry_days": 30, "max_files_per_transfer": 100, "max_chunk_size": 67108864},
"usage": {"transfers_this_month": 17, "volume_this_month": 268744704},
"features": {"password_protection": true, "self_destroy": true, "preview": true,
"custom_expiry": true, "scheduled_send": true, "max_downloads": true, "download_stats": true},
"rate_limit": {"requests_per_minute": 60, "chunks_per_minute": 600}
} List transfers
/transfers?status=all&page=1&per_page=50| Parameter | Description |
|---|---|
status | all (default), active, expired or pending |
page, per_page | pagination, up to 100 per page (default 50); newest first |
{
"data": [
{"id": 1022, "token": "XoIpz6WLicyxE45l", "url": "https://www.syntransfer.ro/download.php?t=XoIpz6WLicyxE45l",
"status": "active", "mode": "e2ee", "title": "Q3 report", "message": null, "recipient_email": null,
"file_count": 4, "total_size": 31984467, "download_count": 2, "max_downloads": null,
"password_protected": false, "self_destroy": false,
"created_at": "2026-10-14T19:31:39+03:00", "expires_at": "2026-11-13T19:31:39+02:00", "scheduled_at": null}
],
"page": 1, "per_page": 50, "total": 1
} Create a transfer
/transfers| Field | Description |
|---|---|
files * | 1 to 100 objects: name, size (original size in bytes), chunks (how many chunks you will send), optional mime_type |
mode | e2ee (default: you encrypt the files), or an unencrypted mode: preview_download, preview_watermark, preview_only |
title, message | shown on the download page and in emails |
recipient_email | email the link to this address (not available with e2ee: the server never has the key) |
password | protect the download with a password |
self_destroy | true: delete after every file was downloaded once |
expires_at | earlier expiry than the plan default |
scheduled_at | send the recipient email later (requires recipient_email) |
max_downloads | maximum number of downloads |
lang | language of the emails (en, ro, de, ...); default: your account language |
Options that are not included in your plan are refused with 403 feature_not_in_plan.
curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"mode":"preview_download","title":"Contract","recipient_email":"client@company.com",
"files":[{"name":"contract.pdf","size":2400000,"chunks":1,"mime_type":"application/pdf"}]}' \
https://www.syntransfer.ro/api/v1/transfers Response 201: the transfer (status pending) with files[] (each with its index) and:
"upload": {
"chunk_url": "https://www.syntransfer.ro/api/v1/transfers/1031/files/{index}/chunks/{n}",
"finalize_url": "https://www.syntransfer.ro/api/v1/transfers/1031/finalize",
"max_chunk_size": 67108864
} Upload a chunk
/transfers/{id}/files/{index}/chunks/{n}The body is the raw chunk (Content-Type: application/octet-stream, Content-Length required). index is the file index from the create response, n goes from 0 to chunks - 1. Optional header X-Chunk-SHA256 (hex) lets the server reject damaged chunks.
curl -H "Authorization: Bearer $KEY" -X PUT \
-H "X-Chunk-SHA256: $(sha256sum part0 | cut -d' ' -f1)" \
--data-binary @part0 https://www.syntransfer.ro/api/v1/transfers/1031/files/0/chunks/0 {"file_index": 0, "chunk": 0, "chunks_received": 1, "chunks_total": 1, "file_complete": true, "stored_size": 2400000} Finalize
/transfers/{id}/finalizeActivates the transfer and sends the confirmation email (and the recipient email, unless scheduled). Returns the transfer with files[]. If a chunk is missing you get 409 upload_incomplete:
{"error": {"code": "upload_incomplete", "message": "Some files are not completely uploaded.",
"files": [{"index": 0, "chunks_received": 1, "chunks_total": 2}]}} Transfer details
/transfers/{id}The transfer with files[] (id, index, name, size, stored_size, mime_type, downloads). If your plan includes download statistics, downloads[] lists the latest 100 downloads (file_id, ip, user_agent, at).
Delete a transfer
/transfers/{id}Deletes the files from storage and the transfer link stops working.
{"deleted": true, "id": 1031} End-to-end encryption
In e2ee mode you encrypt the files before upload, in the same format the website uses, so the download page can decrypt them in the browser:
- generate one random AES-256-GCM key (32 bytes) per transfer;
- split each file into plaintext chunks (8 MB recommended) and encrypt each one with a new random 12-byte IV;
- each encrypted chunk is
[4-byte big-endian length][12-byte IV][ciphertext + 16-byte tag], where length = 12 + ciphertext length (tag included); - the first chunk of every file starts with the 4 bytes
SYNE; - declare the original (plaintext) sizes when you create the transfer;
- the link is
url+#key=+ the key in base64url without padding. The part after#is never sent to the server.
# Python (cryptography)
import os, struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
def encrypt_chunk(plain, first):
iv = os.urandom(12)
ct = AESGCM(key).encrypt(iv, plain, None) # ciphertext + tag
out = struct.pack(">I", 12 + len(ct)) + iv + ct
return b"SYNE" + out if first else out e2ee mode: the server cannot send a working link without the key. Send the link yourself.Command line app
syn uses this API and handles chunking, end-to-end encryption, parallel upload and retries for you. One file, nothing to install.
syn login # paste your API key
syn whoami # plan, limits, usage
syn send report.pdf photos/ # end-to-end encrypted, prints the link
syn send contract.pdf --no-encrypt --to client@company.com --title "Contract"
syn send backup.tar.gz -q # print only the link (for scripts)
syn list --status active
syn info 1022
syn delete 1022 --yes
syn --json list # JSON output for integrations The key can also come from the SYN_API_KEY environment variable (useful on servers and in CI). Run syn --help or syn send --help for every option.
Complete example
An unencrypted transfer of one file, with curl and jq:
KEY=syt_...
API=https://www.syntransfer.ro/api/v1
FILE=report.pdf
SIZE=$(stat -c %s "$FILE")
ID=$(curl -s -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{\"mode\":\"preview_download\",\"files\":[{\"name\":\"$FILE\",\"size\":$SIZE,\"chunks\":1}]}" \
$API/transfers | jq -r .id)
curl -s -H "Authorization: Bearer $KEY" -X PUT --data-binary @"$FILE" \
$API/transfers/$ID/files/0/chunks/0 > /dev/null
curl -s -H "Authorization: Bearer $KEY" -X POST $API/transfers/$ID/finalize | jq -r .url Files larger than 64 MB must be split into several chunks (split -b 8M), with chunks set accordingly.