Browse developers
Start here
Working with the API
Receiving events
Proposals
Reference
- Developers
- Working with the API
Errors and how to handle them
The shape of every error we return, the codes you can branch on, and the one that catches most first requests.
Last updated
Every error we return has the same five fields, whatever went wrong:
{
"statusCode": 404,
"message": "Envelope not found",
"error": "Not Found",
"timestamp": "2026-08-26T09:41:12.884Z",
"path": "/v1/envelopes/3b7d0e11-4c22-4f8a-9a1e-8d2c4b5e6f70"
}
statusCode repeats the HTTP status. error is the standard reason phrase for that status.
timestamp and path are there so a support conversation has something to look up. message is a
string, or an array of strings when a request body failed validation. Some errors add further keys
beside these five, and that is how a machine-readable code arrives.
The one that catches most first requests
An unrecognised property in the body is a 400. We validate against the exact shape a route
accepts and refuse anything else rather than ignoring it:
{
"statusCode": 400,
"message": ["property recipientEmail should not exist"],
"error": "Bad Request",
"timestamp": "2026-08-26T09:41:12.884Z",
"path": "/v1/envelopes"
}
This is far and away the most common reason a first request fails, and the cause is nearly always a field name that is close but not right, or a field copied from a different route. Read the property name in the message and compare it against the API reference; the answer is in the message itself.
Refusing is deliberate. Silently dropping a field you thought you had set is how an envelope goes out with no expiry date and nobody finds out for a month.
Validation failures
Anything else the body gets wrong arrives the same way, one string per problem:
{
"statusCode": 400,
"message": [
"subject must be a string",
"documentIds must contain at least 1 elements",
"recipients.0.email must be an email"
],
"error": "Bad Request",
"timestamp": "2026-08-26T09:41:12.884Z",
"path": "/v1/envelopes"
}
The dotted path in each entry addresses the offending value inside your body, so
recipients.0.email is the email of the first recipient.
Statuses
| Status | What it means for you |
|---|---|
400 | The request is malformed or a value is unacceptable. Fix it; retrying unchanged will fail again |
401 | The credential is missing or not accepted. See Authentication |
403 | The credential is real but not allowed to do this, or the account's plan does not include it |
404 | No such resource, or none your account may see. The two are the same answer on purpose |
409 | The request was well formed but the resource has moved on, usually a race. Re-read and decide |
413 | The file is larger than we accept |
429 | You are going too fast. Back off using Retry-After: see Rate limits |
5xx | Ours. Retry with backoff, using an idempotency key for anything that creates something |
Codes you can branch on
Most refusals carry prose in message and nothing more. Branch on the status code, and on
code where it is present. Never match on the text of message: it is display copy as well as
diagnosis, it gets reworded, and a rewording should not break your integration.
These are the code values in use today.
code | Status | Meaning |
|---|---|---|
feature-not-enabled | 403 | The account's plan does not include this. Carries a feature key naming it |
subscription-read-only | 403 | The account can read but not write at present |
proposals-access-denied | 403 | This caller may not reach proposals |
no-cecursign-account | 403 | The identity presented has no CecurSign account |
ACTING_USER_REQUIRED | 400 | This write must name the person it acts for. Send X-Acting-User |
ACTING_USER_NOT_PROVISIONED | 400 | The X-Acting-User value matches no active user in this account |
INVALID_IDEMPOTENCY_KEY | 400 | The Idempotency-Key is outside 10 to 255 characters |
IDEMPOTENCY_CONFLICT | 409 | The same key is still in flight. Wait, then retry |
pricing-invalid | 400 | The pricing engine refused the figures. Carries pricingCode, and usually field and lineIndex |
proposal-transition-invalid | 409 | Illegal state change. Carries from and to |
proposal-not-editable | 400 | The proposal has left draft and its content is frozen. Carries status |
daily-quota-exceeded | 429 | The account's daily API allowance is spent |
Pricing refusals in detail
A pricing-invalid body carries a second discriminator, because the kind of refusal and the rule
that was broken are different questions:
{
"code": "pricing-invalid",
"pricingCode": "PRECISION_EXCEEDED",
"field": "amount",
"lineIndex": 2,
"statusCode": 400,
"message": "Pricing could not be computed: line 3 amount has more than 2 decimal places",
"error": "Bad Request",
"timestamp": "2026-08-26T09:41:12.884Z",
"path": "/v1/proposals/8a2b.../services"
}
pricingCode is one of INVALID_INPUT_SHAPE, INVALID_CURRENCY, INVALID_NUMBER,
PRECISION_EXCEEDED, NEGATIVE_AMOUNT, AMOUNT_OUT_OF_RANGE, INVALID_VAT_RATE,
INVALID_FEE_TYPE, INVALID_BILLING_FREQUENCY or INVALID_ESTIMATE_RANGE.
lineIndex is zero-based and the prose in message is one-based. Both are there because both
are needed: the index addresses the array you are holding, the sentence is what you show the
author. lineIndex: 2 is the line message calls line 3. field and lineIndex are omitted
rather than sent as null when the engine did not supply them, so treat both as optional.
Retrying
429 and 5xx are worth retrying; 4xx otherwise is not, because the same request will be
refused the same way. For anything that creates a resource, retry with the same
idempotency key, so a request that timed out after it succeeded does not
give your client two copies of the same contract.