Developer

Getting Started

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.

Introduction

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.

What you can build

You want to…Use
List the templates available to your accountGET /get-template-by-user
Read a template's recipient roles before sendingGET /get-template-detail
Send a document for signature from your own appPOST /send-envelop
Show signing progress per recipient in your UIGET /envelop-tracking
Be notified the moment a document is completed or declinedCustom webhooks (Settings → Custom Webhook)
Debug a webhook your endpoint did not acceptPOST /webhook/deliveries

Server-to-server only

Requests are authenticated with an API key. Call the API from your backend and keep the key there — never ship it in a browser, mobile app or any client the end user controls.

Integration Sequence

Five steps from a new account to a signed document.

  1. 1

    Sign up or log in to Mocha Signature

    Create an account at app.mochatechnologies.com/register or sign in at app.mochatechnologies.com/login. For sandbox testing, add uat to the host: app.uat.mochatechnologies.com.
  2. 2

    Create a template

    Go to Templates → New Template. Upload the document, define the recipient roles and their privileges (needs to sign / view / approve), place the fields on the page, then save. The template list shows the generated template_id — that is what you send to the API.
  3. 3

    Generate an API key

    Go to Settings → Key Management → + New Key. The key appears in the key table. Store it in your secret manager or environment configuration — it authenticates every call.
  4. 4

    Call the endpoints

    Send both the X-Tenant and Api-Key headers on every request. Start with /get-template-detail, then /send-envelop.
  5. 5

    Configure webhooks

    Go to Settings → Custom Webhook and register your endpoint so completion and decline events are pushed to you instead of polled.

Authentication

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.

HeaderValueWhere to find it
X-TenantYour tenant identifierProvided with your account; the subdomain / organisation identifier your account belongs to
Api-KeyThe API key you generatedSettings → Key Management → + New Key
Content-Typeapplication/jsonAlways — every endpoint takes a JSON body
Headers
# 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 GET \
  'https://services.us.uat.mochatechnologies.com/signature/api/V1/get-template-by-user?user_id=acf28c4584f007dca67b' \
  -H "X-Tenant: $MOCHA_TENANT" \
  -H "Api-Key: $MOCHA_API_KEY"

A 401 means one of three things

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

Environments

Test against UAT, then swap the host for production.

EnvironmentAPI base URLWeb app
UAT (sandbox)https://services.us.uat.mochatechnologies.com/signature/api/V1app.uat.mochatechnologies.com
Productionhttps://services.ap.mochatechnologies.com/signature/api/V1app.mochatechnologies.com

Only the host differs — the /signature/api/V1 path and every endpoint below it are identical. Keep the base URL in configuration so you can promote an integration without touching code.

POSThttps://services.us.uat.mochatechnologies.com/signature/api/V1/send-envelopX-Tenant + Api-Key required

Accounts and templates are per environment

A UAT account, its templates and its API keys do not exist in production. Create the template and generate a fresh key again in the production app before you go live.

Your First Request

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.

cURL
# 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 GET \
  'https://services.us.uat.mochatechnologies.com/signature/api/V1/get-template-by-user?user_id=acf28c4584f007dca67b' \
  -H "X-Tenant: $MOCHA_TENANT" \
  -H "Api-Key: $MOCHA_API_KEY"
PHP
<?php

$baseUrl = 'https://services.us.uat.mochatechnologies.com/signature/api/V1';
$query   = http_build_query(['user_id' => 'acf28c4584f007dca67b']);

$ch = curl_init($baseUrl . '/get-template-by-user?' . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        'X-Tenant: ' . getenv('MOCHA_TENANT'),
        'Api-Key: '  . getenv('MOCHA_API_KEY'),
    ],
]);

$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'] ?? [];
Node.js
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;
}

async function callMochaSignatureGet(endpoint, params) {
  const query = new URLSearchParams(params);
  const response = await fetch(BASE_URL + endpoint + "?" + query, {
    method: "GET",
    headers: {
      "X-Tenant": process.env.MOCHA_TENANT,
      "Api-Key": process.env.MOCHA_API_KEY,
    },
  });

  const payload = await response.json();

  if (!response.ok) {
    throw new Error(
      "Mocha Signature " + response.status + ": " + JSON.stringify(payload),
    );
  }

  return payload;
}

const { data: templates } = await callMochaSignatureGet("/get-template-by-user", {
  user_id: "acf28c4584f007dca67b",
});
Python
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"],
}

response = requests.get(
    BASE_URL + "/get-template-by-user",
    headers=HEADERS,
    params={"user_id": "acf28c4584f007dca67b"},
    timeout=30,
)
response.raise_for_status()

templates = response.json()["data"]

Sending your first envelope

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.

