Integrate electronic signature into your stack
Send envelopes, track signatures, receive webhooks. Simple REST API, OpenAPI 3.0, curl/Node/Python examples — everything you need to connect Certyneo to your HRIS, CRM or business software in a few hours.
Quick start
Three steps: create an API key from settings, encode your PDF in base64, send. The response contains the `signUrl` which you can share directly with the recipient.
# 1. Upload the PDF (multipart) and capture the returned document id.
DOC_ID=$(curl -s -X POST https://certyneo.com/api/v1/documents \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-F "file=@contrat.pdf" | jq -r .id)
# 2. Create a DRAFT envelope referencing the uploaded document.
ENV_ID=$(curl -s -X POST https://certyneo.com/api/v1/envelopes \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d "{
\"subject\": \"Contrat de prestation\",
\"documentIds\": [\"$DOC_ID\"],
\"recipients\": [
{ \"email\": \"client@example.com\", \"name\": \"Marie Dubois\", \"role\": \"SIGNER\" }
]
}" | jq -r .id)
# 3. Dispatch the envelope — this sends the invitation email/SMS.
curl -X POST https://certyneo.com/api/v1/envelopes/$ENV_ID/send \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"// Plain fetch, no SDK to install.
const auth = { Authorization: `Bearer ${process.env.CERTYNEO_API_KEY}` };
// 1. Upload the PDF (multipart).
const fd = new FormData();
fd.append("file", new Blob([pdfBuffer], { type: "application/pdf" }), "contrat.pdf");
const doc = await fetch("https://certyneo.com/api/v1/documents", {
method: "POST", headers: auth, body: fd,
}).then((r) => r.json());
// 2. Create the DRAFT envelope.
const envelope = await fetch("https://certyneo.com/api/v1/envelopes", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
subject: "Contrat de prestation",
documentIds: [doc.id],
recipients: [
{ email: "client@example.com", name: "Marie Dubois", role: "SIGNER" },
],
}),
}).then((r) => r.json());
// 3. Dispatch — this triggers the invitation channel for every recipient.
await fetch(`https://certyneo.com/api/v1/envelopes/${envelope.id}/send`, {
method: "POST", headers: auth,
});
console.log(envelope.id);import os, requests
auth = {"Authorization": f"Bearer {os.environ['CERTYNEO_API_KEY']}"}
# 1. Upload the PDF (multipart).
with open("contrat.pdf", "rb") as f:
doc = requests.post(
"https://certyneo.com/api/v1/documents",
headers=auth,
files={"file": ("contrat.pdf", f, "application/pdf")},
).json()
# 2. Create the DRAFT envelope.
envelope = requests.post(
"https://certyneo.com/api/v1/envelopes",
headers={**auth, "Content-Type": "application/json"},
json={
"subject": "Contrat de prestation",
"documentIds": [doc["id"]],
"recipients": [
{"email": "client@example.com", "name": "Marie Dubois", "role": "SIGNER"},
],
},
).json()
# 3. Dispatch — this triggers the invitation channel for every recipient.
requests.post(
f"https://certyneo.com/api/v1/envelopes/{envelope['id']}/send",
headers=auth,
)
print(envelope["id"])Try the API from your tools
The Postman collection and the RapidAPI listing are both generated from the OpenAPI specification that documents this page. All three therefore stay aligned with the real API, endpoint by endpoint, instead of drifting apart the first time one is added.
Postman collection
All 25 requests grouped by area — envelopes, documents, templates, seals, webhooks — each with an example body and an example response. Paste your key into the collection's apiKey variable, then run GET /health: it needs no authentication and confirms your setup is sound before the first authenticated call.
Envelopes
Creation, sending, status tracking, cancellation. An envelope can contain multiple documents and multiple signers (parallel or sequential).
Webhooks
Every envelope and recipient event (`envelope.sent`, `recipient.signed`, `envelope.completed`…) delivered to the URL of your choice — full list at /developers/webhooks. HMAC SHA-256 on each payload to verify the origin.
Simple authentication
Bearer token. One key per environment (test / prod). Instantly revocable. Limit 100 req/min/key, burst of 200, clean 429 with Retry-After header.
Available endpoints
52 public routes: account, documents, envelopes, templates, webhooks, electronic seals, API keys and billing. Every route accepts a Bearer token and returns JSON.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/account/me | Identity of the authenticated caller (id, email, plan) — scope-less credential probe |
| GET | /api/v1/sandbox | Count the data created with test keys (deleted automatically after 30 days) |
| DELETE | /api/v1/sandbox | Delete all data created with test keys, without touching real data |
| POST | /api/v1/documents | Upload a PDF (multipart) — returns document id |
| GET | /api/v1/documents | List documents |
| GET | /api/v1/documents/{id} | Fetch document metadata |
| DELETE | /api/v1/documents/{id} | Delete document |
| GET | /api/v1/envelopes | List envelopes (filter with ?status= and ?limit=) |
| POST | /api/v1/envelopes | Create envelope (status: DRAFT) — from a templateId, or from documentIds with an optional fields array |
| GET | /api/v1/envelopes/{id} | Fetch envelope state |
| PATCH | /api/v1/envelopes/{id} | Update DRAFT envelope |
| DELETE | /api/v1/envelopes/{id} | Delete DRAFT envelope (409 once sent — void it instead) |
| POST | /api/v1/envelopes/{id}/send | Dispatch DRAFT — sends invitations |
| POST | /api/v1/envelopes/{id}/void | Void a sent envelope not yet fully signed — pending signers are notified |
| GET | /api/v1/envelopes/{id}/audit-trail | Download eIDAS audit-trail PDF |
| POST | /api/v1/envelopes/{id}/embed-url | Returns a recipient's signing URL together with an embedding authorization for the origin you ask for. Call it at click time: the authorization lasts two hours. |
| GET | /api/v1/audit-anchors/{root} | Public: metadata of an anchored audit batch, or the .ots proof file with ?format=ots (no auth) |
| POST | /api/v1/audit-anchors/verify | Public: verify an audit entry + proof against its anchored Merkle root (no auth, reveals nothing) |
| GET | /api/v1/envelopes/{id}/signed-document | Download signed PDF (once COMPLETED) |
| GET | /api/v1/envelopes/bulk | List your bulk-send jobs |
| POST | /api/v1/envelopes/bulk | Bulk send: create N envelopes from one template + a CSV (Standard/Business only) |
| GET | /api/v1/envelopes/bulk/{id} | Bulk-send job progress: counts, per-row failures, created envelopes |
| GET | /api/v1/templates | List reusable envelope templates |
| POST | /api/v1/templates | Create a template (documents, roles, positioned fields) |
| GET | /api/v1/workspaces | List the workspaces you can create envelopes in |
| POST | /api/v1/sepa-mandates | Generate a SEPA mandate PDF and its DRAFT envelope, fields already placed |
| POST | /api/v1/payroll-adapters/normalize | Normalise a payroll CSV (Silae, Sage Paie, PayFit, Lucca) into the bulk-send shape |
| POST | /api/v1/ag-copropriete | Create a condominium general-meeting envelope (resolutions + ownership shares) |
| POST | /api/v1/ag-copropriete/{envelopeId}/votes | Record a co-owner's votes on the meeting resolutions |
| POST | /api/v1/ag-copropriete/{envelopeId}/tally | Tally the meeting - per-resolution result weighted by ownership shares |
| GET | /api/v1/videos/{videoId} | Download a stored identity video - GDPR art. 15 access path |
| GET | /api/v1/webhooks | List webhooks |
| POST | /api/v1/webhooks | Register webhook — returns the signing secret once |
| GET | /api/v1/webhooks/{id} | Fetch webhook subscription |
| PATCH | /api/v1/webhooks/{id} | Update url / events / active state |
| DELETE | /api/v1/webhooks/{id} | Unregister |
| POST | /api/v1/seals | Apply a qualified electronic seal to a document |
| GET | /api/v1/seals/{id} | Fetch seal status |
| GET | /api/v1/seals/{id}/certificate | Download the seal certificate |
| GET | /api/v1/payslips/workspaces | Payslips: the workspaces this key delivers for - owned, or delegated by a client (accounting firms, payroll bureaus) |
| GET | /api/v1/payslips/employees | Payslips: the payroll roster, with who opposed electronic delivery (paper payslip due) |
| PUT | /api/v1/payslips/employees | Payslips: add or update roster employees (never removes anyone) |
| POST | /api/v1/payslips | Deliver one payslip (PDF) to an employee's safe - sealed, kept 50 years, retry-safe |
| GET | /api/v1/payslips | List delivered payslips with their consultation proof |
| GET | /api/v1/keys | List API keys |
| POST | /api/v1/keys | Create API key — the secret is shown once |
| PATCH | /api/v1/keys/{id} | Rename / revoke key |
| DELETE | /api/v1/keys/{id} | Delete key |
| GET | /api/v1/billing/usage | Current period usage and projected cost |
| GET | /api/v1/status | Service status |
| GET | /api/v1/health | Health check - database, queue, and object storage |
| GET | /api/v1/openapi | Machine-readable OpenAPI specification |
Envelope templates
A template stores the PDF, the roles and the position of every signature field once, then gets reused on every send: pass its id as templateId instead of documentIds, and the positioned fields are copied onto the envelope you create.
- • Templates are created from the dashboard (Templates → New template), where you drop the document and place the fields with the mouse — that is the simplest path. The API can also create them through POST /api/v1/templates, taking the documents, the roles and the fields; note that fields are positioned in absolute coordinates (page, x, y, width, height), so you need to know the PDF layout.
- • On an account that has no template saved yet, GET /api/v1/templates returns an empty list. That is the normal behaviour, not an authentication problem.
- • The list only contains templates owned by the user the API key belongs to. A template created by a colleague will not appear, even inside a shared workspace: generate the key from the account that owns the template.
- • templateId and documentIds are mutually exclusive: send one or the other, never both and never neither.
- • Supply at least as many SIGNER recipients as the template has signer roles, otherwise creation is rejected with a 400. The signerCount field returned by the list tells you how many are expected.
- • The envelope inherits the template's signature level, unless the request passes signatureLevel explicitly.
# 1. Discover the templates saved on this account.
curl -s https://certyneo.com/api/v1/templates \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# {
# "data": [
# { "id": "cmdq7f4k80001s6y2h1xa9pl3", "name": "Contrat de prestation",
# "signerCount": 2, "documentCount": 1, "signatureLevel": "SIMPLE" }
# ],
# "pagination": { "page": 1, "pageSize": 20, "total": 1, "pages": 1 }
# }
#
# An empty "data" array means no template exists on this account yet —
# create one from the dashboard, it is not an authentication problem.
# 2. Create the envelope FROM the template: no documentIds and no field
# coordinates, both are carried by the template. Pass one SIGNER
# recipient per signer role, in the template's role order.
curl -X POST https://certyneo.com/api/v1/envelopes \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"subject": "Contrat de prestation",
"templateId": "cmdq7f4k80001s6y2h1xa9pl3",
"recipients": [
{ "email": "client@example.com", "name": "Marie Dubois", "role": "SIGNER" },
{ "email": "legal@example.com", "name": "Paul Martin", "role": "SIGNER" }
]
}'
# 3. The envelope is DRAFT at this point — POST /envelopes/{id}/send
# dispatches it, exactly as in the quick-start above.From Power Automate or Zapier
The "Create envelope" action in our no-code connectors only covers the template path — the Template field is required there. For an ad-hoc document that changes on every run, use the "Upload document" action then a raw HTTP action against POST /api/v1/envelopes, passing the returned documentIds.
Sending the document: two accepted shapes
POST /api/v1/documents accepts the file in either of two ways. The same checks apply to both: allowed types, the 50 MB cap, magic-byte verification and the virus scan.
- • As multipart/form-data, with a part named file. This is the classic shape, the one curl -F and most libraries send.
- • As a raw body: the file bytes are the request body, and the Content-Type header gives the type (application/pdf, for instance). Useful from a tool that forwards the content as-is without wrapping the request — this is what the Power Automate connector does.
- • In raw mode the filename has nowhere to live in the body: pass it with the X-File-Name header or the ?fileName= query parameter. Without it, the document is named after its type.
- • An unsupported type answers 415 naming both accepted shapes, and a body announced as multipart but unreadable answers 400. Neither is a server error.
# a. multipart/form-data — the classic shape.
curl -X POST https://certyneo.com/api/v1/documents \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-F "file=@contrat.pdf;type=application/pdf"
# b. raw body — the file bytes ARE the body, typed by Content-Type.
# The filename has nowhere to live in the body, so pass it as a header
# (or ?fileName=). Without it the document is named after its type.
curl -X POST https://certyneo.com/api/v1/documents \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/pdf" \
-H "X-File-Name: contrat.pdf" \
--data-binary "@contrat.pdf"
# Both return the same 201 with the document id to pass as documentIds.Signature field placement
Without a template, an envelope created from documentIds has no pre-placed field: the signer receives the document with nowhere to sign. The fields array, sent in the same create call, positions every field to the point — the API-side equivalent of what a template stores once and for all.
- • Coordinates are in PDF points, with the origin at the TOP-LEFT of the page and the Y axis pointing down (an A4 page is 595 × 842 points). x and y are the field box's top-left corner, width and height its size.
- • pageNumber starts at 1, documentIndex starts at 0. A page number beyond the document is not rejected at creation: the field is skipped at signing time and appears nowhere — the first thing to check when a field goes missing.
- • recipientEmail must match one of the recipients in the same call, case-insensitively. Otherwise creation fails listing every offending row, so you fix them in one round-trip instead of one at a time.
- • fields and templateId are mutually exclusive: a template already carries its own layout. The fields array is therefore only used together with documentIds.
- • Accepted types: SIGNATURE, INITIALS, DATE_SIGNED, TEXT, CHECKBOX and RADIO_GROUP. required defaults to true; placeholder and dateFormat are optional, and options is only kept for RADIO_GROUP.
- • An envelope accepts at most 100 fields, 20 documents and 50 recipients — your plan's own limits may be lower.
# Ad-hoc envelope WITH pre-placed fields — no template involved.
# Coordinates are PDF points, origin TOP-LEFT of the page, +Y downwards
# (A4 = 595 x 842 pt). x / y are the field box's top-left corner.
#
# The last field is positioned by ANCHOR instead of by eye: the server
# locates "Signature du client" in the PDF and computes the spot. x / y
# stay required there — they are the fallback if the text is not found.
curl -X POST https://certyneo.com/api/v1/envelopes \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"subject": "Contrat de prestation",
"documentIds": ["cmdq7f4k80001s6y2h1xa9pl3"],
"recipients": [
{ "email": "client@example.com", "name": "Marie Dubois", "role": "SIGNER" }
],
"fields": [
{ "recipientEmail": "client@example.com", "documentIndex": 0,
"pageNumber": 2, "fieldType": "SIGNATURE",
"x": 90, "y": 640, "width": 180, "height": 44 },
{ "recipientEmail": "client@example.com", "documentIndex": 0,
"pageNumber": 2, "fieldType": "DATE_SIGNED",
"x": 320, "y": 640, "width": 140, "height": 30,
"dateFormat": "DD/MM/YYYY" },
{ "recipientEmail": "client@example.com", "documentIndex": 0,
"pageNumber": 2, "fieldType": "TEXT",
"x": 90, "y": 700, "width": 200, "height": 30,
"placeholder": "Fonction", "required": false },
{ "recipientEmail": "client@example.com", "documentIndex": 0,
"pageNumber": 2, "fieldType": "SIGNATURE",
"anchorText": "Signature du client", "anchorPlacement": "below",
"anchorIndex": 0,
"x": 90, "y": 640, "width": 180, "height": 44 }
]
}'
# The envelope is DRAFT at this point — POST /envelopes/{id}/send
# dispatches it, exactly as in the quick-start above.Text anchors: place a field without knowing the coordinates
Instead of coordinates, a field can quote a string printed in the document: anchorText locates it in the PDF and the server computes the position at creation time. This is the mode to prefer when the document is regenerated on every send — mail merge, contract generators — because the layout moves while the words "Signature du client" stay put. anchorPlacement sets which side of the text the field lands on (right by default, otherwise below, above or left) and anchorIndex picks the occurrence when the text appears several times.
x and y stay required even with an anchor: they are the fallback. A missed anchor does not fail the creation — the field keeps the literal position you supplied, with no error or warning in the response. So give it a sensible fallback rather than 0,0, and check the result on a first send.
Embedded signing: sign without leaving your application
Your customer reviews and signs the document from inside your own interface. The ceremony opens in a window on top of your page, and they come back to you automatically once it is done. Nothing changes server-side: create the envelope as usual with a redirectUrl, then hand the recipient's signing URL to the SDK.
- • Call Certyneo.sign() directly in your click handler and pass your server call through getUrl. An await before sign() leaves the user gesture behind, and Safari and Firefox then block the window. The SDK opens it immediately and loads it once your promise resolves.
- • redirectUrl is the return channel. We send the signer back there with certyneo_status (completed or declined) and certyneo_envelope appended. Load the same script on that return page: it is what notifies the portal window still open behind it.
- • In embedded mode our own calls to action are suppressed. Your customer is never invited to create a Certyneo account.
- • If the browser refuses the window, the SDK falls back to a full-page redirect on its own. The journey is identical for the signer and your return page receives exactly the same parameters.
- • There is deliberately no "the user gave up" callback: the window's state is not readable from your page, so such a callback would fire wrongly. A signature that does not arrive stays pending until the webhook says otherwise.
// 1. Your server creates and sends the envelope, then hands the browser the
// recipient's signing URL. Nothing new on the API side:
// POST /api/v1/envelopes { …, "redirectUrl": "https://your-app.com/quote/42/signed" }
// POST /api/v1/envelopes/{id}/send → recipients[].accessToken
// signingUrl = "https://certyneo.com/sign/" + accessToken
// 2. On your quote page — load the SDK once:
// <script src="https://certyneo.com/embed.js"></script>
document.querySelector("#sign").addEventListener("click", () => {
Certyneo.sign({
// Called AFTER the window is already open, so no popup blocker ever
// sees a delay between the click and window.open.
getUrl: () =>
fetch("/api/quote/42/signature", { method: "POST" })
.then((r) => r.json())
.then((d) => d.signingUrl),
onCompleted: (e) => showConfirmation(e.envelopeId),
onDeclined: () => showDeclined(),
onError: (e) => showError(e),
});
});
// 3. On https://your-app.com/quote/42/signed — your return page — load the
// same script and render a short confirmation. It relays the outcome to
// the portal window behind it, then closes itself.The browser event is not proof of signature
onCompleted is a display signal emitted by a browser, so anyone can forge it from their own console. Never move a record to "signed" on that basis: use it to refresh the screen, and rely on the signed envelope.completed webhook received by your server for the real state. This is the classic pitfall of payment integrations, and it repeats identically here.
Developing on your own machine
With a test key, redirectUrl accepts http://localhost (and 127.0.0.1), so you can build the whole flow locally before deploying. A live key rejects it, so that no production envelope ever sends a real signer to a machine that is not theirs.
Show the ceremony inside your page
Frame mode displays the signature inside your own interface, with no window. It asks one more step than window mode: proving that the site displaying it is yours. That is what stops a third party from framing our ceremonies to mislead your customers.
- • Declare your site under Settings → Embedded signing, then publish the DNS record shown there. An exact address: a different subdomain or port counts as a different site.
- • For each signature, request a fresh URL from POST /api/v1/envelopes/{id}/embed-url. It carries the display authorization and lasts two hours, which matches click time rather than envelope creation.
- • Two journeys cannot run inside a frame: qualified signatures, whose identity check refuses to be framed in turn, and payment on signature. In both cases the signer is sent to a full window.
- • The “Powered by Certyneo” line disappears from the frame on the Business plan and above. Frame mode itself is included from the Standard plan.
// Mode cadre : la cérémonie s'affiche DANS votre page.
//
// 1. Déclarez votre site une fois pour toutes dans Réglages → Signature
// intégrée, et publiez l'enregistrement DNS qu'on vous y donne.
//
// 2. À chaque signature, votre serveur demande une URL fraîche. Elle porte
// l'autorisation qui permet au navigateur d'afficher la cérémonie chez
// vous, et vaut deux heures :
//
// POST /api/v1/envelopes/{id}/embed-url
// { "origin": "https://portail.exemple.fr" }
// -> { "signingUrl": "…", "expiresAt": "…" }
Certyneo.sign({
mode: "iframe",
el: "#zone-signature", // sélecteur ou nœud de votre page
getUrl: () =>
fetch("/api/devis/42/signature", { method: "POST" })
.then((r) => r.json())
.then((d) => d.signingUrl),
onCompleted: (e) => afficherConfirmation(e.envelopeId),
onDeclined: () => afficherRefus(),
});
// Le cadre parle à votre page par postMessage — pas besoin de page de retour,
// contrairement au mode fenêtre.Get a signature without sending any email
A portal that shows the ceremony inside its own pages rarely needs our invitation: it lands next to their own flow and offers an account to a customer who does not want one. Set a recipient's notification channel to NONE and nothing is sent to them — you are the one taking them to their signature.
- • The setting is per recipient, not per envelope: you can silence the customer you host and keep the email for an outside counter-signer, who has no portal of their own.
- • It is limited to signers and approvers, the only ones you have a link to hand over. You read that link in the create and send responses, or you ask for a fresh URL at click time from POST /api/v1/envelopes/{id}/embed-url.
- • Automatic reminders stop on their own for that recipient: we never wrote to them, so we do not start with a reminder. Every other recipient is still reminded as usual.
- • The signature certificate says so, recipient by recipient. An evidence file that stayed silent about it would suggest this signer received an invitation from us that never existed.
POST /api/v1/envelopes
{
"subject": "Mandat de gestion",
"documentIds": ["doc_8f2c1a4b9e7d"],
"recipients": [
{
"email": "client@exemple.fr",
"name": "Camille Client",
"role": "SIGNER",
"notificationChannel": "NONE"
},
{
"email": "notaire@exemple.fr",
"name": "Maître Notaire",
"role": "SIGNER"
}
]
}
// La réponse rend le lien de chaque signataire :
//
// "recipients": [
// { "email": "client@exemple.fr", "accessToken": "…",
// "notificationChannel": "NONE" },
// { "email": "notaire@exemple.fr", "accessToken": "…",
// "notificationChannel": "EMAIL" }
// ]
//
// Le client ne reçoit rien : vous l'emmenez sur sa signature depuis votre
// portail. Le notaire, lui, reçoit son invitation comme d'habitude.Show your customer's name as the sender
By default, the signer reads the name of the API key owner. If you have documents signed for several companies with a single key, pass the sender field when creating the envelope: the signing page and the invitation and reminder emails then show the name of the company concerned.
- • Available on the Business plan and above. On a lower plan the request is refused, rather than letting the signer read a name other than the one you expect.
- • The name is display only: the certificate and audit trail keep the real sender, and emails still leave from Certyneo.
- • The address is optional. Without it, the signer sees only the name, never the key owner's address.
POST /api/v1/envelopes
{
"subject": "Devis 2026-118",
"documentIds": ["doc_8f2c1a4b9e7d"],
"recipients": [
{ "email": "client@exemple.fr", "name": "Camille Client", "role": "SIGNER" }
],
"sender": {
"name": "1.2.3. Panneaux Solaires",
"email": "devis@123-panneaux-solaires.fr"
}
}Authentication
Each call carries an API key in the Authorization header. Keys are generated from Settings → API Keys and are displayed only once.
GET /api/v1/account/me HTTP/1.1Host: certyneo.com
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
# 200 OK
{ "data": { "id": "usr_…", "email": "you@example.com", "plan": "BUSINESS", "environment": "live" } }- • Format: sk_live_… in production, sk_test_… for sandbox. Header: Authorization: Bearer <clé>.
- • Scopes: envelopes, documents, webhooks, seals — in read (:read) or write (:write). Writing implies reading; the * scope grants all rights.
- • Keys sk_test_ create sandbox resources, excluded from quota and billing. No real email is sent to the recipient, unless their address matches the sending account's own email — useful for testing the full flow on yourself.
- • Errors: 401 invalid key, 403 insufficient scope, 429 rate limit exceeded, 402 monthly quota reached.
Response format
One thing to know before writing your client: collections are wrapped in a data object, while single resources are returned flat. Reading response.data.data on a single resource therefore returns undefined.
// GET /api/v1/envelopes
// Collections are WRAPPED in a "data" array.
{
"data": [
{ "id": "env_abc123", "subject": "Contrat", "status": "SENT" }
]
}// GET /api/v1/envelopes/{id}
// Single resources are returned FLAT — no "data" envelope.
{
"id": "env_abc123",
"subject": "Contrat",
"status": "COMPLETED",
"recipients": [ /* … */ ]
}Rate limits
Rate limits ensure stable service quality for all customers. If you need more, contact us.
- • 100 requests per minute per API key
- • Burst tolerated up to 200 requests in less than 10 seconds
- • 429 response with Retry-After header indicating the delay in seconds