BoldSign AI Assistant
Chat with the BoldSign AI AssistantIntegration errors and fixes
These are the errors developers and coding agents most often meet while building a BoldSign integration in the sandbox. Each one gives the exact response, why it happens, and the fix. For the meaning of each HTTP status code, see Possible errors and solutions.
Before you start: confirm the key and region
Make one read-only call before building anything:
curl -i https://api.boldsign.com/v1/brand/list -H "X-API-KEY: $BOLDSIGN_API_KEY"
200 means the key and the base URL match. A new sandbox account has no brands, so {"result":[]} is a normal answer.
401 Unauthorized on every call
Response: 401, on every endpoint.
Cause: the API key is wrong or revoked, or the base URL belongs to another region. A key from a US account gets 401 from api-eu.boldsign.com, api-ca.boldsign.com and api-au.boldsign.com.
Fix:
- Check the key under API > API Key in the account the key came from.
- Call
GET /v1/brand/liston each regional host:- US:
https://api.boldsign.com - EU:
https://api-eu.boldsign.com - CA:
https://api-ca.boldsign.com - AU:
https://api-au.boldsign.com
- US:
- Use the host that returns
200.
403 "Invalid Document ID" right after sending
Response:
{"error":"Invalid Document ID"}
This comes back from GET /v1/document/getEmbeddedSignLink or GET /v1/document/properties within a second or two of POST /v1/document/send returning that same documentId.
Cause: sending is asynchronous. The API returns the document ID at once, and the document becomes available a few seconds later; usually within a few seconds. The ID is not wrong.
Fix: retry the next call briefly, 3 to 5 times, 2 to 3 seconds apart, before treating it as an error. For a definite answer, listen for the Sent or SendFailed webhook.
async function retrySoon<T>(fn: () => Promise<T>, tries = 5): Promise<T> {
for (let i = 1; ; i++) {
try { return await fn(); }
catch (err: any) {
const status = err?.statusCode ?? err?.response?.status; // Node SDK HttpError or Axios error
if (i >= tries || (status !== 400 && status !== 403)) throw err;
await new Promise((r) => setTimeout(r, 2000 * i));
}
}
}
const { signLink } = await retrySoon(() => documentApi.getEmbeddedSignLink(documentId, signerEmail));
400 "Invalid template id." right after creating a template
Response:
{"error":"Invalid template id."}
This comes back from POST /v1/template/send?templateId=... within a second of POST /v1/template/create returning that template ID.
Cause: template creation is also asynchronous.
Fix: wait for the TemplateCreated webhook, or retry briefly as above. In production you normally send from a template created earlier, so this mostly affects tests and set-up scripts.
403 "Forbidden" that does not go away
Response:
{"error":"Forbidden"}
This comes back from GET /v1/document/properties for a document the same key sent, still minutes later.
Cause: the key cannot see that document. BoldSign returns the same 403 {"error":"Forbidden"} for a document ID that does not exist, so either the send failed after returning the ID (look for a SendFailed event), or the document belongs to another account, environment or team than the key.
Fix:
- In the BoldSign web app, open Documents in the account the key came from, and look for the document.
- Check API > API Key: the key's Environment must be the one you expect (Sandbox or Live).
- For integration and coding-agent testing, use a dedicated developer sandbox account (https://account.boldsign.com/signup?planId=1076).
429 Too Many Requests
Response: 429 with a Retry-After header.
Cause: the sandbox allows 50 requests per hour per account, counted across all keys and OAuth apps.
Fix:
- wait for
Retry-After; - use webhooks instead of polling
properties; - in tests, mock the API rather than calling it in a loop.
Your webhook endpoint rejects real BoldSign events
Symptom: your endpoint returns 401 (or your signature check fails) for events BoldSign really sent.
Causes:
- The HMAC was computed over the body alone. The signed message is the timestamp, a full stop, then the raw body.
- The signature was compared with the whole
X-BoldSign-Signatureheader, instead of withs0ands1. - The framework parsed the JSON before the check, so the bytes changed.
- The secret is not this webhook's own secret (each webhook has its own; see Reveal).
- The server clock is more than 5 minutes off, so the timestamp check fails.
Fix, as described in Verify webhook events:
import crypto from 'crypto';
export function verifyBoldSignSignature(rawBody: Buffer, header: string | undefined, secret: string): boolean {
if (!header || !secret) return false;
let t = '';
const signatures: string[] = [];
for (const part of header.split(',')) { // "t=1700000000, s0=ab12..., s1=cd34..."
const i = part.indexOf('=');
if (i < 0) return false;
const key = part.slice(0, i).trim(), value = part.slice(i + 1).trim();
if (key === 't') t = value; else if (/^s\d+$/.test(key)) signatures.push(value.toLowerCase());
}
if (!/^\d+$/.test(t) || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay protection
const expected = crypto.createHmac('sha256', secret).update(Buffer.concat([Buffer.from(`${t}.`), rawBody])).digest();
return signatures.some((s) => /^[0-9a-f]{64}$/.test(s) && crypto.timingSafeEqual(Buffer.from(s, 'hex'), expected));
}
The Node SDK has the same check in boldsign/dist/WebhookUtility (not exported from the package root). It throws RangeError if a signature has the wrong length, so wrap it in try/catch.
The webhook was added but BoldSign's check fails
Cause: when you add a webhook, BoldSign sends a request with the header X-BoldSign-Event: Verification, and your endpoint must answer 200. Endpoints that run the signature check first, or that look for the event type in the body, reject it.
Fix: check the header first and return 200 straight away:
app.post('/webhooks/boldsign', express.raw({ type: '*/*' }), (req, res) => {
if (req.get('X-BoldSign-Event') === 'Verification') return res.sendStatus(200);
// ...then verify the signature
});
The webhook handler cannot find the document in the event
Cause: the code reads payload.document. In the event payload, the subject is in payload.data, and data.object says what it is (document, template, senderIdentity, identityVerification).
Fix: read payload.event.eventType for the event and payload.data.documentId for the document. See Sample event data.
No webhook ever arrives during local development
Causes:
- BoldSign cannot reach
localhost. - BoldSign does not replay events from before the webhook was added.
Fix: expose the app first with cloudflared tunnel --url http://localhost:3000 (Listen for callback). Register the HTTPS address under API > Webhooks, then send a new document.
Text tags do not become fields
Causes:
useTextTagswas not set totrue.- The tags use another syntax.
- The tags are images, not text, in the PDF.
Fix: the syntax is {{fieldType|signerIndex|required}}, for example {{sign|1|*}} or {{date|1|*}}. Send with useTextTags: true and leave out formFields. Spaces inside the braces widen the field, and a larger font makes it taller. White text hides the tag on the signed copy. See Text tags.
"This document's state has been modified" when the signer presses Finish
Message: "This document's state has been modified after you had initiated the signing process so please reload the document to continue signing."
Cause: the signing page was opened within seconds of the send, while BoldSign was still processing the new document.
Fix: reload the page and sign again. In an app, open the signing page only after the Sent webhook arrives, or a few seconds after the send. A signer who opens the email is not affected.
Fields appear in the wrong place
Cause: bounds were given in PDF points. Bounds are measured from the top-left of the page in pixels at 96 per inch (1 point = 1.333 px), so point values land too high and too far left.
Fix: multiply point values by 1.333, or use text tags.
The signer receives no email
Causes:
disableEmails: truewas set. That is expected for embedded signing, where the signer signs only through the embedded link.- The address bounced.
Fix: for embedded signing, get a fresh link with getEmbeddedSignLink on each visit. For test signers, use <anyname>@boldsign.dev addresses.
Complete working examples
The tested starter recipes (send a document, embedded signing, send from a template, with the SDK version pinned) handle every case on this page. They are in the recipes/ folder of the BoldSign integration skill: https://github.com/boldsign/boldsign-esignature-skills