Quickstart
In this guide you connect a script to your MyCLM provider, send a PDF for signature and get notified when it is signed. It takes about ten minutes.
You need: an account with the Integrations permission (integrations.manage, owners and administrators have it) on your organization, a PDF file, and curl, Node.js 20+, Python 3 or PHP 8.
-
Create an API key
Section titled “Create an API key”In the app, open Settings → Integrations → API keys and click New API key.
- Name: what the key is for, e.g.
CRM sync. - Scope: the entity (or group) the key works in. It sees what a member attached there would see.
- Role: the rights of the key, e.g. Member (can send signature requests). A key never gets more rights than yours.
- Expiry and Allowed IP addresses are optional but recommended in production.
Copy the Client ID, Client secret, Token URL and Scope. The secret is shown once: store it in your secrets manager.
Terminal window export MLT_API_URL=http://localhost:3100export MLT_TOKEN_URL='<Token URL>'export MLT_CLIENT_ID='<Client ID>'export MLT_CLIENT_SECRET='<Client secret>'export MLT_SCOPE='<Scope>' - Name: what the key is for, e.g.
-
Get an access token
Section titled “Get an access token”API keys use the OAuth 2
client_credentialsgrant on the identity provider of your MyCLM provider.Terminal window export MLT_TOKEN=$(curl -s -X POST "$MLT_TOKEN_URL" \-u "$MLT_CLIENT_ID:$MLT_CLIENT_SECRET" \-d grant_type=client_credentials \--data-urlencode "scope=$MLT_SCOPE" | jq -r .access_token)const basic = Buffer.from(`${process.env.MLT_CLIENT_ID}:${process.env.MLT_CLIENT_SECRET}`).toString('base64');const response = await fetch(process.env.MLT_TOKEN_URL!, {method: 'POST',headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' },body: new URLSearchParams({ grant_type: 'client_credentials', scope: process.env.MLT_SCOPE! }),});const { access_token: token, expires_in: expiresIn } = await response.json();import os, requestsresponse = requests.post(os.environ["MLT_TOKEN_URL"],auth=(os.environ["MLT_CLIENT_ID"], os.environ["MLT_CLIENT_SECRET"]),data={"grant_type": "client_credentials", "scope": os.environ["MLT_SCOPE"]},)response.raise_for_status()token = response.json()["access_token"]<?phprequire 'vendor/autoload.php'; // composer require guzzlehttp/guzzle$http = new GuzzleHttp\Client();$response = $http->post(getenv('MLT_TOKEN_URL'), ['auth' => [getenv('MLT_CLIENT_ID'), getenv('MLT_CLIENT_SECRET')],'form_params' => ['grant_type' => 'client_credentials', 'scope' => getenv('MLT_SCOPE')],]);$token = json_decode((string) $response->getBody(), true)['access_token'];The token is valid for 10 minutes (
expires_in, in seconds): cache it and reuse it until it expires. -
Find the entity to work in
Section titled “Find the entity to work in”Resources belong to an entity.
GET /v1/metells you where your key can work: takescopes[].entityId.Terminal window curl -s "$MLT_API_URL/v1/me" -H "Authorization: Bearer $MLT_TOKEN" | jq '.scopes[] | {entityId, entityName, permissions}'const me = await fetch(`${process.env.MLT_API_URL}/v1/me`, {headers: { Authorization: `Bearer ${token}` },}).then((r) => r.json());const entityId = me.scopes[0].entityId;api = os.environ["MLT_API_URL"]headers = {"Authorization": f"Bearer {token}"}me = requests.get(f"{api}/v1/me", headers=headers).json()entity_id = me["scopes"][0]["entityId"]$api = getenv('MLT_API_URL');$headers = ['Authorization' => "Bearer $token"];$me = json_decode((string) $http->get("$api/v1/me", ['headers' => $headers])->getBody(), true);$entityId = $me['scopes'][0]['entityId']; -
Send a signature request
Section titled “Send a signature request”The request is
multipart/form-data: afilepart with the PDF and adatapart with the options as a JSON string.Terminal window curl -s -X POST "$MLT_API_URL/v1/signature-requests" \-H "Authorization: Bearer $MLT_TOKEN" \-F 'data={"entityId":"<entityId>","title":"Bail - Lot 12","signers":[{"firstName":"Marie","lastName":"Durand","email":"marie.durand@example.fr"}]}' \-F 'file=@bail-lot-12.pdf;type=application/pdf'import { openAsBlob } from 'node:fs';const form = new FormData();form.append('data', JSON.stringify({entityId,title: 'Bail - Lot 12',signers: [{ firstName: 'Marie', lastName: 'Durand', email: 'marie.durand@example.fr' }],}));form.append('file', await openAsBlob('bail-lot-12.pdf', { type: 'application/pdf' }), 'bail-lot-12.pdf');const request = await fetch(`${process.env.MLT_API_URL}/v1/signature-requests`, {method: 'POST',headers: { Authorization: `Bearer ${token}` },body: form,}).then((r) => r.json());console.log(request.id, request.status); // "sent"import jsonwith open("bail-lot-12.pdf", "rb") as pdf:response = requests.post(f"{api}/v1/signature-requests",headers=headers,data={"data": json.dumps({"entityId": entity_id,"title": "Bail - Lot 12","signers": [{"firstName": "Marie", "lastName": "Durand", "email": "marie.durand@example.fr"}],})},files={"file": ("bail-lot-12.pdf", pdf, "application/pdf")},)response.raise_for_status()request = response.json()print(request["id"], request["status"]) # "sent"$response = $http->post("$api/v1/signature-requests", ['headers' => $headers,'multipart' => [['name' => 'data', 'contents' => json_encode(['entityId' => $entityId,'title' => 'Bail - Lot 12','signers' => [['firstName' => 'Marie', 'lastName' => 'Durand', 'email' => 'marie.durand@example.fr']],])],['name' => 'file', 'contents' => fopen('bail-lot-12.pdf', 'r'), 'filename' => 'bail-lot-12.pdf', 'headers' => ['Content-Type' => 'application/pdf']],],]);$request = json_decode((string) $response->getBody(), true);echo $request['id'], ' ', $request['status']; // "sent"The response is the signature request with
status: "sent". Marie receives an email with a signing link. -
Receive the webhook
Section titled “Receive the webhook”In Settings → Integrations → Webhooks, click New subscription, enter your HTTPS endpoint and select
signature.signedandsignature.completed. Copy the signing secret (shown once) and click Send a test event to check your endpoint.When Marie signs, your endpoint receives:
{"id": "0192f3b0-12c4-7e8a-b3d1-5f6a7b8c9d0e","type": "signature.completed","apiVersion": "1","createdAt": "2026-10-01T10:12:45.000Z","providerId": "0192f3a1-7c2e-7b4d-9a61-100000000001","orgId": "0192f3a1-7c2e-7b4d-9a61-200000000001","data": { "requestId": "0192f3a1-7c2e-7b4d-9a61-400000000001", "title": "Bail - Lot 12" }}Always verify the
X-MLT-Signatureheader before trusting a payload, and answer2xxwithin 10 seconds. -
Download the signed document
Section titled “Download the signed document”Terminal window curl -s "$MLT_API_URL/v1/signature-requests/<id>/document?variant=signed" \-H "Authorization: Bearer $MLT_TOKEN" -o bail-lot-12-signed.pdfimport { writeFile } from 'node:fs/promises';const pdf = await fetch(`${process.env.MLT_API_URL}/v1/signature-requests/${request.id}/document?variant=signed`, {headers: { Authorization: `Bearer ${token}` },});await writeFile('bail-lot-12-signed.pdf', Buffer.from(await pdf.arrayBuffer()));pdf = requests.get(f"{api}/v1/signature-requests/{request['id']}/document", params={"variant": "signed"}, headers=headers)pdf.raise_for_status()open("bail-lot-12-signed.pdf", "wb").write(pdf.content)$http->get("$api/v1/signature-requests/{$request['id']}/document", ['headers' => $headers,'query' => ['variant' => 'signed'],'sink' => 'bail-lot-12-signed.pdf',]);
Next steps
Section titled “Next steps”- Signature requests guide: signing order, reminders, eIDAS levels.
- Webhooks: event catalog, retries, replay.
- Errors and Rate limits before going to production.