Tracing a call, and watching your usage
Two things every integration eventually builds for itself, and neither of them is your product. Here they are, so you do not have to.
Every response carries an id
Every answer this service gives — the API's, and the console's pages —
carries an X-Request-Id header:
X-Request-Id: req_9f2c6b1a4e7d40f0a1b2c3d4e5f60718
Log it next to your own request. It is the only thing that turns "a call failed this morning" into a row we can look at: it is written onto the generation itself, so quoting one identifies the exact job, its template, how long it took and what it answered.
On a failure you do not even have to keep the header. The same id is in the body, so an error you paste into a support message has already told us which call it was:
{
"code": "TEMPLATE_VERSION_NOT_ACTIVE",
"number": 1204,
"message": "version 2 of template 'contract' is not active",
"retryable": false,
"requestId": "req_9f2c6b1a4e7d40f0a1b2c3d4e5f60718"
}
The id is ours, minted per request. If you send an X-Request-Id
of your own we ignore it, and that is deliberate: two of your requests carrying
one id would be indistinguishable in our logs, which is the whole property the
id exists to provide. Record the one we return alongside your own correlation
id and you have both halves.
One call for what you have used
GET /api/v1/usage answers four
questions in one request: what your balance is, what operations cost you, what
you have spent, and which limits will refuse you.
curl https://your-host/api/v1/usage \
-H "Authorization: Bearer YOUR_API_KEY"
{
"organisation": { "id": "...", "name": "Acme",
"deliveryMode": "prepaid", // deprecated
"suites": { "documents": true, "signatures": true, "flow": true } },
"balance": { "credits": 4310, "overdraft": 0,
"rates": { "document": 5, "signature": 10, "sms": 2, "email": 0,
"seal": 1, "timestamp": 3 } },
"spent": { "windowDays": 30, "since": "...",
"credits": { "document": 640, "signature": 150 },
"operations": { "document": 128, "signature": 15 } },
"thisMonth": { "since": "...",
"documents": { "production": { "succeeded": 96, "failed": 2 },
"test": { "succeeded": 5, "failed": 0 } },
"documentsSucceeded": 101, "documentsFailed": 2, // deprecated
"credits": { "document": 480 } },
"testItems": { "used": 5, "limit": 20, "remaining": 15,
"resetsAt": "2026-10-16T00:00:00+00:00" },
"limits": { "requestsPerMinute": 30, "documentsPerMonth": null,
"jsonBytes": 10485760, "templateBytes": 26214400,
"assetBytes": 10485760, "maxPages": 5000,
"downloadLinks": { "defaultSeconds": 3600, "maxSeconds": 172800 },
"storedFilesDays": 30 },
"asOf": "..."
}
Everything under limits is a number that produces a refusal,
in the same units the refusal uses — so you can raise an alarm before a
429 rather than after one. It carries every row of the console's
Limits page: the largest JSON payload, template and uploaded file
(assetBytes: an image, an attachment or a PDF to sign), pages per
document, how long a download link lives by default and at most
(downloadLinks: a longer request is refused, never shortened) and how
many days an uploaded file is kept. balance.credits against
balance.rates tells you how many more of each operation you can
actually perform.
It belongs to your account, not to a product. Your balance is the same fact whether you bought composition, signature or the flow — so this endpoint keeps answering even when one of the three is switched off for your organisation, which is exactly the moment you want to look at it.
Production and test are never one number.
thisMonth.documents counts what you generated for real under
production and the isTest calls apart under
test; test calls spent no credit. The two older fields,
thisMonth.documentsSucceeded and documentsFailed,
have always added the two together; they are kept so a client that reads them
keeps working, are deprecated and removed in 2.0, and should be
replaced by documents.production.
testItems is today’s shared test pool:
used, limit, remaining, and
resetsAt, the next 00:00 UTC (the pool resets once a day, at a fixed
moment). The console shows the same split: lists and totals say
“1,240 documents · 37 test”, with a Production / Test filter,
and the dashboard says how long until the pool refills.
Times in this answer are UTC: Times and time zones.
Every organisation is prepaid: the balance is a budget that goes down
as you spend it and is refused at zero (QUOTA_EXCEEDED). Two
fields are deprecated and will be removed in 2.0, and are kept
only so a client that reads them keeps working:
organisation.deliveryMode is always the constant
"prepaid", and limits.documentsPerMonth is always
null because there is no monthly document cap any more. Do not
branch on either.
Times and time zones
Every timestamp in an API answer or a webhook is UTC, ISO 8601, and never changes with
your settings, so a program never has to guess a zone. What a person reads — your console, the emails,
the certificate of completion and the CSV exports — is written in the locale of your
organisation: its time zone and its way of writing a date (for example it-IT, Europe/Rome,
21/10/2026 14:30). We set the locale for your organisation from a fixed list; write to us to
change it. It replaced the separate Time zone and Date format settings, which had no effect.
- The certificate of completion states the local time and the UTC instant; an export carries the offset on every time.
- The signing invitation and every reminder say the day, time and time zone the link stops working.
- The line printed under each signature on the document (Signed by …) gives the time in your organisation's zone, with the zone named, and a date field carries your organisation's day.
- The daily test pool refills at 00:00 UTC; the console's Limits page shows that moment in your organisation's time (02:00 in Italy in summer, 01:00 in winter) and how long is left.
Which of your keys does what
Give each integration its own key, with a name that says what uses it (billing-export, crm-sync). Every authenticated request is counted against the key that made it, so the key table in your console (API keys) shows, per key, when it was last used, how many requests it made in the last 30 days and how many of those failed. That is how you find the integration that is erroring, the key nobody uses any more (revoke it), and the one that is about to expire.
What is counted is the request, its outcome and its route, never its
content: no payload, no address, no key. The counters are kept per key and per
day, are written every half minute or so, and are not an invoice — what a call
cost is in output.credits and in your
credit history. A request made with a key that is unknown, revoked or
expired belongs to no key and is not counted.
The whole lifecycle, in order
GET /api/v1/flow/{flowId}/events
answers the third question a flow raises. GET /api/v1/flow/{flowId}
says where it is; this says how it got there.
{
"flowId": "...", "schema": 1, "count": 7,
"events": [
{ "sequence": 1, "type": "composition.started", "occurredAt": "...", "recipient": null },
{ "sequence": 2, "type": "composition.completed", "occurredAt": "...", "recipient": null },
{ "sequence": 3, "type": "envelope.created", "occurredAt": "...", "recipient": null },
{ "sequence": 4, "type": "envelope.sent", "occurredAt": "...", "recipient": null },
{ "sequence": 5, "type": "recipient.viewed", "occurredAt": "...", "recipient": "anna@example.com" },
{ "sequence": 6, "type": "recipient.signed", "occurredAt": "...", "recipient": "anna@example.com" },
{ "sequence": 7, "type": "envelope.completed", "occurredAt": "...", "recipient": null }
]
}
Three subjects — composition, envelope and
recipient — and the second and third use the same words the
callback does, so envelope.completed means one thing wherever you
meet it.
sequence is a position, not an identifier: it
orders the list and lets you spot a gap. id is what
deduplicates.
Available while the flow is still open, which is the difference between this and the evidence bundle — that is the archival form of the same facts and exists only once the matter is settled, which is the one moment it is no use for debugging.
Deduplicating a callback
If you take webhook callbacks, the body carries an event block
so a retry cannot be mistaken for a second signature:
{
"envelopeId": "...", "status": "completed",
"event": { "id": "evt_...", "type": "envelope.completed",
"occurredAt": "2026-09-22T18:20:00+00:00", "schema": 1 }
}
event.id is stable across retries: keep the
ids you have processed and ignore one you have already seen. A receiver that
answered us and then timed out will be retried, and that retry is the same
event, not a new one.
event.occurredAt is inside the signed body on purpose. The HMAC
proves a body came from us; it does not prove when, so a captured
request replayed months later still verifies. Reject anything far older than
your own tolerance.
The same values are repeated in X-Signer-Event-Id and
X-Signer-Attempt for convenience — but the headers are not
covered by the signature. Deduplicate on the body.
Being told when a signer is replaced
By default a callback says only how an envelope ended
(envelope.completed, declined, expired,
cancelled), at onSuccess or onError. To also hear a
signer being replaced (POST .../recipients/{id}/replace), name a
third address in the request's callbackUrl:
"callbackUrl": { "onSuccess": "https://you.example/ok",
"onError": "https://you.example/ko",
"onEvent": "https://you.example/event" }
With an environment, onEvent is a
path like the other two. It is its own address, and opt-in, on purpose:
a hit on onSuccess or onError has always meant “signed” or
“did not sign”, and an integration written for that must never be sent something
else. Without onEvent nothing is sent, and onEvent never receives
how the envelope ended. The body is the envelope as GET shows it, the usual signed
event block ("type": "recipient.replaced", same id
across retries), the header X-Signer-Event: event, and the replacement:
{ "envelopeId": "...", "status": "in_progress", "recipients": [ ... ],
"event": { "id": "evt_...", "type": "recipient.replaced", "occurredAt": "...", "schema": 1 },
"replaced": { "recipientId": "...",
"from": { "email": "anna@example.com", "fullName": "Anna Rossi" },
"to": { "email": "carla@example.com", "fullName": "Carla Bianchi" },
"reason": "Anna left the company" } }
Delivered like every callback: signed, retried, listed in the console's webhook log and replayable. It is the one event that arrives before the end, so do not treat it as an outcome; the envelope is still open.
The machine contract
The OpenAPI document is at /openapi.json
— public, no key needed, no cookie set. We do not ship language SDKs on
purpose: the API is one POST with a JSON body, and five client libraries to
keep in step with a published contract would be five things to get out of
date. Generate one from the spec if you want one.
It describes the three product prefixes and the account-level endpoints.
The older spellings — /api/v1/documents,
/api/v1/templates, /api/v1/signatures — still work
and are governed identically; they are simply not advertised, so a generated
client has one method per call rather than two.
Setting a callback up: Know when something finished · Every error code: Outcomes and error codes · What a credit costs: Credit, prices and VAT.