Template

Proposal Schema + Merge Map (PandaDoc + DocuSign)

A ready‑to‑ship template bundle for solo operators: a compact proposal JSON Schema plus a cross‑platform merge map (PandaDoc tokens/field tags ↔ DocuSign tabLabels/anchorStrings), webhook checklists, and a v1→v2 changelog format. Assemble once, render to either tool, and e‑sign in minutes.

Use this template to ship a schema‑first proposal flow you can render to either PandaDoc or DocuSign without rewriting content. Workflow: define your proposal JSON once → map keys to PandaDoc tokens/field tags and DocuSign tabLabels/anchorStrings → create/send via API → confirm via webhooks. Replace fields in [BRACKETS] and copy the ready‑to‑paste blocks into your editor, templates, or codebase.

Proposal JSON Schema (v1.0)

Paste this into your repo as /contracts/proposal.schema.json. It’s deliberately compact and opinionated for solo operators: locked scope blocks + priced add‑ons, value framing up top, and signer roles kept to two parties.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://headcountzero.me/schemas/proposal-v1.json",
  "title": "Proposal v1",
  "type": "object",
  "required": ["company", "client", "problem", "outcomes", "scope_blocks", "options", "commercials", "timeline", "acceptance"],
  "properties": {
    "company": {
      "type": "object",
      "required": ["name", "email", "signer_name", "signer_title"],
      "properties": {
        "name": {"type": "string"},
        "email": {"type": "string", "format": "email"},
        "signer_name": {"type": "string"},
        "signer_title": {"type": "string"}
      }
    },
    "client": {
      "type": "object",
      "required": ["name", "email", "company"],
      "properties": {
        "company": {"type": "string"},
        "name": {"type": "string"},
        "email": {"type": "string", "format": "email"},
        "role": {"type": "string", "default": "Client"}
      }
    },
    "problem": {"type": "string"},
    "current_state": {"type": "string"},
    "outcomes": {
      "type": "array",
      "items": {"type": "string"},
      "minItems": 1
    },
    "scope_blocks": {
      "description": "Locked scope. Each block is included; quantities may vary only if defined.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "title", "description", "uom", "qty", "unit_price"],
        "properties": {
          "id": {"type": "string"},
          "title": {"type": "string"},
          "description": {"type": "string"},
          "uom": {"type": "string", "description": "unit of measure (e.g., deliverable, hour, run)"},
          "qty": {"type": "number", "minimum": 0},
          "unit_price": {"type": "number", "minimum": 0},
          "notes": {"type": "string"}
        }
      }
    },
    "options": {
      "description": "Priced add‑ons the client can select.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "title", "description", "uom", "qty", "unit_price", "selected"],
        "properties": {
          "id": {"type": "string"},
          "title": {"type": "string"},
          "description": {"type": "string"},
          "uom": {"type": "string"},
          "qty": {"type": "number", "minimum": 0},
          "unit_price": {"type": "number", "minimum": 0},
          "selected": {"type": "boolean", "default": false}
        }
      }
    },
    "assumptions": {"type": "array", "items": {"type": "string"}},
    "timeline": {
      "type": "object",
      "required": ["start_date", "end_date", "milestones"],
      "properties": {
        "start_date": {"type": "string", "format": "date"},
        "end_date": {"type": "string", "format": "date"},
        "milestones": {"type": "array", "items": {"type": "string"}}
      }
    },
    "commercials": {
      "type": "object",
      "required": ["currency", "payment_terms", "tax_rate"],
      "properties": {
        "currency": {"type": "string", "default": "USD"},
        "payment_terms": {"type": "string", "default": "50% upfront, 50% on delivery"},
        "tax_rate": {"type": "number", "minimum": 0, "maximum": 1},
        "discounts": {"type": "array", "items": {"type": "string"}}
      }
    },
    "acceptance": {
      "type": "object",
      "required": ["effective_date"],
      "properties": {
        "effective_date": {"type": "string", "format": "date"},
        "client_signer": {"type": "string"},
        "company_signer": {"type": "string"}
      }
    },
    "meta": {
      "type": "object",
      "properties": {
        "proposal_id": {"type": "string"},
        "version": {"type": "string", "default": "v1"},
        "created_at": {"type": "string", "format": "date-time"},
        "notes": {"type": "string"}
      }
    }
  }
}

