diff --git a/bulk-send/README.md b/bulk-send/README.md new file mode 100644 index 0000000..d4e86e6 --- /dev/null +++ b/bulk-send/README.md @@ -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 \ + --csv bulk-send/recipients.csv \ + --dry-run +``` + +### Live Send +```bash +python3 bulk-send/bulk_send.py \ + --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}` diff --git a/bulk-send/bulk_send.py b/bulk-send/bulk_send.py new file mode 100644 index 0000000..15b101d --- /dev/null +++ b/bulk-send/bulk_send.py @@ -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 --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() diff --git a/bulk-send/recipients.csv b/bulk-send/recipients.csv new file mode 100644 index 0000000..2f837b1 --- /dev/null +++ b/bulk-send/recipients.csv @@ -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