Skip to content

Generate a Contract with the API

Your own system already holds the deal data — the CRM, the procurement tool, the internal request app. Rekeying it into a generation form to produce a contract is duplicate work, and it does not scale to a few hundred agreements.

The Pactly API generates a contract from one of your templates in a single call. You send the values, you get the contract back. This guide walks the whole path: key, template, values, generation, download.

  • A template that already produces the contract you want. The API generates from the same templates the interface uses; it does not create or edit them.
  • Permission to create an API key in your account.
  • Anything that can send an HTTPS request. The examples here use curl.

Every request goes to https://clientapi.pactly.ai and carries your key in an x-api-key header. The full endpoint reference lives at api-docs.pactly.com.

Step 1 of 3
1
Open Account Settings

Sign in to Pactly, open the account menu, and go to Account Settings.

2
Go to API Keys

Select the API Keys section.

3
Create the key

Click Create New API Key, name it after the system that will use it, and confirm. The key is shown only once — copy it and store it in your secret manager. You cannot view it again.

Step 1 of 3

The key is scoped to your company. It reaches your templates and the contracts generated from them, and nothing belonging to anyone else.

Confirm it works before going further:

Terminal window
curl -s https://clientapi.pactly.ai/v1/test \
-H "x-api-key: YOUR_API_KEY"

A valid key returns {"message":"It works!"}. A 401 means the key is wrong, revoked, or missing from the header.

List the templates your company has:

Terminal window
curl -s https://clientapi.pactly.ai/v1/templates \
-H "x-api-key: YOUR_API_KEY"

Each entry carries the id you generate from, along with everything that template accepts:

[
{
"id": "507f1f77bcf86cd799439013",
"name": "Software License Agreement",
"version": "2.1.0",
"description": "Standard licensing agreement",
"createdAt": "2026-06-15T08:00:00Z",
"variables": [ ... ],
"valuemaps": [ ... ],
"parties": [ ... ]
}
]

Store the id. Template ids are stable; do not look the template up by name on every run, because renaming a template in Pactly would silently break your integration.

Step 3: See what values the template takes

Section titled “Step 3: See what values the template takes”

A template accepts three kinds of input, and all three are listed on the template record. You send them in one flat values map, keyed by id.

A variable prints a single value into the document.

{
"id": "SupplierName",
"question": "Who is supplying?",
"valueType": "string",
"required": true,
"default": null,
"conditional": false
}
FieldWhat it tells you
idThe key to use in values
valueTypestring, number, or boolean. Types are checked, not coerced — sending 24 for a string is rejected
requiredGeneration fails with a 400 when this is missing
defaultWhat the template author suggests. Not applied automatically — send it yourself or the spot renders empty
conditionaltrue when the variable switches a block of text in or out rather than printing a value. Send a real boolean

A valuemap is a fixed set of choices. Send the label, and Pactly resolves it to the underlying text.

{
"id": "ContractingEntity",
"question": "Which entity is contracting?",
"allowMultipleSelection": false,
"choices": ["Singapore - Acme Holdings", "UK - Acme Ltd"]
}

Send one of the strings from choices exactly. When allowMultipleSelection is true, send an array of labels instead of a single one.

A party is a contact the document prints details for. Send an object containing the attributes that party declares.

{
"id": "party1",
"label": "Supplier",
"type": "entity",
"attributes": ["entityName", "entityRegNo", "country", "address"]
}

attributes is the complete accepted list — any other key is rejected. When type is "unknown", the template has not decided whether this party is a company or a person, so send "type": "entity" or "type": "individual" alongside the attributes. Leave it out and the document’s entity-specific and individual-specific clauses have nothing to work from.

Post the template id and the values map:

