Sealford

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.

RequestWhat it does
GET /meThe team and plan this key belongs to.
GET /documentsYour documents, newest first.
POST /documentsUpload 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}/linksThe document's share links.
POST /documents/{id}/linksCreate 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}/viewsVisits to one link, with time per page.
GET /documents/{id}/viewsVisits to one document across all its links.
GET /views?since=2026-11-11T08:00:00ZAll visits since a moment in time.
GET /dataroomsYour data rooms with their documents and links.

Visits by your own team are left out unless you add include_internal=true.

Link fields

FieldMeaning
nameYour label for the link.
referenceYour own reference, such as a CRM lead number. Sent back in webhooks.
activefalse switches the link off.
require_email, verify_emailAsk for an email; confirm it with a one-time code.
passwordSet a password. An empty string removes it.
expires_atWhen the link stops working, for example "2026-12-31T22:00:00Z". null removes it.
allow, blockLists of emails or domains.
allow_download, watermark, watermark_text, screenshot_protectionDocument protection.
welcome, questions, agreement_id, feedbackWhat visitors see before and after. questions is a list of {"label": "...", "required": true}.
notifyEmail 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:

EventWhen
link.viewedSomeone opens a link.
view.finishedA visit ends (sent about 10 minutes after they stop reading, with time per page).
document.downloadedSomeone downloads a document.
dataroom.question_askedSomeone 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.