Node.js — end-to-end
// 1. Read the template so you know exactly which roles it expects.
const detail = await callMochaSignatureGet("/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 callMochaSignatureGet("/envelop-tracking", {
  envelop_id: sent.envelope_id,
});

Save the 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.

Request & Response Conventions

How to read the bodies you get back.

Requests

  • Every endpoint is POST with a JSON body — including the read-only ones such as /get-template-detail.
  • Always send Content-Type: application/json alongside the two authentication headers.
  • There are no query-string parameters; all input goes in the body.

Responses

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.

Branch on the HTTP status code

The HTTP status code is always authoritative. Treat the body fields as informational and read data for the payload. Do not write logic that depends on status being a boolean or a number.

Validation failures

Validation failures return 422 with msg holding a field → messages map. Surface those messages directly; they name the offending field.

422 response
{
  "status": false,
  "msg": {
    "template_id": ["The template id field is required."]
  }
}

Webhooks Setup

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.

EventFires when
document.completedEvery required recipient has completed the envelope. Carries a pre-signed link to the finished PDF, valid for 15 minutes.
document.declinedA recipient declines or rejects the envelope. The signing flow stops there — remaining recipients are not asked to sign.

Whose envelopes does a webhook receive?

A webhook receives events only for envelopes sent by the user who configured it. Matching is on tenant + sending user + event type.

In practice: the user signed in when you create the configuration must be the same user whose id you pass as user_id to POST /send-envelop. Your application key's owner is that user — the user_id on the key record is the value to use.

If your integration sends on behalf of several users, each sending user needs their own webhook configuration. There is currently no tenant-wide subscription that catches every sender in an account.

Envelope completes but nothing arrives?

This is almost always a sender mismatch — the webhook was configured by a different user than the user_id used in /send-envelop. Nothing in the API response reports it. Check that the two match before investigating anything else, then confirm the configuration is active and the event is subscribed.

Secret keys

  • A secret key is generated when you save the configuration and is visible only once — copy and store it immediately.
  • Additional keys are managed under + Manage Keys → + Add Secret Key.
  • At most two secret keys can be active at a time, which lets you rotate without downtime: add the new key, deploy support for it, then remove the old one.

Headers on every delivery

HeaderDescription
X-Webhook-EventEvent type, for example document.completed.
X-Webhook-Delivery-IdUnique per delivery. Store it — it is the lookup key for the Deliveries endpoint.
X-Webhook-TimestampISO-8601 send time.
X-Authorization-DigestAlways HMACSHA256.
X-Webhook-Signature-1Base64 HMAC-SHA256 of the raw body, computed with active secret #1.
X-Webhook-Signature-2Present when a second secret is active (during rotation).

Payload

document.completed
{
  "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.metadata echoes the metadata object you passed to /send-envelop. It is null for any envelope created in the web app, because only the API path writes it — handle null rather than assuming your keys are present.
  • Watch the spelling: /send-envelop returns the id as envelope_id, while the webhook payload carries the same value as data.envelop_id. Same identifier, different field name.
  • The payload carries no per-recipient detail. For who signed, who declined and why, call /envelop-tracking on receipt.

Pausing loses events

Setting a configuration to paused or inactive stops delivery immediately. Events that occur while it is paused are not queued or backfilled — they are lost, and re-activating does not replay them. Use /envelop-tracking to catch up on anything sent during a pause.

Download links expire

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.

Verifying Webhook Signatures

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.

Validate against the raw body

Re-encoding the JSON changes the digest and every signature check will fail. Read the raw bytes before any framework parses them — php://input in PHP, express.raw() in Express, request.body (bytes) in Django/FastAPI.

What the body looks like on the wire

The payload examples on this page are formatted for reading. The real request body is compact — no newlines, no indentation — with forward slashes unescaped (https://…, never https:\/\/…) and no trailing newline. The signature is computed over exactly those bytes.

Actual bytes signed
{"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/...","mime_type":"application/pdf"}},"timestamp":"2026-08-03T10:24:11.482000Z"}

This is why you must hash the raw request stream and never a re-serialised object: your encoder will almost certainly produce different bytes — Python and PHP add a space after : and ,, and PHP's default json_encode escapes slashes — and the digest will not match.

Testing with a webhook inspector

Request-inspection tools usually display the body pretty-printed and may copy it that way. Disable any formatting or beautify option before copying a body you intend to verify against — the reformatted copy will not match the signature.
PHP
<?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]);
Node.js / Express
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
  },
);

Retries & Idempotency

What happens when your endpoint is down.

Delivery is retried up to four attempts. Respond 2xx quickly to acknowledge — anything else, or a timeout past 15 seconds, marks the delivery failed and schedules the next attempt.

AttemptSent afterElapsed
1immediately0
260 seconds~1 min
35 minutes~6 min
4 (last)15 minutes~21 min

One attempt can be several requests

