Khatm
Khatm Developer Sandbox API
Integrate the Khatm signing workflow into your application.
Create requests, detect and confirm fields, remind signers, cancel workflows, and retrieve completed documents with their evidence.
Quickstart
- Create a Sandbox key from the API Sandbox tab in your workspace. Copy the khatm_test_… key and the whsec_… webhook secret: both are shown only once.
- Create a request with the PDF and your signers:
curl "$KHATM_BASE_URL/v1/signature-requests" \
-H "Authorization: Bearer $KHATM_API_KEY" \
-F "document=@contract.pdf;type=application/pdf" \
-F 'request={
"client_reference": "contract-123",
"signers": [{ "reference": "customer", "name": "Alice Martin",
"email": "alice@example.com", "order": 1 }],
"fields": [{ "signer_reference": "customer", "type": "signature",
"page": 1, "x": 90, "y": 610, "width": 170, "height": 55 }]
}'- Activate it with POST /signature-requests/{id}/activate to get one signing_url per signer.
- Track the status by webhook or with GET /signature-requests/{id}, then download the signed PDF and evidence from /artifacts/{type}.
No coordinates at hand? Omit fields, call POST /signature-requests/{id}/detect-fields, then confirm the suggestions with PUT /signature-requests/{id}/fields.
Back to your app
Add return_url when creating the request: after signing (or declining), the signer is sent back to your app automatically. Khatm appends khatm_status (signed, declined, cancelled or expired), signature_request_id, client_reference and signer_reference. Use them for the UI, then confirm the outcome with the API or a webhook.
Other useful options:
- locale (fr, en, ar) per signer sets the signing page language.
- sequential_signing: true enforces the signers' order.
- GET /signature-requests lists your requests (status and client_reference filters, cursor pagination).
- POST /templates creates a reusable template through the API.
Changing a signer
Wrong address, or someone left? Call POST /v1/signature-requests/{id}/signers/{reference}/replace with email (plus name, locale, send_email). Before activation, the signer is simply corrected. After activation, the previous link is revoked and the new signer keeps the same reference, order and fields; the response returns their new signing_url. A signer who already signed or declined can no longer be replaced (409).
Signature templates
A template saves the position of the signatures and fields of a standard contract once. For every new contract with the same layout, you send the PDF and the template id: no coordinates needed.
In your workspace
- Open the Signature templates page and create a template from the reference PDF.
- Choose the number of signers, then place the signatures and information fields.
- Save, then copy the template_id from the list.
By API
Create the template with POST /v1/templates (multipart: document, name, fields). Then send each request with the PDF to sign and template_id instead of fields:
{
"client_reference": "contract-2026-0042",
"template_id": "7f331696-d933-46cd-aeff-1c08151c85c1",
"signers": [{
"reference": "signer_1", "name": "Alice Martin",
"email": "alice@example.com", "order": 1
}],
"field_values": { "contract_amount": "1 200 EUR" }
}Good to know
- Each signer_reference of the template (for example signer_1) is a role: your requests must use the same signer references.
- field_values fills the template's named fields, like contract_amount above.
- template_id and fields cannot be combined in the same request.
- GET /v1/templates lists your ready templates.
- DELETE /v1/templates/{id} archives a template: new requests can no longer use it; requests already created are unaffected.
Email templates
An email template sets the look of the emails sent to your signers: sender name, logo, subject and message. You create it once, then name it in each request.
In your workspace
- Open the Email templates page and click New email template.
- Fill in the sender name, logo, subject and message. The preview shows the result in French, English and Arabic.
- Save, then copy the ebr_… id from the list.
By API
Create the template with POST /v1/email-brands (the logo must be served over HTTPS):
{
"sender_name": "Benali & Partners",
"subject": "Your contract is ready to sign",
"message": "Please sign your contract before Friday.",
"logo_url": "https://example.com/logo.png"
}Then pass its id when you create each request:
{
"client_reference": "contract-2026-0042",
"email_brand_id": "ebr_…",
"signers": [ … ]
}Good to know
- The template applies to invitations, reminders and emails sent to a new signer after a replacement.
- Each email goes out in the signer's locale: fr, en or ar (right-to-left for Arabic).
- Without email_brand_id, Khatm sends its standard email.
- A template is shared by every API key of the account.
- An archived template can no longer be used by new requests.
- For a one-off case, you can send an email object with the request instead of email_brand_id (the two cannot be combined).
Bulk send
POST /v1/bulk-sends sends one template to up to 50 recipients in one call: each recipient (client_reference, signers, field_values) becomes a request that is created and activated. Results are reported per row (active, already_exists or error); an invalid row does not stop the others. Replaying the same batch creates no duplicates and sends no extra emails.
Verifying the signer's identity
Add "verification": "email_otp" to a signer. Before seeing the document, they receive a 6-digit code by email (valid 10 minutes, 5 attempts). The document, signing and declining stay locked on the server until the code is entered. The check shows in the status (verified_at) and in the evidence.
Embedding signing in your app
Set embed_origin (for example https://app.example.com) at creation. Activation then returns an embed_url per signer, to load in an <iframe> on that origin; no other site can display it. The page sends postMessage events to your window (signing.loaded, signing.verification_required, signing.signed, signing.declined, and signing.closed with a status when the link can no longer be used) with signatureRequestId, clientReference and signerReference. Check event.origin and confirm the outcome with a webhook or the status endpoint.
SDKs
Dependency-free Node.js, Python and PHP clients are available in the repository (sdks/). They add an Idempotency-Key automatically, retry 429 and 503 responses, and verify webhooks.
Authentication
Every request sends the key in the Authorization: Bearer khatm_test_… header. Treat it like a password: keep it server-side, never in the browser, a URL or logs. A revoked key is rejected immediately.
Receiving webhooks
- Set your address: add callback_url (HTTPS) to each request. Khatm sends it a JSON POST on every status change of the request or of a signer.
- Keep the secret: each API key has its whsec_… secret, shown when the key is created. You can show it again from the API Sandbox page (Webhook secret button). It is used to verify every event (see below).
- Answer fast: return a 2xx code on receipt, then process the event. Without a 2xx answer, Khatm retries automatically.
- Follow deliveries in your workspace, Webhooks page: every event with its status, its payload, and a button to send it again.
Verifying webhooks
Khatm signs webhook-id + "." + webhook-timestamp + "." + raw body with HMAC-SHA256 and your whsec_… secret. The webhook-signature header is v1, followed by the base64 signature.
- Verify the raw bytes with a constant-time comparison.
- Reject stale timestamps.
- Deduplicate with webhook-id: delivery is at-least-once, with retries (immediately, then about 1 min, 5 min, 30 min and 2 h).
Events: signature_request.activated, signer.viewed (first opening of the link), signer.completed, signer.declined, signer.replaced, signer.expired, then signature_request.completed, .declined, .cancelled or .expired. Signer events include data.signer (reference, email, status). Arrival order is not guaranteed: when in doubt, read GET /v1/signature-requests/{id}.
Tracking and replay: GET /v1/webhook-deliveries lists every event with its state (pending, delivered, failed) and last error. Once your receiver is fixed, POST /v1/webhook-deliveries/{id}/replay sends the event again right away, with the same webhook-id.
Limits and retention
- PDFs up to 8 MiB, up to 10 signers and 100 fields per request.
- Rate limit exceeded: 429 response, with X-RateLimit-Limit and X-RateLimit-Remaining headers.
- Temporary outage: 503 temporarily_unavailable response. Retry with increasing delays; replaying a create call with the same client_reference never creates a duplicate.
- Content is deleted 10 days after upload: download the signed PDF and evidence before retention_expires_at.
API v1
POST /v1/signature-requestsBring the PDF, signers and fields together in one request. A client reference connects it to your business flow.POST /v1/signature-requests/{id}/activateActivate the request to obtain signing links. Place each URL in the right user journey.URL Khatm signing experienceThe signer opens their link, reviews the document and completes the fields assigned to them.EVENT signature_request.completedYour application receives the completion event. Verify it before updating your workflow.GET /v1/signature-requests/{id}/artifacts/{type}Retrieve the finalized document and its evidence, then attach them to the customer record.