Everything you need to connect your product to Mocha Signature: how to authenticate, which environment to call, how to send your first envelope, and how to receive signed documents through webhooks.
What the Mocha Signature external API is for.
The Mocha Signature external API is the partner-facing surface of the Mocha Signature web app. Use it to read the templates in your account, send envelopes out for signing, track each recipient's progress, and receive the finished document on your own endpoint. Every operation is scoped to your tenant account — you only ever see your own data.
| You want to… | Use |
|---|---|
| List the templates available to your account | POST /get-template-by-user |
| Read a template's recipient roles before sending | POST /get-template-detail |
| Send a document for signature from your own app | POST /send-envelop |
| Show signing progress per recipient in your UI | POST /envelop-tracking |
| Be notified the moment a document is completed or declined | Custom webhooks (Settings → Custom Webhook) |
| Debug a webhook your endpoint did not accept | POST /webhook/deliveries |
Five steps from a new account to a signed document.
Sign up or log in to Mocha Signature
uat to the host: app.uat.mochatechnologies.com.Create a template
template_id — that is what you send to the API.Generate an API key
Call the endpoints
X-Tenant and Api-Key headers on every request. Start with /get-template-detail, then /send-envelop.Configure webhooks
Two headers, on every single request.
Every request must carry both headers. There is no OAuth flow, no token exchange and no expiry to manage on your side.
| Header | Value | Where to find it |
|---|---|---|
X-Tenant | Your tenant identifier | Provided with your account; the subdomain / organisation identifier your account belongs to |
Api-Key | The API key you generated | Settings → Key Management → + New Key |
Content-Type | application/json | Always — every endpoint takes a JSON body |
# Read your credentials from the environment - never paste an API key
# into a shell command, where it lands in your history and process list.
# export MOCHA_TENANT=...
# export MOCHA_API_KEY=...
curl -X POST \
'https://services.us.uat.mochatechnologies.com/signature/api/V1/get-template-by-user' \
-H "X-Tenant: $MOCHA_TENANT" \
-H "Api-Key: $MOCHA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "user_id": "acf28c4584f007dca67b" }'Api-Key header is missing, the key was revoked, or the key does not belong to the tenant in X-Tenant. A 401 is also returned if the platform could not issue an internal token for the key's user — retry once, then check the key is still active in Settings.Test against UAT, then flip one host segment for production.
| Environment | API base URL | Web app |
|---|---|---|
| UAT (sandbox) | https://services.us.uat.mochatechnologies.com/signature/api/V1 | app.uat.mochatechnologies.com |
| Production | https://services.us.mochatechnologies.com/signature/api/V1 | app.mochatechnologies.com |
The only difference is the uat segment in the host — remove it for production. Keep the base URL in configuration so you can promote an integration without touching code.
List the templates in your account, in four languages.
The quickest way to confirm your credentials work is to list your templates. A 200 with a data array means the tenant and key are valid.
# Read your credentials from the environment - never paste an API key
# into a shell command, where it lands in your history and process list.
# export MOCHA_TENANT=...
# export MOCHA_API_KEY=...
curl -X POST \
'https://services.us.uat.mochatechnologies.com/signature/api/V1/get-template-by-user' \
-H "X-Tenant: $MOCHA_TENANT" \
-H "Api-Key: $MOCHA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{ "user_id": "acf28c4584f007dca67b" }'<?php
$baseUrl = 'https://services.us.uat.mochatechnologies.com/signature/api/V1';
$payload = json_encode(['user_id' => 'acf28c4584f007dca67b']);
$ch = curl_init($baseUrl . '/get-template-by-user');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'X-Tenant: ' . getenv('MOCHA_TENANT'),
'Api-Key: ' . getenv('MOCHA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $payload,
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// The HTTP status code is authoritative - branch on it, not on the body.
if ($status !== 200) {
throw new RuntimeException('Mocha Signature returned ' . $status . ': ' . $response);
}
$templates = json_decode($response, true)['data'] ?? [];const BASE_URL =
"https://services.us.uat.mochatechnologies.com/signature/api/V1";
async function callMochaSignature(endpoint, body) {
const response = await fetch(BASE_URL + endpoint, {
method: "POST",
headers: {
"X-Tenant": process.env.MOCHA_TENANT,
"Api-Key": process.env.MOCHA_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(
"Mocha Signature " + response.status + ": " + JSON.stringify(payload),
);
}
return payload;
}
const { data: templates } = await callMochaSignature("/get-template-by-user", {
user_id: "acf28c4584f007dca67b",
});import os
import requests
BASE_URL = "https://services.us.uat.mochatechnologies.com/signature/api/V1"
HEADERS = {
"X-Tenant": os.environ["MOCHA_TENANT"],
"Api-Key": os.environ["MOCHA_API_KEY"],
"Content-Type": "application/json",
}
response = requests.post(
BASE_URL + "/get-template-by-user",
headers=HEADERS,
json={"user_id": "acf28c4584f007dca67b"},
timeout=30,
)
response.raise_for_status()
templates = response.json()["data"]Read the template first, send second. The recipient roles you supply must match the template exactly, so never hard-code them — read them from /get-template-detail and map your own recipients onto them.
// 1. Read the template so you know exactly which roles it expects.
const detail = await callMochaSignature("/get-template-detail", {
template_id: "73766101002022",
});
const { required_recipient, recipients_role } = detail.data;
// required_recipient: 2
// recipients_role: ["Signer 1", "Approver"]
// 2. Send the envelope. One entry per role, in the template's own order.
const sent = await callMochaSignature("/send-envelop", {
user_id: "acf28c4584f007dca67b",
company_name: "Your Company",
email_subject: "Please sign the Non-Disclosure Agreement",
message: "Kindly review and sign the attached document.",
template_id: "73766101002022",
recipients_role: [
{ role: "Signer 1", name: "John Doe", email: "john.doe@example.com" },
{ role: "Approver", name: "Alice Smith", email: "alice.smith@example.com" },
],
metadata: { order_id: "ORD-10021", source: "crm" },
});
// 3. Persist envelope_id - it is the key for tracking and for every webhook.
await db.orders.update("ORD-10021", { envelopeId: sent.envelope_id });
// 4. Poll tracking when you need the current state (webhooks push the final one).
const tracking = await callMochaSignature("/envelop-tracking", {
envelop_id: sent.envelope_id,
});envelope_id from /send-envelop is the key for tracking and appears in every webhook payload for that envelope. Persist it against your own record before you do anything else.How to read the bodies you get back.
POST with a JSON body — including the read-only ones such as /get-template-detail.Content-Type: application/json alongside the two authentication headers.Responses are JSON. The shape varies slightly between endpoints as a legacy of the platform's growth: some return status as a boolean plus a separate status_code, others return status as the numeric HTTP code.
data for the payload. Do not write logic that depends on status being a boolean or a number.Validation failures return 422 with msg holding a field → messages map. Surface those messages directly; they name the offending field.
{
"status": false,
"msg": {
"template_id": ["The template id field is required."]
}
}Get the signed document pushed to you instead of polling.
Webhooks are configured in the app, not over the API. Go to Settings → Custom Webhook → + Add Custom Configuration and provide a name, your endpoint URL, and the events it should fire on.
| Event | Fires when |
|---|---|
document.completed | Every required recipient has completed the envelope. Carries a pre-signed link to the finished PDF, valid for 15 minutes. |
document.declined | A recipient declines or rejects the envelope. The signing flow stops there — remaining recipients are not asked to sign. |
| Header | Description |
|---|---|
X-Webhook-Event | Event type, for example document.completed. |
X-Webhook-Delivery-Id | Unique per delivery. Store it — it is the lookup key for the Deliveries endpoint. |
X-Webhook-Timestamp | ISO-8601 send time. |
X-Authorization-Digest | Always HMACSHA256. |
X-Webhook-Signature-1 | Base64 HMAC-SHA256 of the raw body, computed with active secret #1. |
X-Webhook-Signature-2 | Present when a second secret is active (during rotation). |
{
"event": "document.completed",
"data": {
"user_id": "acf28c4584f007dca67b",
"envelop_id": "b5db5fe9-8a44-4ecc-b3f4-222b14b1d5dc",
"status": "completed",
"generated_at": "2026-08-03T10:24:11.000000Z",
"metadata": {
"order_id": "ORD-10021",
"source": "crm"
},
"document": {
"file_name": "b5db5fe9-8a44-4ecc-b3f4-222b14b1d5dc.pdf",
"download_url": "https://s3.amazonaws.com/...?X-Amz-Expires=900&X-Amz-Signature=...",
"mime_type": "application/pdf"
}
},
"timestamp": "2026-08-03T10:24:11.482000Z"
}data.document.download_url is a pre-signed S3 link valid for 15 minutes. Download and store the file when you receive the event — never persist the URL itself.HMAC-SHA256 over the raw request body.
Every delivery is signed with HMAC-SHA256 over the raw request body. You may receive one or more X-Webhook-Signature-* headers. Compute the digest with each of your active secrets and accept the request if any signature matches.
php://input in PHP, express.raw() in Express, request.body (bytes) in Django/FastAPI.<?php
// Read the RAW body. Re-encoding the JSON changes the digest.
$rawBody = file_get_contents('php://input');
$incomingSignatures = [
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE_1'] ?? null,
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE_2'] ?? null,
];
// Load the secret from configuration - never hard-code it in the handler.
$secret = getenv('MOCHA_WEBHOOK_SECRET');
$computed = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
$valid = false;
foreach ($incomingSignatures as $signature) {
if ($signature && hash_equals($computed, $signature)) {
$valid = true;
break;
}
}
if (!$valid) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
// Acknowledge immediately, then process out of band.
http_response_code(200);
echo json_encode(['received' => true]);const crypto = require("crypto");
const express = require("express");
const app = express();
// Your active secret keys - up to two while rotating.
const SECRETS = [
process.env.MOCHA_WEBHOOK_SECRET_1,
process.env.MOCHA_WEBHOOK_SECRET_2,
].filter(Boolean);
function isValidSignature(rawBody, headers) {
const incoming = [
headers["x-webhook-signature-1"],
headers["x-webhook-signature-2"],
].filter(Boolean);
return incoming.some((signature) =>
SECRETS.some((secret) => {
const computed = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("base64");
const a = Buffer.from(computed);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}),
);
}
// express.raw keeps the exact bytes - never verify JSON.stringify(req.body).
app.post(
"/hooks/mocha-signature",
express.raw({ type: "application/json" }),
(req, res) => {
if (!isValidSignature(req.body, req.headers)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // acknowledge fast
queue.add("mocha-signature", event); // process asynchronously
},
);What happens when your endpoint is down.
Delivery is retried up to four attempts. Respond 2xx quickly to acknowledge — anything else, or a timeout, marks the delivery failed and schedules the next attempt.
| Attempt | Sent after |
|---|---|
| 1 | Immediately |
| 2 | 60 seconds |
| 3 | 5 minutes |
| 4 | 15 minutes |
| Final | 30 minutes |
X-Webhook-Delivery-Id (or on data.envelop_id plus event) and ignore duplicates.POST /webhook/deliveries with the delivery id to see exactly what was sent, what your endpoint returned, how many attempts were made and how long each took./envelop-tracking always reflects the current state — it is the reliable fallback.Status codes and the fix for each.
| Status | Meaning |
|---|---|
| 200 | Success. Read the payload from data. |
| 201 | Created — returned by Insert Envelope Template Fields on success. |
| 204 | No template maps to the supplied template_id. Per HTTP semantics the body is not transmitted, so branch on the status code, not the body. |
| 401 | Api-Key missing, revoked, or not owned by the tenant in X-Tenant. |
| 404 | The tenant in X-Tenant does not exist, the key's user has no email on record, or the envelope / delivery id was not found. |
| 422 | Validation failed. msg holds a field → messages map naming what is wrong. |
| 500 | Processing failed — for example the template could not be verified or the recipient roles could not be mapped. error carries the reason. |
| 503 | A dependency needed to authenticate the request was unreachable. Safe to retry after a short backoff. |
| Symptom | Cause and fix |
|---|---|
500 — Recipients role can not map with required template roles. | Your recipients_role entries do not match the template. Call /get-template-detail and make sure the number of entries equals required_recipient and every role string appears in the template's recipients_role list, spelled identically. |
| 422 on Send Envelope with no obvious missing field | message is documented as optional but enforced downstream. Always send it, along with user_id, company_name, email_subject, template_id and recipients_role. |
| 404 from Envelope Tracking with an id that just worked | The request field is envelop_id (single e) while Send Envelope returns it as envelope_id. Map the value across the spelling difference. |
pdf_preview returns 403 when fetched | Pre-signed template URLs are valid for 10 minutes. Fetch promptly or re-request the list. A null value means the object is missing from storage. |
| Webhook signature never matches | You are hashing a re-encoded body. Verify against the raw bytes, and check both X-Webhook-Signature-1 and X-Webhook-Signature-2 during a key rotation. |
| Intermittent 503s | An upstream authentication dependency was briefly unreachable. Retry with exponential backoff; the request was not processed. |
What production integrations get right.
envelope_id on send and X-Webhook-Delivery-Id on receipt — both are the only keys you have for tracking and for debugging deliveries later.metadata for correlation. Anything you put there is echoed back verbatim in every webhook, which lets you match an event to your own order or case without a lookup table.recipients_role before each send stops a template edit from breaking your integration./envelop-tracking for on-demand status in your UI and as a reconciliation fallback, not as a polling loop.