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 accountPOST /get-template-by-user
Read a template's recipient roles before sendingPOST /get-template-detail
Send a document for signature from your own appPOST /send-envelop
Show signing progress per recipient in your UIPOST /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 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" }'

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 flip one host segment for production.

EnvironmentAPI base URLWeb app
UAT (sandbox)https://services.us.uat.mochatechnologies.com/signature/api/V1app.uat.mochatechnologies.com
Productionhttps://services.us.mochatechnologies.com/signature/api/V1app.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.

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 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
<?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'] ?? [];
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;
}

const { data: templates } = await callMochaSignature("/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"],
    "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"]

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 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,
});

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.

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"
}

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.
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, marks the delivery failed and schedules the next attempt.

AttemptSent after
1Immediately
260 seconds
35 minutes
415 minutes
Final30 minutes

Make your handler idempotent

A retry can arrive after your side has already processed the event. Key your processing on X-Webhook-Delivery-Id (or on data.envelop_id plus event) and ignore duplicates.
  • Acknowledge first, process second — return 200 and hand the payload to a queue rather than doing the work inline.
  • Use 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.
  • If you miss an event entirely, /envelop-tracking always reflects the current state — it is the reliable fallback.

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, or the envelope / delivery id was not found.
422Validation failed. msg holds a field → messages map naming what is wrong.
500Processing failed — for example the template could not be verified or the recipient roles could not be mapped. 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
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 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.

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 — both are the only keys you have for tracking and for debugging deliveries later.
  • 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.
  • Prefer webhooks over polling. Use /envelop-tracking for on-demand status in your UI and as a reconciliation fallback, not as a polling loop.