Example record to test your merge:

{
  "company": {"name": "Headcount Zero", "email": "ops@headcountzero.me", "signer_name": "[YOUR NAME]", "signer_title": "Principal"},
  "client": {"company": "[CLIENT COMPANY]", "name": "[CLIENT NAME]", "email": "[CLIENT EMAIL]"},
  "problem": "Manual onboarding is slow and error‑prone.",
  "current_state": "Leads sit 3–5 days before qualification; kickoff docs scattered.",
  "outcomes": ["24‑hour proposal turnaround", "Kickoff within 3 business days", "Audit log of all approvals"],
  "scope_blocks": [
    {"id": "discovery", "title": "Discovery + Scoping", "description": "Structured interview and systems inventory.", "uom": "deliverable", "qty": 1, "unit_price": 800},
    {"id": "build", "title": "Automation Build", "description": "Assemble workflows across Make/Zapier/n8n.", "uom": "deliverable", "qty": 1, "unit_price": 3200}
  ],
  "options": [
    {"id": "support", "title": "30‑day Hypercare", "description": "Weekly tweaks + error handling.", "uom": "day", "qty": 30, "unit_price": 40, "selected": false}
  ],
  "assumptions": ["Client provides tool access within 24 hours of signature"],
  "timeline": {"start_date": "[YYYY-MM-DD]", "end_date": "[YYYY-MM-DD]", "milestones": ["Kickoff", "Pilot", "Go‑Live"]},
  "commercials": {"currency": "USD", "payment_terms": "100% due on signature for projects ≤ $2,000; otherwise 50/50", "tax_rate": 0.0},
  "acceptance": {"effective_date": "[YYYY-MM-DD]"},
  "meta": {"proposal_id": "PROP-2026-001", "version": "v1"}
}

Merge Map: Schema → PandaDoc tokens / DocuSign tabs

