Skip to content
Browse developers

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

StatusWhat it means for you
400The request is malformed or a value is unacceptable. Fix it; retrying unchanged will fail again
401The credential is missing or not accepted. See Authentication
403The credential is real but not allowed to do this, or the account's plan does not include it
404No such resource, or none your account may see. The two are the same answer on purpose
409The request was well formed but the resource has moved on, usually a race. Re-read and decide
413The file is larger than we accept
429You are going too fast. Back off using Retry-After: see Rate limits
5xxOurs. 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.

codeStatusMeaning
feature-not-enabled403The account's plan does not include this. Carries a feature key naming it
subscription-read-only403The account can read but not write at present
proposals-access-denied403This caller may not reach proposals
no-cecursign-account403The identity presented has no CecurSign account
ACTING_USER_REQUIRED400This write must name the person it acts for. Send X-Acting-User
ACTING_USER_NOT_PROVISIONED400The X-Acting-User value matches no active user in this account
INVALID_IDEMPOTENCY_KEY400The Idempotency-Key is outside 10 to 255 characters
IDEMPOTENCY_CONFLICT409The same key is still in flight. Wait, then retry
pricing-invalid400The pricing engine refused the figures. Carries pricingCode, and usually field and lineIndex
proposal-transition-invalid409Illegal state change. Carries from and to
proposal-not-editable400The proposal has left draft and its content is frozen. Carries status
daily-quota-exceeded429The 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.