Go to main content
Certyneo
Public API v1

Integrate electronic signature into your stack

Send envelopes, track signatures, receive webhooks. Simple REST API, OpenAPI 3.0, curl/Node/Python examples — everything 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` that you can share directly with the recipient.

cURLbash
# 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"
JavaScript / Nodets
// 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);
Pythonpython
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.

RapidAPI listing

The same endpoint catalogue, runnable straight from the browser. The playground expects two distinct headers: the RapidAPI key the platform issues you, and your Certyneo key in Authorization — the second one is what actually authorises the 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 complete cycle: envelopes, documents, webhooks, API keys. All routes accept a Bearer token and return JSON.

MethodPathDescription
GET/api/v1/account/meIdentity of the authenticated caller (id, email, plan) — scope-less credential probe
POST/api/v1/documentsUpload a PDF (multipart) — returns document id
GET/api/v1/documentsList documents
GET/api/v1/documents/{id}Fetch document metadata
DELETE/api/v1/documents/{id}Delete document
GET/api/v1/envelopesList envelopes (filter with ?status= and ?limit=)
POST/api/v1/envelopesCreate 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}/sendDispatch DRAFT — sends invitations
GET/api/v1/envelopes/{id}/audit-trailDownload 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/verifyPublic: verify an audit entry + proof against its anchored Merkle root (no auth, reveals nothing)
GET/api/v1/envelopes/{id}/signed-documentDownload signed PDF (once COMPLETED)
GET/api/v1/envelopes/bulkList your bulk-send jobs
POST/api/v1/envelopes/bulkBulk 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/templatesList reusable envelope templates
POST/api/v1/templatesCreate a template (documents, roles, positioned fields)
POST/api/v1/sepa-mandatesGenerate a SEPA mandate PDF and its DRAFT envelope, fields already placed
POST/api/v1/payroll-adapters/normalizeNormalise a payroll CSV (Silae, Sage Paie, PayFit, Lucca) into the bulk-send shape
POST/api/v1/ag-coproprieteCreate a condominium general-meeting envelope (resolutions + ownership shares)
POST/api/v1/ag-copropriete/{envelopeId}/votesRecord a co-owner's votes on the meeting resolutions
POST/api/v1/ag-copropriete/{envelopeId}/tallyTally 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/webhooksList webhooks
POST/api/v1/webhooksRegister 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/sealsApply a qualified electronic seal to a document
GET/api/v1/seals/{id}Fetch seal status
GET/api/v1/seals/{id}/certificateDownload the seal certificate
GET/api/v1/keysList API keys
POST/api/v1/keysCreate 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/usageCurrent period usage and projected cost
GET/api/v1/statusService status
GET/api/v1/openapiMachine-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.
cURLbash
# 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.
cURLbash
# 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.
cURLbash
# 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.

HTTPhttp
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.

Collection — wrappedjson
// GET /api/v1/envelopes
// Collections are WRAPPED in a "data" array.
{
  "data": [
    { "id": "env_abc123", "subject": "Contrat", "status": "SENT" }
  ]
}
Single resource — flatjson
// 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 clients. 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