Map each schema key once, then reuse across platforms. Keep names stable to avoid drift.

  • company.name → PandaDoc token: Company.Name → DocuSign tabLabel: company_name → Anchor: [[COMPANY_NAME]]
  • company.email → PandaDoc token: Company.Email → DocuSign tabLabel: company_email → Anchor: [[COMPANY_EMAIL]]
  • company.signer_name → PandaDoc token: Company.SignerName → DocuSign tabLabel: company_signer_name → Anchor: [[COMPANY_SIGNER_NAME]]
  • company.signer_title → PandaDoc token: Company.SignerTitle → DocuSign tabLabel: company_signer_title → Anchor: [[COMPANY_SIGNER_TITLE]]
  • client.company → PandaDoc token: Client.Company → DocuSign tabLabel: client_company → Anchor: [[CLIENT_COMPANY]]
  • client.name → PandaDoc token: Client.Name → DocuSign tabLabel: client_name → Anchor: [[CLIENT_NAME]]
  • client.email → PandaDoc token: Client.Email → DocuSign tabLabel: client_email → Anchor: [[CLIENT_EMAIL]]
  • problem → PandaDoc token: Proposal.Problem → DocuSign tabLabel: problem → Anchor: [[PROBLEM]]
  • current_state → PandaDoc token: Proposal.CurrentState → DocuSign tabLabel: current_state → Anchor: [[CURRENT_STATE]]
  • outcomes (as bullet list) → PandaDoc token: Proposal.OutcomesHtml → DocuSign tabLabel: outcomes_html → Anchor: [[OUTCOMES_HTML]]
  • scope_blocks (table rows) → PandaDoc pricing table (id: scope) → DocuSign: pre‑merged text block tabLabel: scope_table → Anchor: [[SCOPE_TABLE]]
  • options (table rows) → PandaDoc pricing table (id: options) with optional selection → DocuSign: pre‑merged text block tabLabel: options_table → Anchor: [[OPTIONS_TABLE]]
  • assumptions → PandaDoc token: Proposal.AssumptionsHtml → DocuSign tabLabel: assumptions_html → Anchor: [[ASSUMPTIONS_HTML]]
  • timeline.start_date → PandaDoc token: Timeline.StartDate → DocuSign tabLabel: timeline_start → Anchor: [[TIMELINE_START]]
  • timeline.end_date → PandaDoc token: Timeline.EndDate → DocuSign tabLabel: timeline_end → Anchor: [[TIMELINE_END]]
  • timeline.milestones → PandaDoc token: Timeline.MilestonesHtml → DocuSign tabLabel: milestones_html → Anchor: [[MILESTONES_HTML]]
  • commercials.currency → PandaDoc token: Commercials.Currency → DocuSign tabLabel: currency → Anchor: [[CURRENCY]]
  • commercials.payment_terms → PandaDoc token: Commercials.PaymentTerms → DocuSign tabLabel: payment_terms → Anchor: [[PAYMENT_TERMS]]
  • commercials.tax_rate → PandaDoc token: Commercials.TaxRate → DocuSign tabLabel: tax_rate → Anchor: [[TAX_RATE]]
  • meta.proposal_id → PandaDoc token: Meta.ProposalId → DocuSign tabLabel: proposal_id → Anchor: [[PROPOSAL_ID]]
  • meta.version → PandaDoc token: Meta.Version → DocuSign tabLabel: version → Anchor: [[VERSION]]

Notes

  • Keep PandaDoc role names human‑readable (no underscores). Roles must match recipients for field tags to bind.
  • For DocuSign, keep tabLabel keys lowercase_with_underscores for consistency; anchors in ALL‑CAPS inside [[DOUBLE_BRACKETS]] are easy to search/replace in DOCX. Hide anchors by setting white text or 1pt font, or place them under transparent rectangles.
  • Render any “...Html” tokens/tabs as bullet lists or tables before merging to DocuSign (DocuSign doesn’t render HTML; precompose as text).

PandaDoc: tokens, field tags, and payload template

Use this when creating your PandaDoc template and API calls.

  1. Variables (tokens) to create in the PandaDoc template’s Variables panel. Your code passes values under the same names:
  • Company.Name
  • Company.Email
  • Company.SignerName
  • Company.SignerTitle
  • Client.Company
  • Client.Name
  • Client.Email
  • Proposal.Problem
  • Proposal.CurrentState
  • Proposal.OutcomesHtml
  • Proposal.AssumptionsHtml
  • Timeline.StartDate
  • Timeline.EndDate
  • Timeline.MilestonesHtml
  • Commercials.Currency
  • Commercials.PaymentTerms
  • Commercials.TaxRate
  • Meta.ProposalId
  • Meta.Version
  1. Field tags (place in the template for signer‑specific inputs). Short forms shown in parentheses:
  • Client signature: [s:Client:client_signature] (s)
  • Client signing date: [d:Client:client_sign_date] (d)
  • Company signature: [s:Company:company_signature] (s)
  • Company signing date: [d:Company:company_sign_date] (d)
  • Optional client textbox (PO number): [t:Client:po_number] (t)

Conventions and gotchas

  • Role names are case‑sensitive and cannot contain underscores. Recommended: Client, Company.
  • Signature fields cannot be prefilled. Text and date fields can be prefilled via API if desired.
  • Pricing tables: create two tables in the template: scope (locked rows) and options (add‑ons). Enable “Automatically add products to this table.” Your API call injects rows.
  1. API payload fragments to assemble from the schema

Create from template UUID and send:

