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"No endpoint is deprecated at the moment.
2026-10-23
POST /v1/signing-requests (also with auto_send: true), POST /v1/signing-requests/{request_id}/send, POST /v1/templates/{template_id}/send and POST /v1/bulk-send now refuse a document that the malware scan rejected, the same check the dashboard runs when a request is created: 422 with the new error code document_blocked, and nothing is created or sent. A document whose scan has not finished yet can still be used. When the scan rejects the document of a queued bulk job later, its remaining recipients fail with the same message.2026-10-22
GET /v1/signing-requests/{request_id}/audit-certificate returns the audit certificate (PDF) of a completed, cancelled or expired request. Earlier: 409 with signing_request_wrong_status. Right after completion, while the evidence is still being sealed: 409 with document_not_ready and Retry-After. Scope read.GET /v1/documents/{document_id} returns one document with its signing request ids and removal: what a delete would do now (delete, archive or blocked). Like the list, a key only reaches documents of its own mode (sandbox or live); any other id returns 404 with document_not_found.DELETE /v1/documents/{document_id} deletes a document without evidence, together with its draft requests that were never signed. A document in an open request returns 409 with document_has_open_request; one with signatures or other evidence (kept for 7 years) returns 409 with document_has_evidence. Scope write.GET /v1/signing-requests, GET /v1/documents and GET /v1/events: pass the next_cursor of a page as starting_after until has_more is false. The responses gain has_more and next_cursor. Offset paging works as before; offset together with starting_after returns 422. On /v1/events, after_id keeps working.X-RateLimit-Daily-Limit, X-RateLimit-Daily-Remaining and X-RateLimit-Daily-Reset (next midnight UTC, Unix time).idempotency_key_reused and idempotency_key_in_use.POST /v1/uploads starts an upload and returns an upload id, a part_size and a presigned PUT URL per part. POST /v1/uploads/{upload_id}/parts gives fresh URLs (they are valid for 15 minutes, and the first response holds at most 100). POST /v1/uploads/{upload_id}/complete takes the part numbers with their ETags and returns the same document as POST /v1/documents/upload, including the document.uploaded webhook. POST /v1/uploads/{upload_id}/abort stops an upload. Start and complete accept an Idempotency-Key.%PDF; a file that fails is deleted. Sandbox keys can upload up to 100 MB per file (413 payload_too_large above that, with details.sandbox: true) and nothing counts toward the quota.@builtsign/node): documents.uploadLarge(pathOrStream, { partSize, concurrency }) does the whole upload flow, with parallel parts and a retry per part. Also new: webhooks.verify() with the timestamp tolerance, webhooks.test(), webhooks.redeliver(), webhooks.regenerateSecret({ overlap_hours }), documents.get(), documents.delete(), signingRequests.auditCertificate(), me(), cursor paging with iterate(), the return_url type on embeddedUrl() and details on every BuiltSignError.maxRetryAfter (seconds, default 60). A 429 with a longer Retry-After comes back as BuiltSignRateLimitError at once instead of being slept on; daily_limit_exceeded is never retried.daily_limit_exceeded (429) for the daily API call limit. The minute limit keeps rate_limit_exceeded.POST /v1/signing-requests/{id}/embedded-url accepts an optional return_url: after the signer signs or declines, the iframe navigates there with builtsign_event (signed or declined) and builtsign_request_id added. Its origin must be on the embed allowlist; otherwise the call returns 400 with validation_error. The response echoes return_url.builtsign:ready (with requestId, signerId, signerStatus, requestStatus) and builtsign:error (with code, message, recoverable) to the parent window. Every event carries requestId and signerId.@builtsign/embed 0.2.0: container (element or selector), onReady on builtsign:ready, onError with code and recoverable, a ready_timeout error, and a script-tag build. It checks event.source as well as event.origin.event_id (evt_...). It stays the same on every retry and redelivery, so deduplicate on it. Every request also carries X-Webhook-Delivery-ID: the id of the delivery, kept on automatic retries, new on a redelivery.X-BuiltSign-Webhook-Signature: t=<unix seconds>,v1=<hex>: HMAC-SHA256 with the webhook secret over "{t}.{raw body}". Check it in constant time and reject a t more than 5 minutes from your clock; that stops a captured request from being replayed. @builtsign/node 0.4 does this in webhooks.verify(payload, header, secret, { tolerance }).POST /v1/webhooks/{webhook_id}/test sends a signed test event ("test": true, header X-Webhook-Test: true). Optional body {"event_type": "..."}. It is not stored as a delivery and does not count as a failure.POST /v1/webhooks/deliveries/{delivery_id}/redeliver sends a stored delivery again with the same body and event_id, as a new delivery. Only for deliveries of your own organization, of a webhook in the key's mode (sandbox or live); without the Enterprise plan only sandbox deliveries. A successful redelivery ends the retries still planned for the original.GET /v1/webhooks/{webhook_id}/deliveries, redeliver) now include webhook_id, event_id, attempt_number and next_retry_at./v1 stays stable, breaking changes are announced at least 90 days ahead, and new fields, new webhook events and new error codes are not breaking.Deprecation (RFC 9745), a Link to this changelog (rel="deprecation") and, once its date is set, Sunset (RFC 8594). The spec marks it deprecated: true with the dates in x-deprecation. No endpoint is deprecated yet. The headers are exposed to browsers (CORS).baseUrl and apiKey; create and send calls get a fresh Idempotency-Key.webhooks.verify() and verifyWebhook() only accept X-BuiltSign-Webhook-Signature (t=...,v1=...). The plain hex value of X-Webhook-Signature that 0.3 checked is refused with invalid_signature. Pass the new header when you upgrade.POST /v1/zapier/subscribe and DELETE /v1/zapier/subscribe/{subscription_id} now need scope write; a key with only read gets 403 with auth_scope_missing. They change your account, so read was too little. GET /v1/zapier/samples still takes read.GET /v1/documents lists only documents uploaded with a key of the same mode. POST /v1/signing-requests, POST /v1/templates, PATCH /v1/templates/{template_id} and POST /v1/templates/{template_id}/send return 404 with document_not_found for a document of the other mode. A sandbox request on a live document is no longer possible.GET, PATCH and DELETE on a template or webhook of the other mode, POST /v1/templates/{template_id}/send, POST /v1/bulk-send, POST /v1/webhooks/{webhook_id}/regenerate-secret and GET /v1/webhooks/{webhook_id}/deliveries return 404. Templates and webhooks made in the dashboard are live.GET /v1/bulk-jobs/{bulk_job_id} returns 404 for a job of the other mode.GET, POST and DELETE on /v1/contacts with a sandbox key return 403 with the new error code sandbox_not_supported. Saved contacts are the organization's real address book.Idempotency-Key is now tied to the request. The same key with a different body or on another endpoint returns 422 with idempotency_key_reused instead of the first response. A retry with the same key while the first call is still running waits a few seconds for its answer; if it still runs after that, 409 with idempotency_key_in_use and Retry-After. Before, both calls ran. If the key store is unreachable, a call with an Idempotency-Key returns 503 with Retry-After and does nothing.auto_send: true on POST /v1/signing-requests, the send limits (trial, transaction limit, SMS bundle, ID check credits) are checked before anything is created. A 402 no longer leaves a draft behind (it did since 2026-10-12), so a retry does not create a second one.POST /v1/signing-requests now limits expires_in_days to 1 to 365, signers to at most 50 and message to at most 2000 characters. Outside these: 422 with validation_error. A negative expires_in_days used to create a request that had already expired.Retry-After now gives the seconds until midnight UTC instead of a fixed 3600.503 with Retry-After instead of 500. Nothing was done; retry the call.GET /v1/signing-requests and GET /v1/documents now order items with the same timestamp by id, so offset pages no longer shift between calls.401, 403, 404, 409, 422, 429 and 503 per operation, with examples.POST /v1/signing-requests, POST /v1/templates and PATCH /v1/templates/{template_id} with a document_id, POST /v1/templates/{template_id}/send and POST /v1/bulk-send now return 409 with document_not_ready for a document whose upload is not completed (details.status: "uploading"), or that is still being processed (with Retry-After). A document whose file is not in storage returns 409 with document_file_removed. POST /v1/signing-requests/{request_id}/send refuses a draft on an unfinished upload the same way.POST /v1/uploads/{upload_id}/complete needs every part from 1 up to the last one, and the joined file must be exactly the file_size given at the start. A missing part returns 400 with upload_incomplete and changes nothing; a joined file of another size returns 400 with upload_incomplete and is deleted.POST /v1/uploads/{upload_id}/abort and the rejection on complete no longer delete the document: 409 with document_in_use and details.references. On complete the rejected file itself is still deleted.429 with daily_limit_exceeded instead of rate_limit_exceeded. Its Retry-After runs to midnight UTC.POST /v1/documents/upload with a file over 50 MB now returns 413 with error code payload_too_large (it was 400 with validation_error). details.limit holds the limit and details.use points to /v1/uploads. A request whose Content-Length is over the limit is refused before the body is read.POST /v1/documents/upload now applies the plan limits of the dashboard: the file size of the plan (413 payload_too_large), the storage and the monthly document volume (403 quota_exceeded, with details.resource storage or document_volume). The upload counts toward the monthly volume when your plan has one. Sandbox keys are not affected.POST /v1/webhooks/{webhook_id}/regenerate-secret keeps the previous secret signing for 24 hours: X-BuiltSign-Webhook-Signature then carries a v1 for each secret. Optional body {"overlap_hours": 0-168}; 0 revokes the old secret at once. The response and GET /v1/webhooks show previous_secret_expires_at while the overlap runs. The overlap is only in X-BuiltSign-Webhook-Signature: the deprecated X-Webhook-Signature uses the new secret at once.X-Webhook-Secret; the test event is signed like a real delivery.X-Webhook-Signature (HMAC-SHA256 over the body only, with the current secret) is still sent so existing receivers keep working, but it has no timestamp. Switch to X-BuiltSign-Webhook-Signature.Proposed removal: not before 2027-01-20, 90 days after this notice. That date is not final yet; we confirm it here first, and the header keeps being sent until then.
PATCH /v1/templates/{template_id} with a document_id now checks that the document belongs to your organization (and, with a sandbox key, was uploaded with a sandbox key). Otherwise it returns 404 with document_not_found and nothing is saved. It used to accept any document id. The same check now applies to PATCH /api/templates/{template_id} in the dashboard, and bulk jobs only use a template and document of the job's own organization.PATCH /v1/templates/{template_id} now checks signing_level against your plan, like the dashboard: aes or qes without qualified signatures in the plan returns 403 with feature_not_in_plan.trigger_count and failure_count could miss counts when deliveries ran at the same time. They are now raised in one database statement.builtsign:signed sent the request status in requestId. It now carries the signing request ID.2026-10-13
POST /v1/templates with a name the owner already uses for an active template, and PATCH /v1/templates/{template_id} that renames a template (or sets is_active: true) to such a name, return 409 with error code template_name_exists and the existing template's id in details.template_id. Nothing is saved.Templates that already share a name stay as they are and can still be updated as long as the name does not change.
2026-10-12
POST /v1/signing-requests, POST /v1/templates/{template_id}/send and POST /v1/bulk-send always create the request with phone_verification_required: true, whatever the body or the template says. The field stays in the schemas for compatibility. Requests created before this change keep their setting.phone_verification_required: true on POST /v1/signing-requests still returns 400 with validation_error. A template's phone_verification_required is now ignored, so POST /v1/templates/{template_id}/send and POST /v1/bulk-send with such a template no longer return 400 with a sandbox key; the sandbox request gets no SMS code.phone locks the SMS code to that number. Without a phone the signer enters their own number when signing.sms_invite: true and a phone (paid plans), since 2026-10-11. A phone alone no longer sends the link by SMS.auto_send: true, or POST /v1/signing-requests/{id}/send) now checks the SMS bundle. When the bundle plus purchased SMS credits cannot cover the codes and SMS invites, the call returns 402 with quota_exceeded and details.resource: "sms" (with used, limit and needed). With auto_send: true the request stays a draft.POST /v1/bulk-send with auto_send refuses the whole job this way before anything is queued. A recipient that no longer fits while the job runs fails with that message in error_message.
phone_verification_required; it was always false.POST /v1/bulk-send now keeps each signer's phone and sms_invite. They were dropped before the job ran.2026-10-05
POST /v1/contacts now checks the format of email and returns 422 with error code validation_error for an invalid address such as jan@gmailcom (no dot in the domain). It used to only check for an @, so such a contact was saved and failed later when it was used as a signer.A name of only spaces is now also rejected with 422; leading and trailing spaces are removed before the length check.
The call is still an upsert: an email address that already exists updates that contact.
name that is empty or only spaces now returns 422 with error code validation_error on POST /v1/signing-requests, POST /v1/templates/{template_id}/send and POST /v1/bulk-send. Leading and trailing spaces are removed.On POST /v1/signing-requests a signer name longer than 255 characters is now also rejected with 422 before anything is saved.
2026-10-04
403 with error code feature_not_in_plan and details (feature, required_plan, upgrade_plan, plan). This affects:POST /v1/bulk-send without bulk automation: was 403 with quota_exceeded.signing_level aes or qes while qualified signatures are enabled but not in the plan: was 422. When qualified signatures are not enabled at all the call still returns 422.POST /v1/signing-requests with identity_verification_required and auto_send on a plan without identity checks: was accepted without a check, now 403 (feature: identityVerification). Not enough credits is still 402 with insufficient_credits.GET /v1/usage: storage.limit is now in bytes, the same unit as storage.used. It used to be in GB, so used and limit could not be compared.2026-10-02
GET /v1/me returns the organization and the name of the API key used for the call (organization_id, organization_name, api_key_name, sandbox, scopes). The key itself is never returned.GET /v1/signing-requests/{id}/download no longer returns the unsigned original while the signed PDF is still being prepared.Right after a request turns completed the call can return 409 with error code document_not_ready and a Retry-After header (in seconds). Retry after that delay.
An error without Retry-After is final: 409 with conflict when the signed PDF could not be generated (contact support), 410 with document_file_removed when it was removed after the retention period.
Wait for the document.signed webhook or event before downloading; signing_request.completed can arrive before the signed PDF exists.
POST /v1/documents/upload now checks the file content instead of only the .pdf extension.A file that does not start with a PDF header is rejected with 400 and error code validation_error.
2026-06-14
sandbox field on GET /v1/signing-requests, GET /v1/signing-requests/{id}, and POST /v1/signing-requests responses.Indicates whether the request was created with a sandbox API key. Sandbox requests skip real email delivery, SMS costs, and identity checks.
signing_request.reminded: fired when POST /v1/signing-requests/{id}/remind is called. Payload: signing_request_id, signers_reminded, sent_count.document.uploaded: fired when POST /v1/documents/upload succeeds. Payload: document_id, name, file_size, is_sandbox.member.invited: fired when a team member is invited via the dashboard. Payload: email, name, role, invited_by.@builtsign/mcp in packages/mcp-server/.Enables AI assistants (Claude, Cursor, etc.) to upload documents and send signing requests directly.
2026-06-10
{"error": {"code": "ERROR_CODE", "message": "...", "request_id": "..."}} instead of plain strings.GET /v1/events: new after_id parameter, response includes last_id and has_more.GET /v1/signing-requests filters: created_after, created_before, signer_email.GET /v1/signing-requests: sort (created_at | updated_at | expires_at) and order (asc | desc). Default: created_at desc.signing_request.cancelled webhook now fires correctly when POST /v1/signing-requests/{id}/cancel is called.document.signed event added to SigningEventType and included in Events API responses.signer.reviewed, identity.approved, identity.rejected registered.signing_request.cancelled webhook was not fired when cancelling via API v1.document.signed was missing from the events pull API.2026-05-19
POST /v1/bulk-send: send one template to many recipients in a single call. Progress through GET /v1/bulk-jobs/{bulk_job_id}.GET /v1/contacts: list contacts for your organisation.GET /v1/usage: monthly usage stats (requests used, limit, resets at).is_sandbox: true when creating an API key. Sandbox keys bypass email/SMS/identity, use a sandbox/ S3 prefix, and do not count against usage quotas.POST /v1/documents/upload and POST /v1/signing-requests.enterprise_api_config.2026-05-10
Initial v1 release.
POST /v1/documents/uploadGET /v1/documentsPOST /v1/signing-requests with auto_send flagGET /v1/signing-requestsGET /v1/signing-requests/{id}POST /v1/signing-requests/{id}/sendPOST /v1/signing-requests/{id}/cancelPOST /v1/signing-requests/{id}/remindPOST /v1/signing-requests/{id}/reactivateGET /v1/templatesPOST /v1/templates/{id}/sendGET /v1/eventsGET/POST /v1/webhooks with HMAC-SHA256 signaturesX-API-Key header (bs_ prefix)