Quick start: send a document for signature
Five requests take you from an API key to a document waiting for signatures. Every example below is copied from the API spec, so you can run them as they are and then swap in your own file and people.
You need a SignWith account and a terminal. The examples use cURL, Node 20 or later (save the code as a .mjs file), and Python with the requests package. Pick a language once and every example on the page switches.
1. Get an API key
Get your API keyIn SignWith, open Settings → Developers and create a key. Keys starting with sw_test_ act on your test account, and keys starting with sw_live_ act on your live account, where signers get real emails. Then put the key in an environment variable, so it stays out of your code:
export SIGNWITH_API_KEY="sw_live_..."
2. Check the key works
GET /me returns the user, account and environment the key belongs to. It changes nothing, so it's a safe first call.
curl https://app.signwith.co/api/v1/me \
-H "Authorization: Bearer $SIGNWITH_API_KEY"const response = await fetch('https://app.signwith.co/api/v1/me', {
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
},
})
console.log(response.status, await response.json())import os
import requests
response = requests.get(
"https://app.signwith.co/api/v1/me",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
},
)
print(response.status_code, response.json())A 200 response with your email in user.email means you're ready. A 401 with code unauthenticated means the key is missing or wrong; see authentication.
3. Upload a document as a template
A template is a reusable document with roles (who signs) and fields (what they fill in). Create one from a PDF that SignWith can download from an https:// URL. Replace the file URL with one of yours, or send the file itself as base64 or a multipart upload (see create a template).
curl -X POST https://app.signwith.co/api/v1/templates \
-H "Authorization: Bearer $SIGNWITH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Residential lease",
"external_id": "lease-v3",
"folder": "Leasing",
"documents": [
{
"name": "Lease agreement",
"file": "https://files.acme.co/templates/lease-v3.pdf"
}
]
}'const response = await fetch('https://app.signwith.co/api/v1/templates', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
name: 'Residential lease',
external_id: 'lease-v3',
folder: 'Leasing',
documents: [
{
name: 'Lease agreement',
file: 'https://files.acme.co/templates/lease-v3.pdf',
},
],
}),
})
console.log(response.status, await response.json())import os
import uuid
import requests
response = requests.post(
"https://app.signwith.co/api/v1/templates",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Residential lease",
"external_id": "lease-v3",
"folder": "Leasing",
"documents": [
{
"name": "Lease agreement",
"file": "https://files.acme.co/templates/lease-v3.pdf",
},
],
},
)
print(response.status_code, response.json())The response is the new template. Note its id; the examples below use 311. If the PDF has fillable form fields, they become template fields automatically. Otherwise the template has no fields yet, and it needs at least one before it can be sent.
4. Place the signature fields
Send fields to PATCH /templates/{id} to place a signature box for each role. Each area gives the page and the position as fractions of the page size, measured from the top-left corner. This example puts a Tenant signature and a Landlord signature on page 4, so change page if your document is shorter (422 invalid_field tells you when a page doesn't exist).
curl -X PATCH https://app.signwith.co/api/v1/templates/311 \
-H "Authorization: Bearer $SIGNWITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": [
{
"name": "Tenant signature",
"type": "signature",
"role": "Tenant",
"areas": [
{
"page": 4,
"x": 0.1,
"y": 0.8,
"w": 0.3,
"h": 0.06
}
]
},
{
"name": "Landlord signature",
"type": "signature",
"role": "Landlord",
"areas": [
{
"page": 4,
"x": 0.55,
"y": 0.8,
"w": 0.3,
"h": 0.06
}
]
}
]
}'const response = await fetch('https://app.signwith.co/api/v1/templates/311', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields: [
{
name: 'Tenant signature',
type: 'signature',
role: 'Tenant',
areas: [
{
page: 4,
x: 0.1,
y: 0.8,
w: 0.3,
h: 0.06,
},
],
},
{
name: 'Landlord signature',
type: 'signature',
role: 'Landlord',
areas: [
{
page: 4,
x: 0.55,
y: 0.8,
w: 0.3,
h: 0.06,
},
],
},
],
}),
})
console.log(response.status, await response.json())import os
import requests
response = requests.patch(
"https://app.signwith.co/api/v1/templates/311",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
},
json={
"fields": [
{
"name": "Tenant signature",
"type": "signature",
"role": "Tenant",
"areas": [
{
"page": 4,
"x": 0.1,
"y": 0.8,
"w": 0.3,
"h": 0.06,
},
],
},
{
"name": "Landlord signature",
"type": "signature",
"role": "Landlord",
"areas": [
{
"page": 4,
"x": 0.55,
"y": 0.8,
"w": 0.3,
"h": 0.06,
},
],
},
],
},
)
print(response.status_code, response.json())Roles named in fields are added to the template, so it now has the roles Tenant and Landlord. You can also open the template's edit_url and place fields in the SignWith editor instead.
5. Send it for signature
POST /signature_requests sends the template to one signer per role. This example uses send_email: false, so nobody is emailed while you test: the response includes each signer's private signing_url for you to open yourself.
curl -X POST https://app.signwith.co/api/v1/signature_requests \
-H "Authorization: Bearer $SIGNWITH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"template_id": 311,
"signing_order": "parallel",
"send_email": false,
"signers": [
{
"role": "Tenant",
"name": "Grace Hopper",
"email": "[email protected]"
},
{
"role": "Landlord",
"name": "Ada Lovelace",
"email": "[email protected]"
}
]
}'const response = await fetch('https://app.signwith.co/api/v1/signature_requests', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
template_id: 311,
signing_order: 'parallel',
send_email: false,
signers: [
{
role: 'Tenant',
name: 'Grace Hopper',
email: '[email protected]',
},
{
role: 'Landlord',
name: 'Ada Lovelace',
email: '[email protected]',
},
],
}),
})
console.log(response.status, await response.json())import os
import uuid
import requests
response = requests.post(
"https://app.signwith.co/api/v1/signature_requests",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"template_id": 311,
"signing_order": "parallel",
"send_email": False,
"signers": [
{
"role": "Tenant",
"name": "Grace Hopper",
"email": "[email protected]",
},
{
"role": "Landlord",
"name": "Ada Lovelace",
"email": "[email protected]",
},
],
},
)
print(response.status_code, response.json())To have SignWith email the signers instead, leave out send_email. Add signing_order: "sequential" to invite them one at a time, message for your own email text, and prefill to fill fields in advance. The signature requests reference has every option.
The request includes an Idempotency-Key header, so if the network fails you can retry the same request without sending the document twice. See idempotency.
6. Check the status
Open each signing_url and sign. Then fetch the signature request with GET /signature_requests/{id}:
curl https://app.signwith.co/api/v1/signature_requests/4812 \
-H "Authorization: Bearer $SIGNWITH_API_KEY"const response = await fetch('https://app.signwith.co/api/v1/signature_requests/4812', {
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
},
})
console.log(response.status, await response.json())import os
import requests
response = requests.get(
"https://app.signwith.co/api/v1/signature_requests/4812",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
},
)
print(response.status_code, response.json())When status is completed, the response has download links for the signed documents and the audit_trail_url. One credit covers one signed document, and sending the same template again doesn't use another; see credits and billing.
Next steps
- Get told when people sign instead of polling: set up webhooks.
- Handle failures by their stable code: errors.
- Let an AI assistant send documents for you: connect Claude, ChatGPT or Cursor.
Start building
The API and MCP server come with every account. 3 free documents a month, then pay per document.