Create a Template Contract and Sync Data Back
Goal
Create a SpotDraft contract from your source system, keep your own identifiers attached to it, and sync the final contract status, key data, and signed document back after execution.
When to use this
Use this when your CRM, ERP, procurement system, or internal workflow owns the business record and SpotDraft owns drafting, signature, and the final repository copy.
The normal production flow is:
- your system creates the contract in SpotDraft
- your system stores the SpotDraft contract id alongside its own external id
- webhook events drive downstream sync
- your system pulls final metadata and the executed file only when needed
Endpoints used
POST /api/v2.1/public/contracts/POST /api/v2.1/public/contracts/{composite_id}/send_to_counterpartiesGET /api/v2.1/public/contracts/{composite_id}/statusGET /api/v2.1/public/contracts/{composite_id}/key_pointersfor contract metadataPOST /api/v2.1/public/contracts/{composite_id}/download_link
Prerequisites
- a valid public API key
- a published template-backed contract flow in SpotDraft
- the
contract_template_idfor that template - the contract data and counterparty data available in your source system
- a stable identifier from your system, such as a CRM deal id or ERP request id
Overview
This flow depends on workspace-specific template configuration:
contract_template_idmust resolve in the target workspacecontract_datakeys must match that template's configured field names
Treat the values below as an example shape, not a globally valid payload.
Treat organization_type as a workspace-specific value, not a universal enum. If your workspace uses a different label, send that exact value instead of copying "company" literally.
1. Create the contract from structured data
Write your upstream record id into external_metadata.id. That is the lookup key you should later use for webhooks, reconciliation, and support debugging.
- Python
- Node.js
- curl
import requests
base_url = "https://api.in.spotdraft.com" # Replace with your workspace region.
headers = {
"client-id": SPOTDRAFT_CLIENT_ID,
"client-secret": SPOTDRAFT_CLIENT_SECRET,
"Content-Type": "application/json",
}
payload = {
"contract_template_id": 1204, # Replace with a template your workspace can access.
"contract_name": "Acme Master Services Agreement",
"contract_data": {
"agreement_date": "2026-04-20",
"customer_name": "Acme Inc.",
"billing_currency": {"type": "USD", "value": 12000},
"subscription_term": {"type": "MONTHS", "value": 12, "days": 365},
"auto_renewal": True,
"notice_period_days": 30,
},
"counter_party_details": [
{
"is_individual": False,
"organization_type": "company",
"organization_name": "Acme Inc.",
"poc_details": {
"first_name": "Avery",
"last_name": "Stone",
"email": "legal@acme.example",
},
"organization_details": {
"jurisdiction_iso_code": "US",
"address": {
"line_one": "350 Fifth Avenue",
"city": "New York",
"state": "NY",
"country_iso_code": "US",
"zipcode": "10118",
},
},
}
],
"external_metadata": {
"id": "hubspot-deal-10024",
"integration_name": "HubSpot",
"record_type": "Deal",
"record_data": {
"deal_stage": "contractsent",
"owner_email": "owner@acme.example",
},
},
}
response = requests.post(
f"{base_url}/api/v2.1/public/contracts/",
headers=headers,
json=payload,
timeout=30,
)
response.raise_for_status()
contract = response.json()
const baseUrl = 'https://api.in.spotdraft.com'; // Replace with your workspace region.
const headers = {
'client-id': process.env.SPOTDRAFT_CLIENT_ID,
'client-secret': process.env.SPOTDRAFT_CLIENT_SECRET,
'Content-Type': 'application/json',
};
const payload = {
contract_template_id: 1204, // Replace with a template your workspace can access.
contract_name: 'Acme Master Services Agreement',
contract_data: {
agreement_date: '2026-04-20',
customer_name: 'Acme Inc.',
billing_currency: {type: 'USD', value: 12000},
subscription_term: {type: 'MONTHS', value: 12, days: 365},
auto_renewal: true,
notice_period_days: 30,
},
counter_party_details: [
{
is_individual: false,
organization_type: 'company',
organization_name: 'Acme Inc.',
poc_details: {
first_name: 'Avery',
last_name: 'Stone',
email: 'legal@acme.example',
},
organization_details: {
jurisdiction_iso_code: 'US',
address: {
line_one: '350 Fifth Avenue',
city: 'New York',
state: 'NY',
country_iso_code: 'US',
zipcode: '10118',
},
},
},
],
external_metadata: {
id: 'hubspot-deal-10024',
integration_name: 'HubSpot',
record_type: 'Deal',
record_data: {
deal_stage: 'contractsent',
owner_email: 'owner@acme.example',
},
},
};
const response = await fetch(`${baseUrl}/api/v2.1/public/contracts/`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Create contract failed: ${response.status}`);
}
const contract = await response.json();
curl --request POST \
--url https://api.in.spotdraft.com/api/v2.1/public/contracts/ \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{
"contract_template_id": 1204,
"contract_name": "Acme Master Services Agreement",
"contract_data": {
"agreement_date": "2026-04-20",
"customer_name": "Acme Inc.",
"billing_currency": {"type": "USD", "value": 12000},
"subscription_term": {"type": "MONTHS", "value": 12, "days": 365},
"auto_renewal": true,
"notice_period_days": 30
},
"counter_party_details": [
{
"is_individual": false,
"organization_type": "company",
"organization_name": "Acme Inc.",
"poc_details": {
"first_name": "Avery",
"last_name": "Stone",
"email": "legal@acme.example"
},
"organization_details": {
"jurisdiction_iso_code": "US",
"address": {
"line_one": "350 Fifth Avenue",
"city": "New York",
"state": "NY",
"country_iso_code": "US",
"zipcode": "10118"
}
}
}
],
"external_metadata": {
"id": "hubspot-deal-10024",
"integration_name": "HubSpot",
"record_type": "Deal",
"record_data": {
"deal_stage": "contractsent",
"owner_email": "owner@acme.example"
}
}
}'
2. Send the contract to counterparties
Once the contract is ready for signature, move it forward explicitly.
Replace T-1234 with the id returned from the create call.
- Python
- Node.js
- curl
requests.post(
f"{base_url}/api/v2.1/public/contracts/T-1234/send_to_counterparties",
headers=headers,
json={},
timeout=30,
).raise_for_status()
const sendResponse = await fetch(
`${baseUrl}/api/v2.1/public/contracts/T-1234/send_to_counterparties`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
},
);
if (!sendResponse.ok) {
throw new Error(`Send to counterparties failed: ${sendResponse.status}`);
}
curl --request POST \
--url https://api.in.spotdraft.com/api/v2.1/public/contracts/T-1234/send_to_counterparties \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{}'
3. Track completion from webhooks, then reconcile if needed
The normal production path is:
- register lifecycle webhooks for the contract events your integration cares about
- use those deliveries to trigger downstream sync
- call the status endpoint only when a delivery needs repair, validation, or bounded reconciliation
That keeps the integration event-driven without forcing you to trust webhook delivery as the only operational safety net.
- Python
- Node.js
- curl
status_response = requests.get(
f"{base_url}/api/v2.1/public/contracts/T-1234/status",
headers=headers,
timeout=30,
)
status_response.raise_for_status()
status_payload = status_response.json()
const statusResponse = await fetch(
`${baseUrl}/api/v2.1/public/contracts/T-1234/status`,
{headers},
);
if (!statusResponse.ok) {
throw new Error(`Status fetch failed: ${statusResponse.status}`);
}
const statusPayload = await statusResponse.json();
curl --request GET \
--url https://api.in.spotdraft.com/api/v2.1/public/contracts/T-1234/status \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET"
4. Pull contract metadata back
After the contract is executed, fetch the extracted contract metadata your downstream systems care about. The API endpoint still uses the historical key_pointers path name.
- Python
- Node.js
- curl
key_pointer_response = requests.get(
f"{base_url}/api/v2.1/public/contracts/T-1234/key_pointers",
headers=headers,
timeout=30,
)
key_pointer_response.raise_for_status()
contract_metadata = key_pointer_response.json()
const keyPointerResponse = await fetch(
`${baseUrl}/api/v2.1/public/contracts/T-1234/key_pointers`,
{headers},
);
if (!keyPointerResponse.ok) {
throw new Error(`Key pointer fetch failed: ${keyPointerResponse.status}`);
}
const contractMetadata = await keyPointerResponse.json();
curl --request GET \
--url https://api.in.spotdraft.com/api/v2.1/public/contracts/T-1234/key_pointers \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET"
5. Generate a signed document download link
Use a signed URL instead of scraping the SpotDraft UI or storing an app-only link in your system.
- Python
- Node.js
- curl
download_response = requests.post(
f"{base_url}/api/v2.1/public/contracts/T-1234/download_link",
headers=headers,
json={"format": "pdf"},
timeout=30,
)
download_response.raise_for_status()
download_link = download_response.json()
const downloadResponse = await fetch(
`${baseUrl}/api/v2.1/public/contracts/T-1234/download_link`,
{
method: 'POST',
headers,
body: JSON.stringify({format: 'pdf'}),
},
);
if (!downloadResponse.ok) {
throw new Error(`Download link generation failed: ${downloadResponse.status}`);
}
const downloadLink = await downloadResponse.json();
curl --request POST \
--url https://api.in.spotdraft.com/api/v2.1/public/contracts/T-1234/download_link \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{"format": "pdf"}'
Production checklist
- your own upstream identifier in
external_metadata.id - the SpotDraft
id - the latest contract status from webhook-driven sync
- the download URL expiry or your own copied file location for the executed file
- any contract metadata that drives downstream workflows, such as effective date or contract value
Production notes
POST /api/v2.1/public/contracts/requirescontract_data, even if the selected template has very few fields.- For non-individual counterparties, send
organization_typeandorganization_nameconsistently.organization_typeis workspace-specific. - Treat
GET /statusas operational visibility. Prefer webhooks over polling for long-running workflows.
Common failure points
-
inaccessible template id A template id from another workspace or environment will fail even if the shape of the request is valid.
-
template field mismatch
contract_datakeys must match the selected template's field names exactly. -
creator entity mismatch Some contract types enforce creator-side entity constraints. If your workspace uses multiple entities, validate that the chosen template supports the creator entity you expect.