Each of the four attempts makes up to 3 HTTP requests — two immediate retries, 500 ms apart, on a connection error or a non-2xx. A fully failing delivery can therefore reach your endpoint up to 12 times, and three of those can arrive inside the same second. Deduplicate on data.envelop_id plus event so repeats are ignored safely.

Make your handler idempotent

A retry can arrive after your side has already processed the event. Key your processing on data.envelop_id plus event, and ignore anything you have already seen. Do not deduplicate on X-Webhook-Delivery-Id: that id is unique per delivery attempt, so every retry carries a different one and you would process the same completion up to four times. Store it for diagnostics — it is the lookup key for the Deliveries endpoint — never as your dedupe key.
  • Acknowledge first, process second — return 200 and hand the payload to a queue rather than doing the work inline.
  • Use POST /webhook/deliveries with a delivery id to see exactly what was sent, what your endpoint returned, and how long it took. Each attempt is its own record, so look up the id from the attempt you want to inspect.
  • If you miss an event entirely, /envelop-tracking always reflects the current state — it is the reliable fallback.

An abandoned delivery is final

After the fourth attempt the delivery is abandoned. There is no replay endpoint, no way to re-trigger an event for an envelope that already produced one, and no test-ping — the only way to exercise your endpoint end to end is to send and complete a real envelope. For that reason, /envelop-tracking reconciliation is not optional in production: poll it for envelopes you have sent but not yet seen a terminal event for, and treat the webhook as the fast path rather than the only path.

Errors & Troubleshooting

Status codes and the fix for each.

StatusMeaning
200Success. Read the payload from data.
201Created — returned by Insert Envelope Template Fields on success.
204No template maps to the supplied template_id. Per HTTP semantics the body is not transmitted, so branch on the status code, not the body.
401Api-Key missing, revoked, or not owned by the tenant in X-Tenant.
404The tenant in X-Tenant does not exist, the key's user has no email on record, the envelope / delivery id was not found, or Send Envelope's user_id / template_id does not match any existing record.
422Validation failed, a required field was missing, or — on Send Envelope — recipients_role does not map onto the template's roles (wrong count, unknown role, or wrong order for a sequence template). msg holds a field → messages map naming what is wrong.
500An unexpected error occurred while processing the request. error carries the reason.
503A dependency needed to authenticate the request was unreachable. Safe to retry after a short backoff.

Common integration mistakes

SymptomCause and fix
422 — 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. For a sequence template they must also be in the template's own signing order — role 1 first.
404 — Template [...] does not exist / User [...] does not existtemplate_id or user_id does not match any record for the current tenant. Double-check the id and that you are calling the right environment (UAT vs. production).
422 — Could not provision recipient [...]A recipient in recipients_role doesn't have an account yet, and the identity service rejected the new-user details — most commonly an email address it considers undeliverable (e.g. @example.com). Use a real, deliverable recipient email.
422 on Send Envelope with no obvious missing fieldmessage 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 workedThe 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 fetchedPre-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 matchesYou 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 503sAn upstream authentication dependency was briefly unreachable. Retry with exponential backoff; the request was not processed.
Envelope completes but the endpoint is never calledAlmost always a sender mismatch — the webhook was configured by a different user than the user_id used in /send-envelop. Also confirm the configuration is active and the event is subscribed.
Signature verifies live but not against a captured sampleThe captured body was reformatted. Pretty-printers, copy-as-JSON buttons and editors that append a trailing newline all change the bytes. Re-capture raw.
The same completion is processed several timesYou are deduplicating on X-Webhook-Delivery-Id, which is unique per attempt. Key on data.envelop_id plus event.

Best Practices

What production integrations get right.

  • Keep the API key server-side. Store it in a secret manager, rotate it from Settings → Key Management, and never expose it to a browser or mobile client.
  • Persist every identifier. Save envelope_id on send and X-Webhook-Delivery-Id on receipt — the first is your tracking key, the second your debugging key. Deduplicate on data.envelop_id plus event, never on the delivery id.
  • Treat pre-signed URLs as single-use. Template previews last 10 minutes, signed documents 15 minutes. Download to your own storage on receipt.
  • Use 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.
  • Read template roles at runtime. Templates change. Fetching recipients_role before each send stops a template edit from breaking your integration.
  • Rotate webhook secrets with two active keys. Add the new key, accept both signatures, then remove the old one — no missed deliveries.
  • Retry 503 and network failures, never 422. A 422 will fail identically until you fix the payload.
  • Webhooks for outcomes, tracking for progress. The two events fire only when an envelope reaches a terminal state. There are no events for envelope sent, email delivered, document viewed, or an individual signer finishing in a multi-signer envelope. If your UI needs progress before completion, read /envelop-tracking, which exposes per-recipient status, viewed_at and completed_at.