Add bulk send prototype — script, sample CSV, and demo guide

This commit is contained in:
Paul Huliganga 2026-04-20 01:49:19 -04:00
parent 39982008d3
commit 854d112372
3 changed files with 245 additions and 0 deletions

77
bulk-send/README.md Normal file
View File

@ -0,0 +1,77 @@
# DocuSign Bulk Send — Demo Guide
## What is Bulk Send?
Bulk Send lets you send one template to **many recipients at once** — each gets their own unique envelope, personalized with their name, email, and custom field values.
## Files in This Directory
| File | Purpose |
|------|---------|
| `bulk_send.py` | Main script — creates bulk list, envelope, and sends |
| `recipients.csv` | Sample recipient list (5 demo contacts) |
---
## CSV Format
```csv
Name,Email,Company,Title,CustomField1
Alice Johnson,alice.johnson@example.com,Acme Corp,CEO,CONTRACT-001
```
| Column | Maps To |
|--------|---------|
| Name | Recipient display name |
| Email | Recipient email address |
| Company | Tab value in template (`Company` tab) |
| Title | Tab value in template (`Title` tab) |
| CustomField1 | Envelope custom field (tracking/reference) |
---
## How to Run
### Dry Run (safe — no emails sent)
```bash
source venv/bin/activate
python3 bulk-send/bulk_send.py \
--template-id <YOUR_TEMPLATE_ID> \
--csv bulk-send/recipients.csv \
--dry-run
```
### Live Send
```bash
python3 bulk-send/bulk_send.py \
--template-id <YOUR_TEMPLATE_ID> \
--csv bulk-send/recipients.csv
```
---
## How It Works (3 API Calls)
```
1. POST /bulk_send_lists → Upload recipient list → get bulk_list_id
2. POST /envelopes → Create draft envelope from template → get envelope_id
3. POST /bulk_send_lists/{id}/send → Trigger send → get batch_id
```
Each recipient gets:
- Their own envelope
- Pre-filled tabs (name, company, title)
- Unique signing link via email
---
## DocuSign API Docs
- [Bulk Send Concept](https://developers.docusign.com/docs/esign-rest-api/esign101/concepts/envelopes/bulk-send/)
- [How-To: Bulk Send Envelopes](https://developers.docusign.com/docs/esign-rest-api/how-to/bulk-send-envelopes/)
---
## Notes for Customer Demo
- Uses **demo environment** (demo.docusign.net) — no real emails sent
- Template must have a signer role named **"Signer"**
- Tabs in template must be named `Company` and `Title` to auto-fill from CSV
- Batch status can be checked via: `GET /bulk_send_batch/{batchId}`

162
bulk-send/bulk_send.py Normal file
View File

@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
bulk_send.py DocuSign Bulk Send Prototype
--------------------------------------------
Sends a template to multiple recipients from a CSV file using
the DocuSign eSignature Bulk Send API (v2.1).
Usage:
python3 bulk_send.py --template-id <TEMPLATE_ID> --csv recipients.csv
DocuSign API reference:
https://developers.docusign.com/docs/esign-rest-api/how-to/bulk-send-envelopes/
"""
import os
import csv
import json
import argparse
import requests
from dotenv import load_dotenv
from auth_helper import get_access_token # reuses existing JWT auth
load_dotenv()
ACCOUNT_ID = os.getenv("DOCUSIGN_ACCOUNT_ID")
BASE_URL = f"https://demo.docusign.net/restapi/v2.1/accounts/{ACCOUNT_ID}"
def load_recipients(csv_path: str) -> list[dict]:
"""Load recipients from CSV file."""
recipients = []
with open(csv_path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
recipients.append(row)
print(f"✅ Loaded {len(recipients)} recipients from {csv_path}")
return recipients
def create_bulk_send_list(token: str, recipients: list[dict], list_name: str = "Bulk Send Demo") -> str:
"""Create a bulk send list in DocuSign and return its ID."""
url = f"{BASE_URL}/bulk_send_lists"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
bulk_copies = []
for r in recipients:
bulk_copies.append({
"recipients": {
"signers": [{
"name": r["Name"],
"email": r["Email"],
"roleName": "Signer", # must match template role name
"tabs": {
"textTabs": [
{"tabLabel": "Company", "value": r.get("Company", "")},
{"tabLabel": "Title", "value": r.get("Title", "")},
]
}
}]
},
"customFields": {
"textCustomFields": [{
"name": "CustomField1",
"value": r.get("CustomField1", "")
}]
}
})
payload = {
"name": list_name,
"bulkCopies": bulk_copies
}
resp = requests.post(url, headers=headers, json=payload)
resp.raise_for_status()
list_id = resp.json()["listId"]
print(f"✅ Bulk send list created: {list_id}")
return list_id
def create_envelope_from_template(token: str, template_id: str, bulk_list_id: str) -> str:
"""Create a draft envelope from template, linked to the bulk send list."""
url = f"{BASE_URL}/envelopes"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"status": "created", # draft — bulk send will send it
"templateId": template_id,
"templateRoles": [{
"roleName": "Signer",
"name": "Bulk Recipient", # placeholder, overridden by bulk list
"email": "bulk@placeholder.invalid"
}],
"customFields": {
"textCustomFields": [{
"name": "mailingListId",
"value": bulk_list_id,
"required": "false",
"show": "false"
}]
}
}
resp = requests.post(url, headers=headers, json=payload)
resp.raise_for_status()
envelope_id = resp.json()["envelopeId"]
print(f"✅ Draft envelope created: {envelope_id}")
return envelope_id
def send_bulk(token: str, envelope_id: str, bulk_list_id: str) -> dict:
"""Trigger the bulk send."""
url = f"{BASE_URL}/bulk_send_lists/{bulk_list_id}/send"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {"envelopeOrTemplateId": envelope_id}
resp = requests.post(url, headers=headers, json=payload)
resp.raise_for_status()
result = resp.json()
print(f"✅ Bulk send triggered!")
print(f" Batch ID: {result.get('batchId')}")
print(f" Queued: {result.get('queued')}")
print(f" Envelopes: {result.get('totalEnvelopes')}")
return result
def main():
parser = argparse.ArgumentParser(description="DocuSign Bulk Send Prototype")
parser.add_argument("--template-id", required=True, help="DocuSign template ID to send")
parser.add_argument("--csv", default="bulk-send/recipients.csv", help="Path to recipients CSV")
parser.add_argument("--list-name", default="Bulk Send Demo", help="Name for the bulk send list")
parser.add_argument("--dry-run", action="store_true", help="Create list + envelope but don't send")
args = parser.parse_args()
print("\n🚀 DocuSign Bulk Send Prototype")
print("================================")
token = get_access_token()
recipients = load_recipients(args.csv)
bulk_list_id = create_bulk_send_list(token, recipients, args.list_name)
envelope_id = create_envelope_from_template(token, args.template_id, bulk_list_id)
if args.dry_run:
print("\n⚠️ Dry run — skipping actual send.")
print(f" Bulk List ID: {bulk_list_id}")
print(f" Envelope ID: {envelope_id}")
else:
result = send_bulk(token, envelope_id, bulk_list_id)
print(f"\n✅ Done! Batch ID: {result.get('batchId')}")
if __name__ == "__main__":
main()

6
bulk-send/recipients.csv Normal file
View File

@ -0,0 +1,6 @@
Name,Email,Company,Title,CustomField1
Alice Johnson,alice.johnson@example.com,Acme Corp,CEO,CONTRACT-001
Bob Smith,bob.smith@example.com,Globex Inc,CFO,CONTRACT-002
Carol White,carol.white@example.com,Initech Ltd,COO,CONTRACT-003
David Lee,david.lee@example.com,Umbrella Co,VP Sales,CONTRACT-004
Emma Davis,emma.davis@example.com,Stark Industries,Director,CONTRACT-005
1 Name Email Company Title CustomField1
2 Alice Johnson alice.johnson@example.com Acme Corp CEO CONTRACT-001
3 Bob Smith bob.smith@example.com Globex Inc CFO CONTRACT-002
4 Carol White carol.white@example.com Initech Ltd COO CONTRACT-003
5 David Lee david.lee@example.com Umbrella Co VP Sales CONTRACT-004
6 Emma Davis emma.davis@example.com Stark Industries Director CONTRACT-005