DoConure

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.