Terminal window
curl -s -X POST https://clientapi.pactly.ai/v1/contracts/generate \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "507f1f77bcf86cd799439013",
"contractName": "Acme - Software License 2026",
"values": {
"SupplierName": "Acme Pte Ltd",
"Term": 24,
"Agency": false,
"ContractingEntity": "Singapore - Acme Holdings",
"party1": {
"entityName": "Acme Pte Ltd",
"entityRegNo": "196800306E",
"country": "Singapore",
"address": "1 Raffles Place, Singapore 048616"
}
}
}'

contractName is optional and defaults to “Pactly contract”. Anything you leave out of values renders as the [●] placeholder rather than failing — the same marker you would see from a half-filled generation form.

Three optional flags in options control the behaviour. They are per request, so the same key can persist one contract and preview the next.

FlagDefaultWhat it does
persisttrueKeeps the contract in Pactly, where it appears in the register like any other. false stores nothing — no contract, no file, no timeline entry — and returns the document in the response
triggerWorkflowsfollows persistRuns your templateContractCreated workflows. This sends real email to real people, so set it to false for a bulk import. It cannot be true when persist is false — a workflow has no contract to act on
referenceYour own identifier, printed wherever the template uses the {{__reference}} token. Accepted only with "persist": false; Pactly allocates its own reference for contracts it keeps

The response follows persist, and persisted tells you which one you got.

Persisted (the default) — a contract record. The document is fetched separately in step 5:

{
"persisted": true,
"id": "507f1f77bcf86cd799439011",
"name": "Acme - Software License 2026",
"reference": "SLA-2026-014",
"template": "507f1f77bcf86cd799439013",
"company": "507f1f77bcf86cd799439010",
"createdAt": "2026-08-19T10:30:00Z",
"effectiveDate": null,
"values": { ... }
}

Ephemeral ("persist": false) — the document itself, because nothing was stored and there is no id to come back for:

{
"persisted": false,
"name": "Acme - Software License 2026",
"template": "507f1f77bcf86cd799439013",
"reference": "PO-2026-0042",
"fileName": "Acme - Software License 2026.docx",
"contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"document": "UEsDBBQABgAIAAAAIQ..."
}

document is the .docx, base64-encoded. Decode it and you have the file:

Terminal window
curl -s -X POST https://clientapi.pactly.ai/v1/contracts/generate \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"templateId":"507f1f77bcf86cd799439013","options":{"persist":false}}' \
| python3 -c "import json,sys,base64; r=json.load(sys.stdin); open(r['fileName'],'wb').write(base64.b64decode(r['document']))"

For a persisted contract, fetch the file with the id from step 4:

Terminal window
curl -s https://clientapi.pactly.ai/v1/contracts/507f1f77bcf86cd799439011/download \
-H "x-api-key: YOUR_API_KEY" \
-o contract.docx

The contract is now a normal Pactly contract. It shows up in the register, it can be sent for approval or signature, and your team can edit it in the Word add-in — nothing about it is second-class for having arrived through the API.

Errors return a JSON body naming the problem rather than a generic failure.

StatusMeansDo this
400A value is the wrong type, a valuemap label is not in choices, a required variable is missing, or the body has a key that is not templateId, contractName, values or optionsRead the message — it names the offending field. Re-check the ids against the template record
401The key is missing, wrong, or revokedConfirm the x-api-key header, then re-issue the key
404The template id does not exist in your companyCheck for a typo; a valid id from another tenant reads as not found
429The generation budget for this key is used upBack off and retry — see the limits below

Generation is bounded per API key, not per IP, so several integrations behind one outbound address do not compete:

  • 300 generations per hour
  • 3 running at once, with a short queue for the rest
  • A minimum spacing of 200 ms between calls

Comfortable for a live integration, and enough headroom for a bulk load if you pace it. Treat a 429 as “slow down and retry”, not as a failure of the request.

That placeholder marks a spot the template expected to fill and could not — almost always a key missing from your values map, or an id that does not match the template. Generation never fails on a missing value; it inserts the marker and carries on. Compare the keys you sent against variables, valuemaps and parties on the template record.

Chat with us

We typically reply within a few minutes