{
  "name": "[CLIENT COMPANY] — [PROJECT/TITLE]",
  "template_uuid": "[PANDADOC_TEMPLATE_UUID]",
  "recipients": [
    {"email": "[CLIENT EMAIL]", "first_name": "[CLIENT NAME]", "role": "Client"},
    {"email": "[COMPANY EMAIL]", "first_name": "[COMPANY SIGNER NAME]", "role": "Company"}
  ],
  "tokens": [
    {"name": "Client.Company", "value": "[CLIENT COMPANY]"},
    {"name": "Client.Name", "value": "[CLIENT NAME]"},
    {"name": "Proposal.Problem", "value": "[PROBLEM]"}
    // ...additional tokens from the list above
  ],
  "pricing_tables": [
    {"name": "scope", "data": {"currency": "[CURRENCY]", "sections": [{"title": "Scope", "rows": [/* map scope_blocks here */]}]}},
    {"name": "options", "data": {"currency": "[CURRENCY]", "sections": [{"title": "Options", "rows": [/* map options here; mark optional */]}]}}
  ]
}

Then POST /public/v1/documents/{id}/send.

DocuSign: tab map + anchor placement templates

Use anchors when the layout can shift (DOCX), or fixed x/y when the PDF never moves. Keep one server template and set values at send time.

  1. AnchorString conventions to place in the DOCX/PDF
  • [[CLIENT_NAME]]
  • [[CLIENT_COMPANY]]
  • [[PROBLEM]]
  • [[OUTCOMES_HTML]]
  • [[SCOPE_TABLE]]
  • [[OPTIONS_TABLE]]
  • [[PAYMENT_TERMS]]
  • [[PROPOSAL_ID]]
  1. Template roles and prefilled tabs (send‑time). Example with one server template:
{
  "templateId": "[DOCUSIGN_TEMPLATE_ID]",
  "templateRoles": [
    {
      "roleName": "Client",
      "name": "[CLIENT NAME]",
      "email": "[CLIENT EMAIL]",
      "tabs": {
        "textTabs": [
          {"tabLabel": "client_name", "value": "[CLIENT NAME]"},
          {"tabLabel": "client_company", "value": "[CLIENT COMPANY]"},
          {"tabLabel": "problem", "value": "[PROBLEM]"}
        ]
      }
    },
    {"roleName": "Company", "name": "[COMPANY SIGNER NAME]", "email": "[COMPANY EMAIL]"}
  ],
  "status": "sent"
}
  1. Programmatic placement by anchors (for docs without pre‑placed fields)
{
  "recipients": {
    "signers": [
      {
        "name": "[CLIENT NAME]",
        "email": "[CLIENT EMAIL]",
        "recipientId": "1",
        "tabs": {
          "textTabs": [
            {"value": "[CLIENT NAME]", "anchorString": "[[CLIENT_NAME]]", "anchorXOffset": "0", "anchorYOffset": "-2", "anchorUnits": "pixels"}
          ],
          "signHereTabs": [
            {"anchorString": "[[SIGN_HERE_CLIENT]]", "anchorXOffset": "0", "anchorYOffset": "0"}
          ],
          "dateSignedTabs": [
            {"anchorString": "[[DATE_CLIENT]]"}
          ]
        }
      }
    ]
  },
  "status": "sent"
}

Tips

  • Keep tabLabel keys aligned with the Merge Map. You can also define custom tabs once and reuse them.
  • For tables (scope/options), precompose the text blocks (monospace columns or bullet lists) before injection; DocuSign won’t render HTML tables.
  • Practical max length for text fields is ~4,000 characters. Split very long sections into multiple tabs if needed.

Webhook event checklist (PandaDoc + DocuSign)

Copy this into your runbook. Confirm events and secrets in both platforms before you go live.

