HL Sign API
A JSON REST API for creating, sending and tracking envelopes. Everything in the web app goes through this same API, so anything you can do there you can automate.
Authentication
Create a key under API keys and send it on every request:
Authorization: Bearer hls_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Base URL: https://sign.hlc.sh/api/v1. Errors look like {"error":{"code":"…","message":"…","details":…}}, with a 4xx/5xx status.
Concepts
- Envelope: one PDF, its recipients and fields. Status is
draft → sent → completed, ordeclined/voided. - Recipient: a
signeror acc(gets the completed copy). Withsequential: true, signers are invited inrouting_order. - Field:
signature,initials,date(filled automatically when signing),name,email,textorcheckbox. - Coordinates: by default in PDF points (1/72") from the top-left of the page, with
pagestarting at 1. Pass"units": "fraction"to use 0–1 fractions of the page instead. Responses always return fractions.
Endpoints
POST/api/v1/envelopes
Create an envelope. Set send: true to send it straight away. Supply the PDF as one of:
document.base64: inline, up to 3 MB.document.url: a public HTTPS URL we download.document.upload_id: for files up to 50 MB. SeePOST /uploads.
Fields reference a recipient by 0-based index, email, or the ref you gave it.
curl https://sign.hlc.sh/api/v1/envelopes \
-H "Authorization: Bearer $HLSIGN_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Subcontract agreement",
"message": "Please sign by Friday.",
"document": { "name": "contract.pdf", "base64": "'"$(base64 -i contract.pdf)"'" },
"recipients": [
{ "ref": "sub", "name": "Jane Builder", "email": "jane@example.com" },
{ "name": "Accounts", "email": "accounts@example.com", "role": "cc" }
],
"fields": [
{ "recipient": "sub", "type": "signature", "page": 3, "x": 72, "y": 600, "width": 180, "height": 40 },
{ "recipient": "sub", "type": "date", "page": 3, "x": 300, "y": 615 },
{ "recipient": "sub", "type": "text", "page": 1, "x": 72, "y": 140, "label": "ABN", "required": true }
],
"webhook_url": "https://example.com/hooks/hlsign",
"send": true
}'Returns 201 with { envelope, signing_links }. signing_links holds each invited signer's private URL, so you can embed signing in your own app or send links yourself.
POST/api/v1/uploads
For large PDFs. Returns a presigned URL that is valid for 15 minutes. PUT the raw bytes to it, then create the envelope with document.upload_id.
UPLOAD=$(curl -s -X POST https://sign.hlc.sh/api/v1/uploads -H "Authorization: Bearer $HLSIGN_KEY")
curl -X PUT "$(echo $UPLOAD | jq -r .upload_url)" -H "Content-Type: application/pdf" --data-binary @big.pdf
# then: "document": { "upload_id": "<upload_id>", "name": "big.pdf" }GET/api/v1/envelopes
Lists envelopes, newest first. Query: status, q (title search), batch (bulk send id), limit (max 100), before (pass next_before from the previous page).
GET/api/v1/envelopes/:id
Full envelope with recipients, their statuses and the fields.
PATCH/api/v1/envelopes/:id
Update a draft. Accepts the same keys as create (except send). recipients and fields replace the existing lists, so send both together.
POST/api/v1/envelopes/:id/send
Send a draft. Every signer must have at least one signature field. Returns signing_links.
POST/api/v1/envelopes/:id/bulk-send
Send a separate copy of a draft to each person on a list, for example a policy everyone has to acknowledge. The draft acts as the template. One signer is the placeholder, and each copy replaces it with a person from the list. Other recipients (countersigners, CCs) are copied onto every envelope unchanged.
The placeholder is the signer with email bulk-recipient@placeholder.invalid, or the first signer. You can also name it with placeholder (recipient id, email or index). Up to 200 people per call. Duplicate emails are dropped. Once every copy is sent, the draft is deleted unless you pass keep_draft: true.
curl https://sign.hlc.sh/api/v1/envelopes/$DRAFT_ID/bulk-send -H "Authorization: Bearer $HLSIGN_KEY" -H "Content-Type: application/json" -d '{ "recipients": [
{ "name": "Dana Driver", "email": "dana@example.com" },
{ "name": "Eli Driver", "email": "eli@example.com" }
] }'
# → 201 { "batch_id": "…", "total": 2,
# "sent": [{ "envelope_id": "…", "email": "dana@example.com", "signing_url": "…", "emailed": true }, …],
# "failed": [] }GET/api/v1/bulk-sends/:id
Progress of a bulk send: counts by status (sent, completed, declined, voided) and each envelope with its recipient. You can also list a batch's envelopes with GET /api/v1/envelopes?batch=:id.
POST/api/v1/envelopes/:id/remind
Emails signers who haven't finished again. Each reminder issues a new link, and the earlier link stops working.
POST/api/v1/envelopes/:id/void
Cancel an in-progress envelope. Body: { "reason": "Wrong version attached" }.
GET/api/v1/envelopes/:id/document
Redirects to a short-lived download URL. ?type=completed (the default once complete) returns the signed PDF with its certificate of completion. ?type=original returns the PDF as uploaded. Add &redirect=false to get { url } as JSON instead.
GET/api/v1/envelopes/:id/audit
The audit trail: who did what, and when, including IP address and user agent.
DELETE/api/v1/envelopes/:id
Delete a draft.
Webhooks
When an envelope has a webhook_url, we POST JSON to it on these events: envelope.sent, recipient.viewed, recipient.signed, envelope.completed, envelope.declined and envelope.voided. Failed deliveries are retried up to 3 times.
POST /hooks/hlsign
X-HLSign-Event: envelope.completed
X-HLSign-Timestamp: 1767225600
X-HLSign-Signature: 5d41402abc4b2a76b9719d911017c592...
{ "event": "envelope.completed", "envelope_id": "…", "status": "completed",
"occurred_at": "2026-01-01T00:00:00.000Z", "data": { "completed_sha256": "…" } }Verify the signature, which is hex(HMAC_SHA256(WEBHOOK_SIGNING_SECRET, timestamp + "." + rawBody)):
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const expected = createHmac("sha256", secret)
.update(`${headers["x-hlsign-timestamp"]}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(headers["x-hlsign-signature"]));
}