Integreer juridisch geldige e-handtekeningen direct in je eigen applicatie. Upload pdf's, verstuur ondertekentransacties, plaats handtekeningvelden en ontvang webhook-events, allemaal via een eenvoudige REST API. De API hoort bij Enterprise; goedgekeurde partners testen gratis met een sandbox-sleutel.
https://api.builtsign.com/v1Instellingen → API-sleutels
Een document laten ondertekenen in 4 API-aanroepen.
Ga naar Instellingen → API-sleutels in je BuiltSign-dashboard en maak een sleutel aan met schrijfrechten. Sla hem veilig op, hij wordt maar één keer getoond.
Settings → API KeysUpload een PDF (tot 50 MB) als multipart-formulierdata. Je ontvangt een document_id voor de volgende stap. Grotere bestanden, tot 1 GB, gaan in delen via POST /v1/uploads.
curl -X POST https://api.builtsign.com/v1/documents/upload \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx" \
-F "file=@contract.pdf"Maak een ondertekentransactie aan met het document_id en een lijst van ondertekenaars. Stel auto_send: false in om eerst velden te plaatsen.
curl -X POST https://api.builtsign.com/v1/signing-requests \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"document_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"signers": [
{ "name": "Jan de Vries", "email": "jan@example.com" }
],
"message": "Graag de offerte ondertekenen.",
"expires_in_days": 7,
"auto_send": false
}'Plaats handtekeningvelden met procentuele coördinaten en verstuur vervolgens om de uitnodigingsmail te sturen.
curl -X POST https://api.builtsign.com/v1/signing-requests/{request_id}/fields \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fields": [
{
"signer_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"field_type": "signature",
"page_number": 1,
"x_percent": 60.0,
"y_percent": 82.0,
"width_percent": 30.0,
"height_percent": 8.0
}
]
}'Laat een tekeningenset of opleverdossier van honderden MB's ondertekenen en zet het resultaat in je eigen systeem. Zes stappen, allemaal in de sandbox.
1. Sandbox-sleutel en webhook
Maak een sandbox-sleutel aan (Instellingen → API-sleutels, vink Sandbox aan). Een webhook die je met die sleutel aanmaakt, krijgt alleen sandbox-events. Bewaar het geheim uit het antwoord; het wordt één keer getoond.
2. Upload de PDF in delen
uploadLarge stuurt het bestand in delen rechtstreeks naar de opslag, tot 1 GB, meerdere delen tegelijk. Een mislukt deel wordt opnieuw geprobeerd met een nieuwe URL.
3. Maak de transactie en plaats velden
Maak de transactie aan met auto_send: false en een Idempotency-Key die bij je eigen record hoort, en plaats daarna de velden in procenten per pagina.
4. Verstuur
Verstuur met een Idempotency-Key, zodat een nieuwe poging na een netwerkfout nooit dubbel verstuurt. In de sandbox gaat er geen mail of sms uit: open de ingebedde ondertekenlink zelf en teken.
5. Ontvang en controleer de webhook
Controleer X-BuiltSign-Webhook-Signature op de ruwe body met webhooks.verify, antwoord direct met 2xx en sla een event_id over die je al verwerkt hebt.
6. Haal de getekende PDF en het auditcertificaat op
Bij document.signed haal je de getekende PDF op via de link van 15 minuten en het auditcertificaat als PDF. Een 409 document_not_ready betekent: wacht Retry-After seconden en probeer het opnieuw.
import { createServer } from "node:http";
import { writeFile } from "node:fs/promises";
import BuiltSign, { BuiltSignError } from "@builtsign/node";
const builtsign = new BuiltSign({ apiKey: process.env.BUILTSIGN_API_KEY! }); // sandbox key
if (!(await builtsign.me()).sandbox) throw new Error("Use a sandbox key");
// 1. A webhook made with a sandbox key only gets sandbox events. The secret is shown once.
const webhook = await builtsign.webhooks.create({
name: "Docstream",
url: "https://your-host.example.com/builtsign",
events: ["document.signed", "signer.declined", "signing_request.expired"],
});
// 2. Up to 1 GB, in parts straight to storage
const document = await builtsign.documents.uploadLarge("drawing-set.pdf", { concurrency: 4 });
// 3. A draft, tied to your own record, then the fields (percent of the page)
const request = await builtsign.signingRequests.create(
{
document_id: document.document_id,
signers: [{ name: "Jan de Vries", email: "jan@example.com", order: 1 }],
auto_send: false,
},
{ idempotencyKey: `docstream-${recordId}` },
);
await builtsign.signingRequests.addFields(request.id, [{
signer_id: request.signers[0].id, field_type: "signature",
page_number: 1, x_percent: 60, y_percent: 82, width_percent: 30, height_percent: 8,
}]);
// 4. Send. Sandbox: no mail goes out, so sign through the embedded link yourself
await builtsign.signingRequests.send(request.id, { idempotencyKey: `send-${request.id}` });
const { signers } = await builtsign.signingRequests.embeddedUrl(request.id, { allowed_origin: "http://localhost:3000" });
console.log("Sign here:", signers[0].embedded_url);
// 5. Verify every delivery on the raw body, answer 2xx at once, deduplicate on event_id
const handled = new Set<string>();
createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk);
let event;
try {
event = builtsign.webhooks.verify(Buffer.concat(chunks), req.headers["x-builtsign-webhook-signature"], webhook.secret);
} catch {
return res.writeHead(401).end();
}
res.writeHead(200).end();
if (handled.has(event.event_id)) return;
handled.add(event.event_id);
if (event.event === "document.signed") await saveResults(event.data.signing_request_id as string);
}).listen(4567);
// 6. Signed PDF and audit certificate; 409 document_not_ready means: wait Retry-After
async function saveResults(id: string) {
const { download_url } = await whenReady(() => builtsign.signingRequests.download(id));
await writeFile(`${id}-signed.pdf`, new Uint8Array(await (await fetch(download_url)).arrayBuffer()));
await writeFile(`${id}-audit-certificate.pdf`, await whenReady(() => builtsign.signingRequests.auditCertificate(id)));
}
async function whenReady<T>(call: () => Promise<T>): Promise<T> {
for (;;) {
try {
return await call();
} catch (err) {
if (!(err instanceof BuiltSignError) || err.code !== "document_not_ready") throw err;
await new Promise((r) => setTimeout(r, (err.retryAfter ?? 5) * 1000));
}
}
}Test alleen met een sandbox-sleutel. Het voorbeeld controleert eerst GET /v1/me en stopt bij een live sleutel.
Alle v1-endpoints vereisen een API-sleutel in de X-API-Key request header.
curl https://api.builtsign.com/v1/signing-requests \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx"| Scope | Permissions |
|---|---|
read | Bronnen ophalen en weergeven |
write | Bronnen aanmaken, bijwerken en versturen |
admin | Gereserveerd voor toekomstig gebruik |
Sandbox-sleutels
Vink Sandbox aan bij het aanmaken van een sleutel. Verzoeken via die sleutel worden als sandbox gemarkeerd: uitnodigingen en herinneringen gaan niet uit, er is geen ID-check of sms en niets telt mee voor je documentverbruik. Getekende documenten krijgen een watermerk en hebben geen juridische waarde, en sandboxdata wordt na 30 dagen verwijderd. Geslaagde antwoorden bevatten de header X-BuiltSign-Mode: sandbox, en webhook-events bij uploaden, aanmaken, versturen en ondertekenen het veld is_sandbox. Nog geen Enterprise-plan? Vraag gratis sandbox-toegang aan; je sleutels hebben dan standaard een limiet van 30 verzoeken per minuut en 500 per dag.
OpenAPI-specificatie
Importeer de specificatie in een codegenerator, of blader er interactief doorheen. Voor Postman is er een kant-en-klare collectie met de variabelen baseUrl en apiKey, en een nieuwe Idempotency-Key bij elke aanmaak- en verstuuraanroep. openapi.json · Interactieve referentie · Postman-collectie
Voor elke sleutel gelden twee limieten: verzoeken per minuut en verzoeken per dag. Elk antwoord laat zien waar je staat.
Elke sleutel heeft een limiet per minuut: standaard 60. Je kunt per sleutel een eigen limiet instellen, maar die komt nooit boven de limiet van je organisatie. De daglimiet (standaard 2000 aanroepen) begint om middernacht UTC opnieuw. Sandbox-sleutels van partners zonder Enterprise: 30 per minuut en 500 per dag.
| Header | Betekenis |
|---|---|
X-RateLimit-Limit | Toegestane verzoeken deze minuut |
X-RateLimit-Remaining | Resterende verzoeken deze minuut |
X-RateLimit-Reset | Unix-tijd waarop het minuutvenster opnieuw begint |
X-RateLimit-Daily-Limit | Toegestane verzoeken vandaag (UTC) |
X-RateLimit-Daily-Remaining | Resterende verzoeken vandaag |
X-RateLimit-Daily-Reset | Unix-tijd van de volgende middernacht UTC |
Retry-After | Bij 429 en 503: seconden wachten voor je het opnieuw probeert |
429 en 503
Boven de limiet per minuut geeft de API 429 met rate_limit_exceeded en Retry-After: wacht zoveel seconden en stuur dezelfde aanroep opnieuw. Boven de daglimiet geeft de API 429 met daily_limit_exceeded, en Retry-After loopt tot middernacht UTC. Is de limiter zelf even onbereikbaar, dan krijg je 503 met service_unavailable en Retry-After. Er is dan niets uitgevoerd, dus opnieuw proberen is veilig, ook zonder Idempotency-Key.
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1793577617
X-RateLimit-Daily-Limit: 2000
X-RateLimit-Daily-Remaining: 1412
X-RateLimit-Daily-Reset: 1793664000
{"error": {"code": "rate_limit_exceeded", "message": "Rate limit exceeded. Please slow down.", "request_id": "req_4e092e9aa07f4103a14f"}}De Node SDK wacht op Retry-After en probeert 429 en 503 zelf opnieuw (maxRetries, standaard 3).
Probeer een aanmaak- of verstuuraanroep veilig opnieuw: met dezelfde Idempotency-Key voert de API hem maar één keer uit.
Stuur een Idempotency-Key-header mee (een unieke tekst van maximaal 255 tekens) bij aanroepen die iets aanmaken of versturen: uploads, ondertekentransacties, versturen, annuleren, herinneren, sjablonen, versturen vanuit een sjabloon en bulkverzending. Een nieuwe poging met dezelfde sleutel en dezelfde body geeft het eerste antwoord terug met de header Idempotency-Replay: true, in plaats van het opnieuw uit te voeren. Sleutels worden 24 uur per API-sleutel bewaard.
curl -X POST https://api.builtsign.com/v1/signing-requests \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: order-1234" \
-H "Content-Type: application/json" \
-d '{"document_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "signers": [{"name": "Jan de Vries", "email": "jan@example.com"}]}'
# Same call again: the first response, with
# Idempotency-Replay: true| Situatie | Antwoord |
|---|---|
| Zelfde sleutel, zelfde body, eerste aanroep klaar | Het eerste antwoord, met Idempotency-Replay: true |
| Zelfde sleutel, andere body of ander endpoint | 422 idempotency_key_reused. Gebruik een nieuwe sleutel voor elk nieuw verzoek. |
| Zelfde sleutel terwijl de eerste aanroep nog loopt | De API wacht een paar seconden op het antwoord. Loopt hij dan nog: 409 idempotency_key_in_use met Retry-After |
| De sleutelopslag is even onbereikbaar | 503 service_unavailable met Retry-After. Er is niets uitgevoerd. |
Alleen geslaagde antwoorden worden bewaard. Na een fout, bijvoorbeeld een 402 of 422, voert een nieuwe poging met dezelfde sleutel opnieuw uit, zodat je de oorzaak kunt oplossen en het nog eens kunt proberen.
De Node SDK zet zelf een sleutel op elke aanmaak- en verstuuraanroep en gebruikt die opnieuw bij zijn eigen nieuwe pogingen. Geef idempotencyKey mee om hem aan je eigen record te koppelen, bijvoorbeeld order-1234.
Lijsten nemen limit (tot 100) met offset of met een cursor.
Pagineren met offset (limit en offset) is prima om een pagina te tonen. Het antwoord bevat items en total.
Gebruik voor synchroniseren de cursor op GET /v1/signing-requests, GET /v1/documents en GET /v1/events: geef de next_cursor van een pagina mee als starting_after, tot has_more false is. Items die intussen bijkomen of verdwijnen, laten je nooit een item overslaan of dubbel krijgen.
curl "https://api.builtsign.com/v1/documents?limit=100" \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx"
# { "items": [...], "total": 214, "has_more": true, "next_cursor": "3fa85f64-..." }
curl "https://api.builtsign.com/v1/documents?limit=100&starting_after=3fa85f64-..." \
-H "X-API-Key: bs_xxxxxxxxxxxxxxxxxxxxxxxx"offset samen met starting_after geeft 422 validation_error, net als een starting_after die geen item uit deze lijst is. Items met dezelfde tijd staan op id, dus de volgorde blijft tussen aanroepen gelijk. Op /v1/events blijft de oudere after_id werken.
Upload pdf's en bekijk eerder geüploade documenten.
Maak ondertekentransacties aan, beheer ze en volg de status.
Plaats velden op een document die ondertekenaars moeten invullen voordat zij kunnen afronden.
Coördinaten zijn procentueel (0–100) ten opzichte van de pagina-afmetingen. Het oorsprongspunt (0, 0) is de linkerbovenhoek van de pagina.
| field_type | Description |
|---|---|
signature | Volledige handtekening |
initials | Paraaf / initialen |
date | Datum (automatisch ingevuld) |
text | Vrije tekstinvoer |
checkbox | Aanvinkvak |
select | Dropdown (geef options[] mee) |
Ontvang real-time meldingen wanneer er iets gebeurt in BuiltSign, in het dashboard of via de API.
Maak een webhook aan via Instellingen → Webhooks, of met je API-sleutel via POST /v1/webhooks. Kies op welke events je wilt abonneren. BuiltSign stuurt een HTTP POST naar jouw endpoint voor elk event.
| Event | Trigger |
|---|---|
signing_request.created | Ondertekentransactie aangemaakt |
signing_request.sent | Transactie verzonden naar ondertekenaars |
signing_request.completed | Alle ondertekenaars hebben getekend |
signing_request.cancelled | Ondertekentransactie geannuleerd |
signing_request.expired | Ondertekentransactie verlopen |
signer.viewed | Ondertekenaar heeft de pagina geopend |
signer.signed | Ondertekenaar heeft ondertekend |
signer.declined | Ondertekenaar heeft geweigerd |
signer.withdrawn | Ondertekenaar heeft handtekening ingetrokken |
signer.reviewed | ID-check van een ondertekenaar wacht op handmatige beoordeling |
identity.approved | ID-check van een ondertekenaar goedgekeurd |
identity.rejected | ID-check van een ondertekenaar afgekeurd |
document.signed | Ondertekende PDF gegenereerd |
signing_request.reminded | Herinneringsmails verstuurd naar openstaande ondertekenaars |
document.uploaded | Document geüpload via API |
member.invited | Teamlid uitgenodigd voor de organisatie |
Elke webhook-POST bevat deze velden:
{
"event_id": "evt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"event": "signing_request.completed",
"timestamp": "2026-05-21T11:00:00Z",
"webhook_id": "wh_xxxxxxxxxxxxxxxxxxxx",
"data": {
"signing_request_id": "sr_xxxxxxxxxxxxxxxxxxxx",
"completed_at": "2026-05-21T11:00:00Z",
"all_signers": [
{ "email": "alice@example.com", "name": "Alice Smith", "signed_at": "2026-05-21T10:30:00Z" },
{ "email": "bob@example.com", "name": "Bob Jones", "signed_at": "2026-05-21T11:00:00Z" }
],
"signed_pdf_s3_key": "orgs/org_xxx/signed/sr_xxx.pdf"
}
}| Header | Value |
|---|---|
| Content-Type | application/json |
| X-BuiltSign-Webhook-Signature | t=<unix seconds>,v1=<HMAC-SHA256 hex>, a second v1 right after a secret rotation |
| X-Webhook-Delivery-ID | delivery UUID, the same on automatic retries |
| X-Webhook-Event | event type (e.g. signer.signed) |
| X-Webhook-Timestamp | unix seconds, the same as t |
| X-Webhook-ID | webhook UUID |
| X-Webhook-Retry | attempt number on retries, resend on a redelivery |
| X-Webhook-Test | true on test events only |
| X-Webhook-Signature | deprecated: HMAC-SHA256 hex over the body only |
Controleer elke webhook met de header X-BuiltSign-Webhook-Signature. Bereken HMAC-SHA256 met je webhookgeheim over de timestamp t, een punt en de ruwe body, vergelijk die in constante tijd met elke v1 en weiger een t die meer dan 5 minuten van je eigen klok afwijkt. Vertrouw de payload nooit zonder deze controle.
import crypto from "crypto";
const TOLERANCE_SECONDS = 300;
function verifyBuiltSign(rawBody, header, secret) {
let t;
const signatures = [];
for (const part of String(header ?? "").split(",")) {
const [key, value] = part.split("=", 2);
if (key === "t") t = Number(value);
if (key === "v1") signatures.push(value);
}
if (!Number.isInteger(t) || signatures.length === 0) return false;
// Refuse old timestamps, so a captured request cannot be replayed.
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(t + ".")
.update(rawBody) // raw Buffer, not parsed JSON
.digest();
// Constant-time compare; after a secret rotation there are two v1 values.
return signatures.some((sig) => {
const given = Buffer.from(sig, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
// Express middleware example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const valid = verifyBuiltSign(
req.body,
req.headers["x-builtsign-webhook-signature"],
process.env.BUILTSIGN_WEBHOOK_SECRET,
);
if (!valid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// Retries repeat event.event_id: skip an event you already handled.
// TODO: handle event (e.g. event.event === "document.signed")
res.sendStatus(200);
});Je ontvanger gebouwd vóór 22 oktober 2026? De header X-Webhook-Signature, een HMAC over alleen de body, wordt nog meegestuurd maar is uitgefaseerd. Hij heeft geen tijdstempel, dus een onderschept verzoek kan opnieuw worden verstuurd. Stap over op X-BuiltSign-Webhook-Signature; de einddatum kondigen we minstens 90 dagen vooraf aan in de changelog.
Herhalingen
Een levering slaagt bij elk 2xx-antwoord binnen 10 seconden. Een mislukte levering probeert BuiltSign nog 9 keer, met exponentiële backoff en spreiding: na ongeveer 1 minuut, 5 minuten, 30 minuten, 2 uur en 6 uur, daarna elke 12 uur, samen ruim 2,5 dag. Pauzeer je de webhook, dan stoppen de herhalingen. Een webhook gaat automatisch uit als 10 events op rij mislukken over minstens 3 dagen.
Dubbele leveringen
Elke payload heeft een event_id dat bij elke herhaling en elke nieuwe levering gelijk blijft, en elk verzoek heeft een X-Webhook-Delivery-ID. Bewaar de event_id van de events die je verwerkt hebt en sla een event over dat je al kent.
Het geheim vervangen
Na het opnieuw genereren tekent het vorige geheim nog 24 uur mee: de header bevat dan een tweede v1. Een ontvanger met het oude geheim keurt leveringen dus nog goed, zodat je het nieuwe geheim kunt uitrollen zonder events te missen. Via de API kies je de overlap met overlap_hours, van 0 tot 168. De overlap geldt alleen voor X-BuiltSign-Webhook-Signature: de oude X-Webhook-Signature gebruikt direct het nieuwe geheim.
POST /v1/webhooks/{webhook_id}/test Stuurt een ondertekend test-event met "test": true en de header X-Webhook-Test.POST /v1/webhooks/deliveries/{delivery_id}/redeliver Stuurt een opgeslagen levering opnieuw, met dezelfde body en event_id.Zero-dependency TypeScript client voor Node.js ≥ 18. Dekt elk API-endpoint met volledige type-veiligheid en ingebouwde retry-logica.
npm install @builtsign/nodeTypeScript · Node.js ≥ 18 · Zero dependencies
import fs from "node:fs/promises";
import BuiltSign from "@builtsign/node";
const builtsign = new BuiltSign({ apiKey: process.env.BUILTSIGN_API_KEY! });
// 1. Upload a PDF
const { document_id } = await builtsign.documents.upload(
await fs.readFile("contract.pdf"),
"contract.pdf",
);
// 2. Create a signing request
const request = await builtsign.signingRequests.create({
document_id,
signers: [
{ name: "Jan de Vries", email: "jan@example.com", order: 1 },
{ name: "Lisa Smit", email: "lisa@example.com", order: 2 },
],
signing_order: "sequential",
expires_in_days: 7,
auto_send: false,
});
// 3. Place signature fields
await builtsign.signingRequests.addFields(request.id, [{
signer_id: request.signers[0].id,
field_type: "signature",
page_number: 1,
x_percent: 60,
y_percent: 82,
width_percent: 30,
height_percent: 8,
}]);
// 4. Send
await builtsign.signingRequests.send(request.id);| Resource | Methods |
|---|---|
| builtsign.documents | upload(), uploadLarge(), list(), iterate(), get(), delete() |
| builtsign.signingRequests | create(), list(), iterate(), get(), events(), send(), cancel(), addFields(), download(), auditCertificate(), remind(), reactivate(), embeddedUrl() |
| builtsign.templates | list(), create(), get(), update(), delete(), send() |
| builtsign.contacts | list(), create(), delete() |
| builtsign.webhooks | list(), get(), create(), update(), delete(), regenerateSecret(), getDeliveries(), test(), redeliver(), verify() |
| builtsign.events | list(), iterate() |
| builtsign.bulk | send(), getJob() |
| builtsign.usage | get() |
| builtsign | me() |
Wat er per versie veranderd is, staat in CHANGELOG.md van het pakket.
Laat AI-assistenten documenten uploaden en ondertekentransacties versturen via het Model Context Protocol.
Het Model Context Protocol (MCP) is een open standaard van Anthropic waarmee AI-assistenten zoals Claude externe tools kunnen aanroepen. De BuiltSign MCP-server biedt 15 tools zodat een AI je ondertekenwerkstroom van begin tot eind kan beheren.
# Claude Code
claude mcp add builtsign \
-e BUILTSIGN_API_KEY=bs_xxxxxxxxxxxxxxxxxxxxxxxx \
-e BUILTSIGN_API_URL=https://api.builtsign.com \
-- npx -y @builtsign/mcp{
"mcpServers": {
"builtsign": {
"command": "npx",
"args": ["-y", "@builtsign/mcp"],
"env": {
"BUILTSIGN_API_KEY": "bs_xxxxxxxxxxxxxxxxxxxxxxxx",
"BUILTSIGN_API_URL": "https://api.builtsign.com"
}
}
}
}| upload_document | lees een lokale PDF en upload hem |
| list_documents | |
| create_signing_request | concept aanmaken |
| create_and_send_signing_request | aanmaken en direct versturen |
| send_signing_request | bestaand concept versturen |
| get_signing_request | status opvragen |
| list_signing_requests | met statusfilter en cursor-paginatie |
| cancel_signing_request | |
| send_reminder | |
| list_templates | |
| send_from_template | rollen koppelen aan ondertekenaars |
| list_events | volledige audit trail met cursor-paginatie |
Koppel je eigen AI-pipeline aan BuiltSign. Na elke voltooide ondertekening ontvang je een webhook met een downloadlink (15 minuten geldig) naar het getekende PDF. Verwerk het volledig op je eigen infrastructuur.
Document getekend
Alle ondertekenaars voltooien het ondertekteningsverzoek. BuiltSign genereert de definitieve PDF en slaat die beveiligd op in S3.
Webhook verstuurd
BuiltSign stuurt direct een JSON-payload naar jouw endpoint, inclusief een presigned URL om het getekende PDF te downloaden (15 minuten geldig).
Jouw AI verwerkt
Jouw pipeline downloadt de PDF en doet wat nodig is: data extraheren, archiveren, workflows starten. BuiltSign ziet de output nooit.
Contractgegevens (namen, bedragen, datums) extraheren en naar CRM of ERP sturen
Vervolgworkflows starten: onboarding, provisioning, compliance-controles
Slack-meldingen sturen met een AI-samenvatting van het getekende document
Getekende contracten in een RAG-pipeline laden voor interne documentzoekopdrachten
BYOAI wordt per organisatie ingesteld. Je geeft een HTTPS-endpoint op en genereert een signing secret.
Ga naar Instellingen → Ontwikkelaars → Bring Your Own AI
Voer je HTTPS-endpoint URL in en genereer een signing secret
Deploy je webhook-handler. Gebruik het signing secret om elk verzoek te verifiëren (zie code hieronder)
BuiltSign stuurt een POST-verzoek met deze JSON-body. De signed_pdf_url is een presigned S3-URL die 15 minuten geldig is.
{
"event": "document.signed",
"signing_request_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"document_name": "Koopcontract 2026.pdf",
"completed_at": "2026-06-16T10:30:00Z",
"signed_pdf_url": "https://s3.amazonaws.com/builtsign/signed/...?X-Amz-Expires=900",
"expires_in": 900,
"signers": [
{
"name": "Jan de Vries",
"email": "jan@example.com",
"signed_at": "2026-06-16T10:28:00Z"
},
{
"name": "Lisa Smit",
"email": "lisa@example.com",
"signed_at": "2026-06-16T10:30:00Z"
}
]
}signed_pdf_url verloopt na 15 minuten. Download de PDF direct in je handler.
Verifieer X-BuiltSign-Signature bij elk verzoek. Weiger alles wat niet overeenkomt.
Verifieer altijd de HMAC-SHA256-handtekening voordat je de payload verwerkt. Gebruik de ruwe request-body (geen geparseerde JSON) voor de HMAC-berekening.
import crypto from "crypto";
import express from "express";
app.post("/ai-webhook", express.raw({ type: "application/json" }), async (req, res) => {
// 1. Verify the signature
const signature = req.headers["x-builtsign-signature"];
const secret = process.env.BUILTSIGN_AI_SECRET;
const expected = crypto
.createHmac("sha256", secret)
.update(req.body) // raw Buffer: do NOT parse first
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
// 2. Download the signed PDF (URL valid 15 minutes)
const pdfResponse = await fetch(payload.signed_pdf_url);
const pdfBuffer = await pdfResponse.arrayBuffer();
// 3. Send to your AI pipeline
await yourAiPipeline.process({
documentName: payload.document_name,
pdf: Buffer.from(pdfBuffer),
signers: payload.signers,
completedAt: payload.completed_at,
});
res.sendStatus(200);
});| Header | Value |
|---|---|
| Content-Type | application/json |
| X-BuiltSign-Signature | HMAC-SHA256 hex digest of raw body |
| X-BuiltSign-Event | document.signed |
Klaar om je AI te koppelen?
Stel je endpoint in via Instellingen. Klaar in minder dan 2 minuten.
Laat ondertekenaars documenten direct in jouw applicatie ondertekenen. Geen doorverwijzing naar builtsign.com.
Een admin zet de origin van je app, zoals https://app.example.com, op de embed-allowlist onder Instellingen > Integraties (maximaal 20). Alleen die origins kunnen de tekenpagina in een iframe laden. Voor ontwikkelen werkt http://localhost.
Roep embeddedUrl() (POST /v1/signing-requests/:id/embedded-url) aan vanuit je backend met de allowed origin van je frontend. Je krijgt per ondertekenaar een URL die 60 minuten in een iframe te laden is. Behandel hem als geheim: wie hem heeft, kan tekenen.
const { signers } = await builtsign.signingRequests.embeddedUrl(
request.id,
{
allowed_origin: "https://yourapp.com",
// Optional: where the iframe goes after signing or declining
return_url: "https://yourapp.com/contracts/123/done",
},
);
// One URL per signer, valid for 60 minutes
const signerUrl = signers[0].embedded_url;return_url is optioneel. Na tekenen of weigeren gaat het iframe daarheen, met builtsign_event=signed of builtsign_event=declined en builtsign_request_id erbij. De origin moet op je allowlist staan; BuiltSign controleert dat bij het maken van de URL en bij elke keer laden.
Installeer @builtsign/embed en geef de URL door aan BuiltSignEmbed. Die maakt het iframe, accepteert alleen berichten van dat iframe en van de BuiltSign-origin, en roept je callbacks aan.
npm install @builtsign/embedimport { BuiltSignEmbed } from "@builtsign/embed";
const embed = new BuiltSignEmbed({
container: "#signing-container", // <div id="signing-container">
embeddedUrl: signerUrl,
onReady: (e) => console.log("Ready", e.requestId),
onSigned: (e) => {
console.log("Signed", e.requestId, e.documentName);
embed.close();
},
onDeclined: (e) => {
console.log("Declined", e.reason);
embed.close();
},
onError: (e) => {
console.error(e.code, e.message);
if (!e.recoverable) embed.close();
},
});
embed.open();| Event | Wanneer |
|---|---|
builtsign:ready | De pagina is geladen en kan getekend worden. Velden: requestId, signerId, signerStatus, requestStatus. |
builtsign:signed | De ondertekenaar heeft getekend. Velden: requestId, signerId, documentName. |
builtsign:declined | De ondertekenaar heeft geweigerd. Velden: requestId, signerId, reason. |
builtsign:error | De pagina kan niet getekend worden, of een actie mislukte. Velden: code, message, recoverable, requestId, signerId. |
Foutcodes: invalid_link, load_failed, embed_session_invalid, signing_request_expired, signing_request_cancelled, signing_request_completed, signer_declined, sign_failed en decline_failed. Bij recoverable: true kan de ondertekenaar het in het iframe opnieuw proberen. De SDK voegt ready_timeout toe als er binnen 30 seconden geen ready komt, meestal omdat de URL verlopen is of je pagina niet de allowed origin is. De pagina stuurt elk bericht alleen naar je allowed origin, nooit naar *.
// Without the SDK: check origin and source yourself
window.addEventListener("message", (event) => {
if (event.origin !== "https://builtsign.com") return;
if (event.source !== iframe.contentWindow) return;
if (event.data?.type === "builtsign:signed") {
// event.data.requestId, event.data.signerId
}
});Bevestig op je server
De events komen uit de browser. Bevestig een handtekening met de webhook signer.signed of document.signed, of met GET /v1/signing-requests/:id, voordat je erop handelt.
De tekenpagina heeft geen cookies of lokale opslag nodig om te tekenen, dus hij werkt ook als de browser die in iframes van derden blokkeert. Zet geen sandbox-attribuut op het iframe. De optionele ID-check gebruikt de camera; de SDK staat die toe op het iframe.
v1 blijft stabiel. Elke wijziging staat in de changelog.
HTTP/1.1 200 OK
Deprecation: @1793577600
Sunset: Mon, 01 Feb 2027 00:00:00 GMT
Link: <https://builtsign.com/developers/changelog>; rel="deprecation"; type="text/html",
<https://builtsign.com/developers/changelog>; rel="sunset"; type="text/html"Op dit moment is er geen endpoint uitgefaseerd.
Heb je white-label branding, een eigen e-maildomein of contractafspraken nodig? BuiltSign is een SaaS-product, geen installatie vereist. Neem contact met ons op voor enterprise-onboarding, maatwerkcontracten en geavanceerde integraties.
Elke fout geeft een JSON-body met een error-object met code, message en request_id. De code is machineleesbaar; noem de request_id als je contact opneemt met support.
{
"error": {
"code": "quota_exceeded",
"message": "Not enough SMS left this period (100/100 used, 2 needed).",
"request_id": "req_4e092e9aa07f4103a14f",
"details": { "resource": "sms", "used": 100, "limit": 100, "needed": 2 }
}
}| Status | Omschrijving |
|---|---|
400 | Ongeldig verzoek, ongeldige invoer of schending van een beperking |
401 | Niet geautoriseerd, ontbrekende of ongeldige API-sleutel |
402 | Betaling vereist, het plan dekt deze verzending niet (proef voorbij, transactielimiet, sms-bundel) of te weinig tegoed |
403 | Verboden, onvoldoende rechten of planbeperking |
404 | Niet gevonden, bron bestaat niet of behoort tot een andere organisatie |
409 | Conflict, actie niet toegestaan in de huidige status |
410 | Weg, de getekende PDF is na de bewaartermijn verwijderd |
413 | Te groot, het bestand is groter dan de limiet (tot 1 GB via POST /v1/uploads) |
422 | Onverwerkbaar, validatiefout (controleer velddetails) |
429 | Te veel verzoeken, limiet overschreden |
502 | Upstream-fout, S3 of e-mailprovider tijdelijk niet beschikbaar |
503 | Even niet beschikbaar, er is niets uitgevoerd: probeer opnieuw na Retry-After seconden |
Reageer op error.code, niet op de melding. Er kunnen codes bijkomen; behandel een onbekende code volgens zijn HTTP-status. Veel fouten hebben ook details, bijvoorbeeld used en limit.
| error.code | Omschrijving |
|---|---|
validation_error | 422 of 400: een veld ontbreekt of is ongeldig. De veldfouten staan in detail. |
auth_api_key_invalid | 401: de sleutel is onbekend, ingetrokken of verlopen. |
auth_scope_missing | 403: de sleutel mist het recht (read of write). |
feature_not_in_plan | 403: de functie zit niet in je plan. details zegt welk plan hem heeft. |
sandbox_not_supported | 403: niet beschikbaar met een sandbox-sleutel, zoals opgeslagen contacten. |
embed_origin_not_allowed | 403: de origin voor ingebed tekenen staat niet op je lijst. Voeg hem toe onder Instellingen, Integraties. |
quota_exceeded | 402 of 403: een plangrens is bereikt. details.resource zegt welke (signing_requests, sms, storage, document_volume). |
insufficient_credits | 402: te weinig tegoed voor ID-checks. |
payload_too_large | 413: het bestand is groter dan de limiet. details.limit bevat die. |
signing_request_not_found | 404: geen transactie met dit id in je organisatie en modus. |
document_not_found | 404: geen document met dit id in je organisatie en modus. |
signing_request_wrong_status | 409: kan niet in de huidige status, bijvoorbeeld het certificaat van een open transactie. |
document_not_ready | 409: de getekende PDF of het certificaat wordt nog gemaakt. Probeer opnieuw na Retry-After. |
document_blocked | 422: de malwarescan heeft dit document geweigerd. Upload een schoon bestand. |
document_has_open_request | 409: het document zit in een open transactie. Annuleer die eerst. |
document_has_evidence | 409: het document bevat bewijs dat 7 jaar bewaard blijft. |
document_file_removed | 410: het bestand is na de bewaartermijn verwijderd. |
template_name_exists | 409: je hebt al een actief sjabloon met deze naam. |
idempotency_key_reused | 422: deze Idempotency-Key is voor een ander verzoek gebruikt. |
idempotency_key_in_use | 409: de eerste aanroep met deze sleutel loopt nog. Probeer opnieuw na Retry-After. |
rate_limit_exceeded | 429: limiet per minuut bereikt. Probeer opnieuw na Retry-After. |
daily_limit_exceeded | 429: daglimiet voor API-aanroepen bereikt. Die gaat om middernacht UTC opnieuw in; probeer niet opnieuw voor Retry-After. |
service_unavailable | 503: even niet beschikbaar, er is niets uitgevoerd. Probeer opnieuw na Retry-After. |