API and webhook reference
Use the API to upload documents, create share links and read visits from your own tools. Use webhooks to be told the moment something happens.
Keys
Create a key under Settings, Integrations. Send it with every request:
Authorization: Bearer dd_your_key_here
A read-only key can only use GET requests. Requests are limited to 120 a minute per key. Errors come back as {"error": "what went wrong"} with a matching status code.
curl https://sealford.com/api/v1/me -H "Authorization: Bearer $KEY"
Endpoints
All addresses start with https://sealford.com/api/v1. Lists accept limit (up to 100) and offset.
| Request | What it does |
|---|---|
GET /me | The team and plan this key belongs to. |
GET /documents | Your documents, newest first. |
POST /documents | Upload a file (form field file, optional name). The document is ready when status is ready. |
GET /documents/{id} | One document. |
DELETE /documents/{id} | Delete a document, its links and its visit records. |
GET /documents/{id}/links | The document's share links. |
POST /documents/{id}/links | Create a share link. Send JSON with any of the link fields below. |
GET /links/{id} | One link, with its address in url. |
PATCH /links/{id} | Change a link. Send only the fields you want to change. |
DELETE /links/{id} | Delete a link. Its address stops working. |
GET /links/{id}/views | Visits to one link, with time per page. |
GET /documents/{id}/views | Visits to one document across all its links. |
GET /views?since=2026-11-11T08:00:00Z | All visits since a moment in time. |
GET /datarooms | Your data rooms with their documents and links. |
Visits by your own team are left out unless you add include_internal=true.
Link fields
| Field | Meaning |
|---|---|
name | Your label for the link. |
reference | Your own reference, such as a CRM lead number. Sent back in webhooks. |
active | false switches the link off. |
require_email, verify_email | Ask for an email; confirm it with a one-time code. |
password | Set a password. An empty string removes it. |
expires_at | When the link stops working, for example "2026-12-31T22:00:00Z". null removes it. |
allow, block | Lists of emails or domains. |
allow_download, watermark, watermark_text, screenshot_protection | Document protection. |
welcome, questions, agreement_id, feedback | What visitors see before and after. questions is a list of {"label": "...", "required": true}. |
notify | Email the link's creator when it is opened. |
curl -X POST https://sealford.com/api/v1/documents/12/links \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name": "Karoo Fibre", "reference": "CRM-LEAD-2026-00042", "require_email": true}'
Webhooks
Add a webhook under Settings, Integrations. We send a POST request with a JSON body to your address for the events you tick:
| Event | When |
|---|---|
link.viewed | Someone opens a link. |
view.finished | A visit ends (sent about 10 minutes after they stop reading, with time per page). |
document.downloaded | Someone downloads a document. |
dataroom.question_asked | Someone asks a question in a data room. |
{
"id": "evt_8f2c...",
"type": "view.finished",
"created_at": "2026-11-11T09:14:03+00:00",
"data": {
"view": {"id": 77, "email": "thandi@karoofibre.co.za", "email_verified": false,
"duration_seconds": 191, "completion_percent": 80, "page_count": 5,
"pages": [{"page": 1, "seconds": 18.0}, {"page": 2, "seconds": 41.2}],
"downloaded": false, "country": "South Africa", "city": "Cape Town",
"device": "Computer", "browser": "Chrome", "os": "Windows"},
"link": {"id": 31, "name": "Karoo Fibre", "url": "...", "reference": "CRM-LEAD-2026-00042"},
"document": {"id": 12, "name": "Proposal"},
"dataroom": null
}
}
Checking the signature
Every message has a header X-Webhook-Signature: t=1794300000,v1=abc123.... To check that the message really came from us, compute an HMAC-SHA256 of t, a full stop, and the exact request body, using the webhook's signing secret as the key. It must equal v1. Reject messages where t is more than five minutes old.
import hmac, hashlib
def is_genuine(secret, header, body_bytes):
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + body_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Answer with any 2xx status within 15 seconds. If your address fails, we try again after 1 minute, 5 minutes, 30 minutes and 2 hours, then stop. The same event can arrive more than once; use its id to ignore repeats.