Environment

  • [HMAC_SECRET]: [GENERATED SHARED SECRET]
  • Acknowledge within 2–3 seconds (return 200 OK). Do heavy work async using a queue.
  • Idempotency key: use [ENVELOPE_OR_DOC_ID] + [EVENT_TYPE] + [EVENT_TIMESTAMP].

PandaDoc — subscribe and handle

  • Subscribe to: document.created, document.sent, document.viewed, document.completed, document.declined.
  • Verification: compute HMAC‑SHA256 over the raw request body with [HMAC_SECRET]; compare to platform signature; if mismatch, 401 and drop.
  • Retries: design handlers to be safe for retries; log X-Request-Id and implement backoff.
  • Minimal handler pseudo‑steps:
    1. Verify HMAC → if fail, 401.
    2. Parse event → upsert proposal status by document_id.
    3. If document.completed: capture PDFs, finalize CRM/deal, notify Slack/Email.
    4. Return 200 quickly (≤ 2s).

DocuSign Connect — subscribe and handle

  • Subscribe to Envelope events: Sent, Delivered, Completed, Declined, Voided.
  • Enable HMAC security. Validate using the raw body against X-DocuSign-Signature-* header(s) with your shared secret(s).
  • With Require Acknowledgement enabled, non‑200 responses are retried; process idempotently.
  • Minimal handler pseudo‑steps mirror PandaDoc (IDs differ: envelopeId, eventType).

Common status map

  • draft → created
  • sent → sent
  • viewed → delivered/viewed
  • completed → completed
  • declined/voided → declined/voided

Monitoring

  • Emit metrics: delivery latency, retry counts, handler time, error rate.
  • Add a manual replay SOP: use vendor UI history to retry failed deliveries while you investigate.

v1→v2 changelog template

Use this to keep your schema, templates, and maps tight as you iterate.

Header

  • Proposal: [PROPOSAL_ID]
  • Version: [FROM_VERSION] → [TO_VERSION]
  • Date: [YYYY‑MM‑DD]
  • Owner: [NAME]

Changes

  • Schema: [WHAT CHANGED IN /proposal.schema.json]
  • PandaDoc: [NEW/REMOVED TOKENS] | [FIELD TAG ADJUSTMENTS]
  • DocuSign: [NEW/REMOVED tabLabels] | [ANCHOR CHANGES]
  • Content: [UPDATED SECTIONS/TEXT]

Migration Steps

  1. Update code that builds tokens/tabs. Paths: [FILE(S)].
  2. Update PandaDoc template variables/field tags. Template: [NAME/UUID].
  3. Update DocuSign server template or anchor markers. Template: [NAME/ID].
  4. Run backfill script for in‑flight drafts if needed. Command: [SCRIPT/MAKE SCENARIO].
  5. Smoke test both renderers with the sample record. Attach screenshots.

Rollback Plan

  • Revert to [FROM_VERSION] templates and merge map.
  • Requeue any drafts created during the failed window.

Changelog Logbook

## [YYYY‑MM‑DD] v[TO_VERSION]
- Change: [SUMMARY]
- Reason: [WHY]
- Impact: [RISK/AREAS]
- Tests: [LINKS/SCREENSHOTS]

Formatter snippets (copy/paste)

Drop these prebuilt functions/snippets into your codebase to turn the schema into rendered blocks.

  1. Outcomes → bullets (for both platforms)
- [OUTCOME 1]
- [OUTCOME 2]
- [OUTCOME 3]
  1. Milestones → bullets
- [MILESTONE 1]
- [MILESTONE 2]
- [MILESTONE 3]
  1. Scope/Options → fixed‑width table (DocuSign‑friendly). Keep lines ≤ 90 chars.
Item                          Qty   UoM        Unit    Line
---------------------------------------------------------------
[Discovery + Scoping]         1     deliverable  $800   $800
[Automation Build]            1     deliverable $3200  $3200
  1. File‑name pattern for storage and audit
proposals/[CLIENT_COMPANY]/[PROPOSAL_ID]-[VERSION]-[YYYYMMDD].pdf