Working with Templates
Templates let you create reusable document structures with predefined signature fields, making it easy to send consistent documents at scale.
When to Use Templates
| Use Case | Recommendation |
|---|---|
| Same document sent frequently | Template - Define once, reuse |
| Different document each time | Direct upload - Use documentBase64 |
| Consistent field placement | Template - Predefined positions |
| Dynamic field positions | Anchor tags - Text-based positioning |
| High-volume sending | Template - Better performance |
Template Lifecycle
Creating a Template
Step 1: Upload the Template Document
- cURL
- JavaScript
- Python
# Base64 encode your template PDF
TEMPLATE_BASE64=$(base64 -i employment-template.pdf)
curl -X POST "https://api.propper.ai/restapi/v2.1/accounts/{accountId}/templates" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Employment Contract\",
\"description\": \"Standard employment agreement template\",
\"documents\": [{
\"documentId\": \"1\",
\"name\": \"Employment Contract.pdf\",
\"documentBase64\": \"$TEMPLATE_BASE64\"
}],
\"recipients\": {
\"signers\": [
{
\"roleName\": \"Employee\",
\"recipientId\": \"1\",
\"routingOrder\": \"1\",
\"tabs\": {
\"signHereTabs\": [{
\"documentId\": \"1\",
\"pageNumber\": \"1\",
\"xPosition\": \"100\",
\"yPosition\": \"600\",
\"width\": \"200\",
\"height\": \"50\"
}],
\"dateSignedTabs\": [{
\"documentId\": \"1\",
\"pageNumber\": \"1\",
\"xPosition\": \"350\",
\"yPosition\": \"600\"
}],
\"textTabs\": [{
\"documentId\": \"1\",
\"pageNumber\": \"1\",
\"xPosition\": \"100\",
\"yPosition\": \"200\",
\"tabLabel\": \"employeeName\",
\"width\": \"200\"
}]
}
},
{
\"roleName\": \"Manager\",
\"recipientId\": \"2\",
\"routingOrder\": \"2\",
\"tabs\": {
\"signHereTabs\": [{
\"documentId\": \"1\",
\"pageNumber\": \"1\",
\"xPosition\": \"100\",
\"yPosition\": \"700\"
}]
}
}
]
}
}"
import fs from 'fs/promises';
const createTemplate = async (accountId, token) => {
const templatePdf = await fs.readFile('./employment-template.pdf');
const templateBase64 = templatePdf.toString('base64');
const response = await fetch(
`https://api.propper.ai/restapi/v2.1/accounts/${accountId}/templates`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Employment Contract',
description: 'Standard employment agreement template',
documents: [{
documentId: '1',
name: 'Employment Contract.pdf',
documentBase64: templateBase64,
}],
recipients: {
signers: [
{
roleName: 'Employee',
recipientId: '1',
routingOrder: '1',
tabs: {
signHereTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '600',
width: '200',
height: '50',
}],
dateSignedTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '350',
yPosition: '600',
}],
textTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '200',
tabLabel: 'employeeName',
width: '200',
}],
},
},
{
roleName: 'Manager',
recipientId: '2',
routingOrder: '2',
tabs: {
signHereTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '700',
}],
},
},
],
},
}),
}
);
return response.json();
};
const template = await createTemplate(accountId, token);
console.log('Template ID:', template.templateId);
import base64
from pathlib import Path
def create_template(account_id: str, token: str) -> dict:
template_content = Path("./employment-template.pdf").read_bytes()
template_base64 = base64.b64encode(template_content).decode("utf-8")
response = requests.post(
f"https://api.propper.ai/restapi/v2.1/accounts/{account_id}/templates",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"name": "Employment Contract",
"description": "Standard employment agreement template",
"documents": [{
"documentId": "1",
"name": "Employment Contract.pdf",
"documentBase64": template_base64,
}],
"recipients": {
"signers": [
{
"roleName": "Employee",
"recipientId": "1",
"routingOrder": "1",
"tabs": {
"signHereTabs": [{
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "600",
"width": "200",
"height": "50",
}],
"dateSignedTabs": [{
"documentId": "1",
"pageNumber": "1",
"xPosition": "350",
"yPosition": "600",
}],
"textTabs": [{
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "200",
"tabLabel": "employeeName",
"width": "200",
}],
},
},
{
"roleName": "Manager",
"recipientId": "2",
"routingOrder": "2",
"tabs": {
"signHereTabs": [{
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "700",
}],
},
},
],
},
},
)
response.raise_for_status()
return response.json()
template = create_template(account_id, token)
print(f"Template ID: {template['templateId']}")
Response:
{
"templateId": "tpl_abc123xyz",
"name": "Employment Contract",
"uri": "/restapi/v2.1/accounts/.../templates/tpl_abc123xyz"
}
Field Types Reference
| Type | Description | Common Properties |
|---|---|---|
signHereTabs | Signature field | xPosition, yPosition, width, height |
initialHereTabs | Initials field | xPosition, yPosition |
dateSignedTabs | Auto-filled signing date | xPosition, yPosition |
textTabs | Free-form text input | tabLabel, value, required, width |
checkboxTabs | Checkbox | tabLabel, selected |
dropdownTabs | Dropdown selection | tabLabel, listItems |
numberTabs | Numeric input | tabLabel, validationPattern |
emailTabs | Email input | tabLabel |
Field Positioning
Coordinate-Based Positioning
Fields are positioned using x/y coordinates from the bottom-left corner of the page:
┌─ ────────────────────────────────────┐
│ │ ↑
│ (100, 600) │ │
│ ┌──────────────┐ │ │
│ │ Signature │ │ │ Y increases
│ └──────────────┘ │ │
│ │ │
│ ← X increases → │ │
└─────────────────────────────────────┘
(0, 0) Bottom-left
Anchor Tag Positioning (Recommended)
Use text anchors in your document for dynamic positioning:
{
"signHereTabs": [{
"anchorString": "/sig1/",
"anchorXOffset": "0",
"anchorYOffset": "-10",
"anchorUnits": "pixels"
}]
}
Place /sig1/ in your document where you want the signature field. The anchor text can be hidden using white text on white background.
- Use unique anchor strings (e.g.,
/sig_employee/,/sig_manager/) - Include anchors in headers/footers for consistent placement
- Use negative
anchorYOffsetto position fields above the anchor
Creating Documents from Templates
- cURL
- JavaScript
- Python
curl -X POST "https://api.propper.ai/restapi/v2.1/accounts/{accountId}/envelopes" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"templateId": "tpl_abc123xyz",
"templateRoles": [
{
"roleName": "Employee",
"email": "john.smith@example.com",
"name": "John Smith",
"tabs": {
"textTabs": [{
"tabLabel": "employeeName",
"value": "John Smith"
}]
}
},
{
"roleName": "Manager",
"email": "sarah.manager@company.com",
"name": "Sarah Manager"
}
],
"status": "sent"
}'
const createFromTemplate = async (accountId, templateId, token) => {
const response = await fetch(
`https://api.propper.ai/restapi/v2.1/accounts/${accountId}/envelopes`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId,
templateRoles: [
{
roleName: 'Employee',
email: 'john.smith@example.com',
name: 'John Smith',
tabs: {
textTabs: [{
tabLabel: 'employeeName',
value: 'John Smith',
}],
},
},
{
roleName: 'Manager',
email: 'sarah.manager@company.com',
name: 'Sarah Manager',
},
],
status: 'sent',
}),
}
);
return response.json();
};
const envelope = await createFromTemplate(accountId, templateId, token);
console.log('Envelope ID:', envelope.envelopeId);
def create_from_template(account_id: str, template_id: str, token: str) -> dict:
response = requests.post(
f"https://api.propper.ai/restapi/v2.1/accounts/{account_id}/envelopes",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"templateId": template_id,
"templateRoles": [
{
"roleName": "Employee",
"email": "john.smith@example.com",
"name": "John Smith",
"tabs": {
"textTabs": [{
"tabLabel": "employeeName",
"value": "John Smith",
}],
},
},
{
"roleName": "Manager",
"email": "sarah.manager@company.com",
"name": "Sarah Manager",
},
],
"status": "sent",
},
)
response.raise_for_status()
return response.json()
envelope = create_from_template(account_id, template_id, token)
print(f"Envelope ID: {envelope['envelopeId']}")
Pre-filling Template Fields
Populate text fields with data when creating from a template:
{
"templateId": "tpl_abc123xyz",
"templateRoles": [{
"roleName": "Employee",
"email": "john@example.com",
"name": "John Smith",
"tabs": {
"textTabs": [
{ "tabLabel": "employeeName", "value": "John Smith" },
{ "tabLabel": "startDate", "value": "2024-02-01" },
{ "tabLabel": "salary", "value": "$85,000" },
{ "tabLabel": "department", "value": "Engineering" }
],
"checkboxTabs": [
{ "tabLabel": "benefitsOptIn", "selected": "true" }
]
}
}]
}
Template Variables (Document Placeholders)
Use {{variableName}} placeholders in your PDF template document:
EMPLOYMENT AGREEMENT
This agreement is entered into by {{employeeName}}
with a start date of {{startDate}} and an annual salary of {{salary}}.
Employee Signature: _______________
Date: _______________
Variables are replaced when creating an envelope:
{
"templateId": "tpl_abc123xyz",
"customFields": {
"textCustomFields": [
{ "name": "employeeName", "value": "John Smith" },
{ "name": "startDate", "value": "February 1, 2024" },
{ "name": "salary", "value": "$85,000" }
]
}
}
Listing Templates
- cURL
- JavaScript
- Python
curl "https://api.propper.ai/restapi/v2.1/accounts/{accountId}/templates" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const listTemplates = async (accountId, token) => {
const response = await fetch(
`https://api.propper.ai/restapi/v2.1/accounts/${accountId}/templates`,
{
headers: { 'Authorization': `Bearer ${token}` },
}
);
const { envelopeTemplates } = await response.json();
return envelopeTemplates;
};
const templates = await listTemplates(accountId, token);
templates.forEach(t => console.log(`${t.templateId}: ${t.name}`));
def list_templates(account_id: str, token: str) -> list:
response = requests.get(
f"https://api.propper.ai/restapi/v2.1/accounts/{account_id}/templates",
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
return response.json().get("envelopeTemplates", [])
templates = list_templates(account_id, token)
for t in templates:
print(f"{t['templateId']}: {t['name']}")
Updating Templates
Update an existing template's properties or fields:
curl -X PUT "https://api.propper.ai/restapi/v2.1/accounts/{accountId}/templates/{templateId}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Employment Contract v2",
"description": "Updated employment agreement with new terms"
}'
Best Practices
Template Organization
templates/
├── hr/
│ ├── employment-contract.pdf
│ ├── nda.pdf
│ └── offer-letter.pdf
├── sales/
│ ├── service-agreement.pdf
│ └── sow.pdf
└── legal/
└── master-services-agreement.pdf
Naming Conventions
Use clear, descriptive names:
HR - Employment Contract - Full TimeSales - MSA - EnterpriseLegal - NDA - Mutual
Version Control
Include version in template names or descriptions:
{
"name": "Employment Contract",
"description": "Version 2.1 - Updated January 2024"
}
Field Validation
Use validation patterns for data integrity:
{
"textTabs": [{
"tabLabel": "ssn",
"validationPattern": "^\\d{3}-\\d{2}-\\d{4}$",
"validationMessage": "Please enter SSN as XXX-XX-XXXX"
}]
}
Next Steps
- Signing Flow Guide - Configure signing workflows
- Embedded Signing - Sign within your app
- Bulk Send Example - Send templates at scale