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
12 routes covering the full cycle: envelopes, documents, webhooks, API keys. All routes accept a Bearer token and return JSON.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/account/me | Identity of the authenticated caller (id, email, plan) — scope-less credential probe |
| 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} | Void / delete DRAFT envelope |
| POST | /api/v1/envelopes/{id}/send | Dispatch DRAFT — sends invitations |
| GET | /api/v1/envelopes/{id}/audit-trail | Download eIDAS audit-trail PDF |
| 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) |
| 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/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/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.
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