API & Webhooks
Fetch your transfers and statistics programmatically, create transfers from your own system, and get notified as soon as someone downloads your files. REST API with permissions per key + HMAC-signed webhooks.
1. Getting started
Create an API key on your account page (part of every paid plan). The key is shown only once — keep it safe. Send it with every request:
Authorization: Bearer dlk_live_xxxxxxxxxxxxxxxxxxxx
Permissions and expiry date
When you create a key you choose what it may do. Do not give a key more permissions than your integration needs.
- Read (lezen) — GET /api/v1/me, GET /api/v1/transfers, GET /api/v1/transfers/<token>.
- Create (maken) — POST /api/v1/transfers.
- Email (mailen) — send email on your behalf with send_transfer, send_folder_link, remind_recipients and request_signature of the MCP server (see 2c); can only be chosen with two-step verification on and only works while it is on. The REST API does not send email.
A new key expires: after 90 days by default, after 365 at most. You confirm creating it with your password (or a link by email) and, if your two-step verification is on, with the code from your app; you then receive an email. Keys you created earlier have no expiry date and keep read and create; your account page warns about them.
What your key may do and until when is in the response of /api/v1/me (field key).
2. Endpoints
Base URL: https://downloadlink.nl/api/v1. All responses are JSON and limited to your own account.
The full description is available as OpenAPI 3.1 at /api/v1/openapi.json.
GET /api/v1/me
Account + storage usage.
curl -H "Authorization: Bearer dlk_live_xxx" \
https://downloadlink.nl/api/v1/me
{
"email": "jij@bedrijf.nl",
"is_trial": false,
"plan": "100 GB",
"storage": { "used_bytes": 5242880, "quota_bytes": 107374182400, "percent": 0 },
"key": { "scopes": ["lezen", "maken"], "expires_at": "2027-01-11T09:12:00+00:00", "type": "api" }
}
GET /api/v1/transfers
Your transfers (newest first). Parameter ?limit= (1–200, default 50).
{
"count": 1,
"transfers": [
{
"token": "a1b2c3d4e5f6a7b8",
"title": "Quote Q3",
"created_at": "2026-06-17T09:12:00+00:00",
"expires_at": "2026-06-24T09:12:00+00:00",
"unlimited": false,
"revoked": false,
"files": 3,
"size_bytes": 8412300,
"downloads": 2,
"recipients": 1,
"url": "https://downloadlink.nl/p/a1b2c3d4e5f6a7b8"
}
]
}
GET /api/v1/transfers/<token>
A single transfer: metadata, the files, and recent download activity (who/when/which file).
{
"token": "a1b2c3d4e5f6a7b8",
"title": "Quote Q3",
"created_at": "2026-06-17T09:12:00+00:00",
"expires_at": "2026-06-24T09:12:00+00:00",
"unlimited": false,
"revoked": false,
"max_downloads": null,
"downloads_used": 0,
"size_bytes": 8412300,
"files": [
{ "name": "quote.pdf", "size_bytes": 412300 }
],
"downloads": 2,
"url": "https://downloadlink.nl/p/a1b2c3d4e5f6a7b8",
"activity": [
{
"downloaded_at": "2026-06-17T10:30:00+00:00",
"type": "zip",
"file": "(whole package / zip)",
"ip": "203.0.113.7"
},
{
"downloaded_at": "2026-06-17T09:58:00+00:00",
"type": "file",
"file": "quote.pdf",
"ip": "198.51.100.24"
}
]
}
Note: ip is the full IP address of the person who downloaded — personal data of your recipient, not an anonymised figure. You receive it in full, just as in the activity overview, the downloads CSV and the proof of delivery (PDF); it is there so you can demonstrate that a package has been retrieved. If you process or store it in your own system, include it in your own privacy statement. We return the 100 most recent records; downloads counts those records. We keep them for a maximum of 12 months — after that, they can no longer be retrieved, not even via the API. downloads_used is the counter for the download limit and remains 0 as long as max_downloads is empty.
Rate limit: 120 requests per minute per key (HTTP 429 when exceeded).
Error codes
- 401 unauthorized — no, unknown or revoked key.
- 401 key_expired — the key has expired (field expired_at); create a new one on your account page.
- 402 payment_required — your account no longer has an active paid plan; your keys are kept.
- 403 insufficient_scope — the key does not have the permission (field required_scope).
- 429 rate_limited — too many requests, or more than 50 new transfers per hour (per key and for all keys of an account together); wait the number of seconds in Retry-After.
2b. Creating a transfer (write)
Deliver a file via your own branded download page, straight from your system — a generated report or invoice, for example:
curl -X POST https://downloadlink.nl/api/v1/transfers \ -H "Authorization: Bearer <sleutel>" \ -F "file=@rapport.pdf" \ -F "title=Rapport week 30" \ -F "expiry_days=30" # of 'never' voor onbeperkt geldig # optioneel: -F "password=geheim" voor een wachtwoord op de downloadpagina
HTTP 201
{
"ok": true,
"token": "a1b2c3d4e5f6a7b8",
"url": "https://downloadlink.nl/p/a1b2c3d4e5f6a7b8",
"expires_at": "2026-08-22T10:30:00+00:00",
"unlimited": false
}
Error paths: 402 payment_required (the write API is part of the paid plans), 402 quota_exceeded (storage full), 413 (file larger than the API limit — use the upload page for that). The download page branding follows the account the key belongs to.
2c. MCP server
Not a developer? Connect your AI assistant in a few clicks: Connect an AI assistant, step by step.
For an MCP client (Model Context Protocol, streamable HTTP transport, without a session). Use the same key in the Authorization header; a key in the URL is refused.
POST https://downloadlink.nl/mcp Authorization: Bearer dlk_live_xxx
- lezen — whoami, list_transfers, get_transfer, list_received, get_received, search_tools, find_music, find_sfx, find_templates, how_to, suggest_improvement, get_transfer_activity, get_delivery_proof, list_contacts, list_send_groups, get_branding, export_account, list_signature_requests, list_folders, get_folder, get_folder_feedback, get_changes_since, read_planning, list_rules.
- maken — create_transfer_from_text, start_upload, complete_upload, create_request_link, extend_transfer, revoke_transfer, update_transfer, restore_transfer, delete_transfer, close_request_link, set_request_link_name, delete_received, save_contact, delete_contact, save_send_group, delete_send_group, set_branding, set_logo, set_tool_style, set_expiry_reminders, cancel_signature_request, create_folder, create_folder_dir, upload_to_folder, delete_folder_item, set_transfer_options, set_folder_options, organize_folder, save_contacts, set_defaults, save_request_template, delete_request_template, describe_tool, run_tool, edit_document, tab_job, open_file, set_rule.
- mailen — send_transfer, send_folder_link, request_signature, remind_recipients.
Email is only sent with send_transfer, send_folder_link, remind_recipients and request_signature: that needs a key with the mailen scope, and that scope only works while two-step verification is on for the account. A tool the key has no scope for is not in tools/list and returns the tool error insufficient_scope.
Address, scopes and tools as JSON: /developers/mcp.json.
whoami and /api/v1/me list in plan_info what the account's plan includes; a refusal because of the plan carries needed and pricing_url.
Through the MCP server a new transfer is valid for 30 days by default and at most 90, also after extending; a transfer that never expires is not possible there (expiry_not_allowed). This applies to every key; the REST API above keeps its own rules.
search_tools searches all tools on the site with the same search as /tools and returns per tool its name, a short summary, the category, the formats and the link to the tool page (in Dutch, English or French). The tools themselves run in the user's browser: the files stay on the user's computer, and the MCP server does not process files.
No tool of the MCP server asks for the account's uploads password, just like the REST API: the key with the scope maken is the authorisation, so it can also change, revoke and delete transfers (such as update_transfer, revoke_transfer and delete_transfer) and fill folders. Give a key only lezen if that must not happen.
get_received gives, per received file of one of the account’s file request links, a download link that works for an hour; such a link counts as a download by the account owner, once per file per hour.
Without a key: an MCP client that supports OAuth finds the authorization server through the WWW-Authenticate header of a 401 and /.well-known/oauth-protected-resource/mcp. The customer logs in and chooses the scopes (lezen is on, maken off, mailen is not available via OAuth). PKCE with S256 and the resource parameter (https://downloadlink.nl/mcp) are required; register with a Client ID Metadata Document or through /oauth/register.
An access token is valid for one hour and only for /mcp; a refresh token for 30 days and works once (rotation). The customer revokes a connection on their account page under Connected assistants.
3. Webhooks
Register an https-URL on your account page (the "Test" button immediately sends a ping-event so you can check your integration). As soon as someone downloads your transfer, we send a POST with this event:
POST https://jouw-server.nl/webhooks/downloadlink
X-Downloadlink-Event: transfer.downloaded
X-Downloadlink-Signature: sha256=<hmac>
Content-Type: application/json
{
"event": "transfer.downloaded",
"delivered_at": "2026-06-17T10:30:00+00:00",
"data": {
"token": "a1b2c3d4e5f6a7b8",
"title": "Quote Q3",
"url": "https://downloadlink.nl/p/a1b2c3d4e5f6a7b8",
"download_type": "zip",
"downloaded_at": "2026-06-17T10:30:00+00:00"
}
}
Verifying the signature
Compute HMAC-SHA256 over the raw request body with your webhook secret and compare it constant-time against the header. Ignore messages that do not match.
Python
import hmac, hashlib
def is_valid(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
Node.js
const crypto = require('crypto');
function isValid(secret, rawBody, header) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(header || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Reply with HTTP 2xx to confirm delivery. After 15 failed attempts we switch the webhook off; a successful test (button on your account page) turns it back on. Only public https URLs are allowed.