E-signature API code examples · cURL, Node, Python, PHP, Ruby
Get your API key. 25 documents free, no card. Copy the e-signature API code example for your stack, set your key, and run it. 25¢ per envelope, sent by text, email, or both, with no monthly fee.
25 documents free. No card needed.
Check your email
We sent a sign-in link to . It is good for 15 minutes, and your API key is waiting on the other side.
B64=$(base64 < agreement.pdf | tr -d '\n') API=https://api.esigndev.com/v1/envelopes AUTH="Authorization: Bearer $SIGLIO_API_KEY" # 1. Send it, by text and email ENV_ID=$(curl -s $API -H "$AUTH" -H "Content-Type: application/json" -d '{ "document_name": "Service agreement", "document_base64": "'"$B64"'", "delivery": "both", "sender": { "company_name": "Your Company" }, "signer": { "name": "Jane Smith", "email": "jane@example.com", "phone": "+18135551212" } }' | sed -E 's/.*"id":"(env_[a-z0-9]+)".*/\1/') # 2. Check where it stands curl -s $API/$ENV_ID -H "$AUTH" # 3. Once it is completed, download the signed PDF curl -s $API/$ENV_ID/document -H "$AUTH" -o signed.pdf
import { readFileSync, writeFileSync } from "node:fs";
const api = "https://api.esigndev.com/v1/envelopes";
const auth = { Authorization: `Bearer ${process.env.SIGLIO_API_KEY}` };
const env = await (await fetch(api, { // 1. send it
method: "POST", headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
document_name: "Service agreement", delivery: "both",
document_base64: readFileSync("agreement.pdf").toString("base64"),
sender: { company_name: "Your Company" },
signer: { name: "Jane Smith", email: "jane@example.com", phone: "+18135551212" },
}),
})).json();
const now = await (await fetch(`${api}/${env.id}`, { headers: auth })).json(); // 2. status
console.log(env.id, now.state);
const pdf = await fetch(`${api}/${env.id}/document`, { headers: auth }); // 3. signed PDF
if (pdf.ok) writeFileSync("signed.pdf", Buffer.from(await pdf.arrayBuffer()));
import base64, os, requests
api = "https://api.esigndev.com/v1/envelopes"
auth = {"Authorization": f"Bearer {os.environ['SIGLIO_API_KEY']}"}
with open("agreement.pdf", "rb") as f: # 1. send it
pdf = base64.b64encode(f.read()).decode()
env = requests.post(api, headers=auth, json={
"document_name": "Service agreement", "document_base64": pdf, "delivery": "both",
"sender": {"company_name": "Your Company"},
"signer": {"name": "Jane Smith", "email": "jane@example.com", "phone": "+18135551212"},
}).json()
now = requests.get(f"{api}/{env['id']}", headers=auth).json() # 2. status
print(env["id"], now["state"])
signed = requests.get(f"{api}/{env['id']}/document", headers=auth) # 3. signed PDF
if signed.ok:
open("signed.pdf", "wb").write(signed.content)
<?php
$api = "https://api.esigndev.com/v1/envelopes"; $auth = "Authorization: Bearer " . getenv("SIGLIO_API_KEY");
function call($url, $auth, $body = null) {
$c = curl_init($url);
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [$auth, "Content-Type: application/json"]]);
if ($body) curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($body));
$out = curl_exec($c); $code = curl_getinfo($c, CURLINFO_RESPONSE_CODE);
return [$code, $out];
}
$env = json_decode(call($api, $auth, [ // 1. send it
"document_name" => "Service agreement", "delivery" => "both",
"document_base64" => base64_encode(file_get_contents("agreement.pdf")),
"sender" => ["company_name" => "Your Company"],
"signer" => ["name" => "Jane Smith", "email" => "jane@example.com", "phone" => "+18135551212"],
])[1], true);
[, $out] = call("$api/{$env['id']}", $auth); // 2. status
echo $env["id"], " ", json_decode($out, true)["state"], "\n";
[$code, $pdf] = call("$api/{$env['id']}/document", $auth); // 3. signed PDF
if ($code === 200) file_put_contents("signed.pdf", $pdf);
require "net/http"; require "json"; require "base64"
API = URI("https://api.esigndev.com/v1/envelopes")
AUTH = { "Authorization" => "Bearer #{ENV.fetch('SIGLIO_API_KEY')}", "Content-Type" => "application/json" }
http = Net::HTTP.new(API.host, API.port).tap { |h| h.use_ssl = API.scheme == "https" }
body = { document_name: "Service agreement", delivery: "both", # 1. send it
document_base64: Base64.strict_encode64(File.binread("agreement.pdf")),
sender: { company_name: "Your Company" },
signer: { name: "Jane Smith", email: "jane@example.com", phone: "+18135551212" } }
env = JSON.parse(http.post(API.path, body.to_json, AUTH).body)
now = JSON.parse(http.get("#{API.path}/#{env['id']}", AUTH).body) # 2. status
puts "#{env['id']} #{now['state']}"
pdf = http.get("#{API.path}/#{env['id']}/document", AUTH) # 3. signed PDF
File.binwrite("signed.pdf", pdf.body) if pdf.code == "200"
Each sample sends agreement.pdf (tagged ^S1 in white text) to Jane Smith by text and email, checks its state, and downloads the signed PDF once it is completed. Set SIGLIO_API_KEY to your key. Node 18 or later, Python with requests, PHP with the curl extension, Ruby’s standard library. In production, add an Idempotency-Key header to the send.
Your app sends the PDF and the signer. Siglio delivers the link, the signer signs on their phone or computer, and your webhook hears about it. Then you fetch the signed PDF.
Every account gets one webhook endpoint and every event type on it. Each delivery carries an X-Siglio-Signature header: t, a Unix time, and v1, the HMAC-SHA256 of t, a full stop and the raw body, keyed with your signing secret. Check it on the raw bytes before you trust the body.
| Event | Fires when |
|---|---|
envelope.delivered | The signing link went out by text, email or both |
envelope.viewed | The signer opened the document |
envelope.partially_signed | Signer one of two has signed. Not done yet |
envelope.completed | Everyone has signed. Download the signed PDF now |
envelope.voided | You voided it, or it expired unsigned after 30 days |
envelope.paid | A payment you asked for was recorded |
envelope.attachments_received | The signer uploaded every file you asked for |
{
"id": "evt_8f3k2m9q1x",
"type": "envelope.completed",
"occurred_at": "2026-10-08T15:04:11.482Z",
"data": {
"envelope": {
"id": "env_k7m2v9x3q1",
"object": "envelope",
"state": "completed",
"document_name": "Service agreement",
"delivery": "both",
"signer": { "name": "Jane Smith", "email": "jane@example.com",
"phone": "+18135551212", "signed_at": "2026-10-08T15:04:09Z" },
"completed_at": "2026-10-08T15:04:09Z"
}
}
}
// Node 18+, no dependencies. Saved as hook.mjs
import { createServer } from "node:http";
import crypto from "node:crypto";
createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]+)$/.exec(req.headers["x-siglio-signature"] || "") || [];
const mac = t && crypto.createHmac("sha256", process.env.SIGLIO_WEBHOOK_SECRET).update(`${t}.${body}`).digest("hex");
const ok = mac && v1.length === mac.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(mac));
if (!ok || Math.abs(Date.now() / 1000 - t) > 300) return res.writeHead(401).end();
const event = JSON.parse(body);
console.log(event.type, event.data.envelope?.id); // queue it, answer fast
res.writeHead(200).end();
});
}).listen(3000);
# Python 3, standard library only
import hashlib, hmac, json, os, re, time
from http.server import BaseHTTPRequestHandler, HTTPServer
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]+)", self.headers.get("X-Siglio-Signature", ""))
mac = m and hmac.new(os.environ["SIGLIO_WEBHOOK_SECRET"].encode(), m[1].encode() + b"." + body, hashlib.sha256).hexdigest()
if not (m and hmac.compare_digest(mac, m[2]) and abs(time.time() - int(m[1])) <= 300):
self.send_response(401); self.end_headers(); return
event = json.loads(body)
print(event["type"], event["data"].get("envelope", {}).get("id")) # queue it, answer fast
self.send_response(200); self.end_headers()
HTTPServer(("", 3000), Hook).serve_forever()
Both handlers reject a bad or missing signature, and anything signed more than five minutes ago, with 401. Answer 2xx fast and do the work from your own queue. Full details in the docs.
There is no official SDK. The API is JSON over HTTPS with a Bearer key, so the HTTP client your language already has is enough, as the samples above show.
siglio-mcp on npm, so Claude Desktop or any MCP client can send a document from a conversation. Setup.| What | Siglio |
|---|---|
| Per envelope | 25¢ |
| SMS delivery | Included, same 25¢ |
| Monthly fee | $0 |
| Per seat | $0 |
| Minimum | $0 |
| Free trial | 25 documents free, no card |
You are billed when an envelope is created, not when it is signed. A declined or ignored envelope still costs 25¢. E-signature API pricing in full.
Yes: 120 requests a minute per API key, across all endpoints. Over it you get a 429 with a Retry-After header. Your 25 free documents use the same key and the same limit.
No. One key works from your first document. After the 25 free ones, add a card and your business details in the dashboard, and the same key keeps sending at 25¢ an envelope.
None, and none are needed: it is plain REST and JSON, so it works from any language. The OpenAPI spec will generate a client if you want one.
Yes. Your endpoint has to be an HTTPS URL, so expose your local port with a tunnel such as ngrok, save that URL in the dashboard, and press Send test: it posts a real signed webhook.test event to your handler.
30 days after the envelope closes, then they are deleted. Download the signed PDF when envelope.completed arrives and keep your own copy.
One email address and your API key is on the next screen. No card, no sales call.
25 documents free. No card needed.
Check your email
We sent a sign-in link to . It is good for 15 minutes, and your API key is waiting on the other side.
Cookies on this site
We use analytics and advertising-measurement cookies to see how people find Siglio and whether our ads work. They measure our own campaigns. We do not build marketing lists from them and we do not share your information with third parties for their marketing. The site works fully with them off. Privacy policy