Back to Invoice Generation & Delivery

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Invoice Generation and Delivery

[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]

This document provides the FullSpec build team with every technical detail required to construct, wire, and validate the Invoice Generation and Delivery automation. It covers the current-state step map, agent specifications with IO contracts, a full end-to-end data flow trace, and the recommended build stack. The process owner and finance lead own the human review step and the payment terms mapping table; the FullSpec team owns everything else from trigger to output.

01Process snapshot

Step
Name
Description
01
Identify Completed Jobs
Bookkeeper checks the CRM or project board each day for jobs marked complete that have not yet been invoiced. Manual polling, no automated trigger. Time cost: 20 min per invoice cycle.
02
Pull Job and Pricing Details
Line items, hourly totals, or fixed prices are located in the job record or a linked Google Sheet and manually noted. Time cost: 15 min per cycle.
03
Verify Client Contact and Terms
Billing contact, currency, payment terms, and tax classification are checked in the HubSpot CRM record before the invoice is created. Time cost: 10 min per cycle.
04
Create Invoice in Accounting Tool
BOTTLENECK. Bookkeeper manually creates a new invoice in Xero, entering client, line items, amounts, tax codes, and due date from scratch. Time cost: 18 min per cycle.
05
Apply Correct Payment Terms
Payment terms (net 14, net 30, upfront deposit) are applied manually based on client tier or contract type. Time cost: 5 min per cycle.
06
Review Invoice Before Sending
Finance lead reviews the draft to catch errors before it reaches the client. This step is retained in the automated flow. Time cost: 8 min per cycle.
07
Send Invoice to Client
Invoice is emailed from Xero or downloaded as a PDF and sent manually via Gmail with a covering message. Time cost: 7 min per cycle.
08
Log Send Date in Tracking Sheet
Invoice number, send date, and amount are recorded in the master Google Sheet. Time cost: 5 min per cycle.
09
Notify Account Manager
Bookkeeper sends a Slack message to the account manager confirming the invoice has gone out. Time cost: 3 min per cycle.
10
Update CRM Deal Stage
BOTTLENECK. Account manager manually moves the HubSpot deal to Invoiced and pastes the invoice reference into the deal notes. Time cost: 5 min per cycle.
Time cost summary: 96 minutes of manual work per invoice cycle across 10 steps. At approximately 90 invoices per month, this totals roughly 144 hours per month and 5 hours per week. The automation replaces steps 1, 2, 3, 4, 5 (Invoice Builder Agent) and steps 7, 8, 9, 10 (Delivery and CRM Sync Agent). Step 6, the finance lead review, is retained as the single human touchpoint. The two bottleneck steps flagged in red (04 and 10) are the highest-value eliminations.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Invoice Builder Agent

Triggered by a HubSpot deal stage change to Closed-Won or Complete, this agent retrieves the full deal record including all line items, billing contact details, currency, and any company-level payment terms stored on the associated HubSpot company object. It applies the correct Xero tax code and payment terms by looking up the client type against the agreed mapping table (stored in Google Sheets), then creates a draft invoice in Xero. A Slack notification is posted to the finance channel with a direct link to the draft so the finance lead can review it without navigating Xero manually. Complexity is Moderate due to the conditional tax and payment terms logic and the requirement for a reliable HubSpot-to-Xero contact matching strategy.

Trigger
HubSpot deal stage changes to 'Closed-Won' or the configured equivalent complete stage via webhook or polling interval (5 min max).
Tools
HubSpot (deal and company read), Xero (contact lookup, draft invoice create), Google Sheets (payment terms mapping table read).
Replaces steps
Steps 1, 2, 3, 4, 5 — Identify Completed Jobs, Pull Job and Pricing Details, Verify Client Contact and Terms, Create Invoice in Accounting Tool, Apply Correct Payment Terms.
Estimated build
14 hours — Moderate complexity.
// Input (from HubSpot deal webhook payload)
deal.id                  // HubSpot deal ID (string)
deal.dealname            // Deal or job name
deal.amount              // Total deal value (decimal)
deal.line_items[]        // Array of line item objects: { name, quantity, unit_price, tax_code_hint }
deal.closedate           // ISO 8601 date string
deal.hubspot_owner_id    // Assigned account manager ID
deal.associated_company  // HubSpot company object ID

// Lookup (from HubSpot company object)
company.domain           // Used as primary matching key to Xero contact
company.billing_contact_email
company.client_tier      // e.g. 'standard', 'premium', 'enterprise'
company.preferred_currency // ISO 4217 code, e.g. 'USD', 'AUD'

// Lookup (from Google Sheets mapping table)
mapping_table[client_tier].payment_terms  // e.g. 'NET30', 'NET14', 'DEPOSIT50'
mapping_table[client_tier].tax_type       // Xero TaxType code, e.g. 'OUTPUT', 'EXEMPTOUTPUT'

// Output (Xero draft invoice object)
xero_invoice.InvoiceID   // Xero-generated GUID
xero_invoice.InvoiceNumber // Auto-assigned by Xero
xero_invoice.Status      // 'DRAFT'
xero_invoice.Contact     // Matched Xero contact object (by domain)
xero_invoice.LineItems[] // Mapped from deal.line_items[]
xero_invoice.DueDate     // Calculated from closedate + payment_terms offset
xero_invoice.CurrencyCode
xero_invoice.Reference   // Populated with deal.dealname

// Slack notification (review prompt)
slack.channel            // #finance or configured channel name
slack.message            // 'Draft invoice [InvoiceNumber] for [Contact.Name] — [Amount] [Currency]. Review: [Xero deep link]'
  • HubSpot OAuth app must have scopes: crm.objects.deals.read, crm.objects.companies.read, crm.objects.line_items.read. Confirm the connected HubSpot account tier supports private app creation (any paid tier or CRM Free with API access enabled).
  • Xero OAuth 2.0 connection requires scopes: accounting.transactions (read/write), accounting.contacts.read. The Xero organisation must be on a plan that includes API access (Starter, Standard, or Premium). Confirm tenant ID before build.
  • Contact matching strategy: use the HubSpot company domain field as the primary lookup key against Xero contact website field. If no match is found, fall back to billing_contact_email against Xero contact email. If still no match, halt the agent, log the error to the error table, and post a Slack alert to #finance with the deal name and a prompt to create the Xero contact manually.
  • The payment terms and tax code mapping table must exist in Google Sheets with at minimum the columns: client_tier, payment_terms_code, xero_tax_type, due_date_offset_days. This table must be signed off by the finance lead before the agent build begins. The sheet ID and tab name must be supplied by the finance lead at build kickoff.
  • Line items: if deal.line_items is empty, the agent must not create a zero-line invoice. Halt, log error, and alert via Slack.
  • Dedupe guard: before creating the Xero draft, query Xero for any existing invoice referencing the same deal.id (stored in the Reference field). If a draft already exists, skip creation and re-post the existing Xero deep link to Slack to avoid duplicate invoices.
  • Currency handling: if company.preferred_currency is not set, default to the Xero organisation's base currency. Log the fallback in the error/audit table.
  • The Slack notification must include a direct deep link to the Xero draft invoice using the format: https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID={xero_invoice.InvoiceID}
Delivery and CRM Sync Agent

This agent listens for a status change on the Xero invoice from Draft to Approved (triggered by the finance lead's one-click approval in Xero). Once the approval event fires, the agent executes four sequential actions: it sends the approved invoice to the client billing contact via Gmail using a branded HTML template with the invoice PDF attached and a payment link included; it updates the HubSpot deal stage to the configured Invoiced stage and writes the Xero invoice number and InvoiceID back to deal custom properties; it appends a new row to the master Google Sheets invoice log; and it posts a confirmation message to the Slack finance channel. Complexity is Moderate due to the multi-tool fan-out pattern and the requirement to retrieve the invoice PDF from Xero's Files API before attaching it to the Gmail send.

Trigger
Xero invoice status changes from DRAFT to AUTHORISED (Xero webhook event: Invoice.Updated, filtered on Status == AUTHORISED).
Tools
Xero (invoice read, PDF retrieve via Files API), Gmail (send with PDF attachment), HubSpot (deal stage update, property write), Google Sheets (row append), Slack (confirmation post).
Replaces steps
Steps 7, 8, 9, 10 — Send Invoice to Client, Log Send Date in Tracking Sheet, Notify Account Manager, Update CRM Deal Stage.
Estimated build
14 hours — Moderate complexity.
// Input (from Xero webhook: Invoice.Updated)
xero_event.InvoiceID     // Xero invoice GUID
xero_event.InvoiceNumber
xero_event.Status        // Must equal 'AUTHORISED' to proceed
xero_event.TenantID      // Xero organisation identifier

// Enriched lookup (Xero GET /Invoices/{InvoiceID})
invoice.Contact.EmailAddress   // Client billing email
invoice.Contact.Name
invoice.Total                  // Invoice total (decimal)
invoice.CurrencyCode
invoice.DueDate                // ISO 8601
invoice.Reference              // Should match deal.dealname from Agent 1
invoice.OnlineInvoiceUrl       // Xero-hosted payment link

// Enriched lookup (Xero GET /Invoices/{InvoiceID}/pdf)
invoice_pdf                    // Binary PDF blob for Gmail attachment

// HubSpot lookup (reverse match via InvoiceID stored in deal Reference)
hubspot_deal.id
hubspot_deal.hubspot_owner_id

// Output: Gmail
gmail.to                 // invoice.Contact.EmailAddress
gmail.subject            // 'Invoice [InvoiceNumber] from [YourCompany.com]'
gmail.body               // Branded HTML template with invoice summary and payment link
gmail.attachment         // invoice_pdf (filename: Invoice-[InvoiceNumber].pdf)

// Output: HubSpot deal update
deal.dealstage           // Set to configured 'Invoiced' stage pipeline ID
deal.invoice_number      // Custom property: xero_invoice_number
deal.invoice_id          // Custom property: xero_invoice_id
deal.invoice_sent_date   // Custom property: ISO 8601 timestamp

// Output: Google Sheets row append (master invoice log)
row.invoice_number       // InvoiceNumber
row.client_name          // Contact.Name
row.amount               // Total
row.currency             // CurrencyCode
row.send_date            // UTC timestamp
row.due_date             // DueDate
row.xero_link            // OnlineInvoiceUrl
row.deal_id              // HubSpot deal ID

// Output: Slack confirmation
slack.channel            // #finance
slack.message            // 'Invoice [InvoiceNumber] sent to [Contact.Name] — [Total] [Currency]. Due [DueDate]. Xero: [OnlineInvoiceUrl]'

// On approval (exception path — invoice NOT approved, returned for correction)
// If Xero status changes to VOIDED or a corrected draft is re-submitted,
// Agent 1 dedupe logic prevents a second draft creation; the finance lead
// manually edits and re-approves. No automated action by this agent until
// Status == AUTHORISED is received again.
  • Xero webhook registration: register a webhook endpoint on the automation platform for the event type Invoice.Updated scoped to the correct Xero tenant. Validate the webhook signature using the Xero HMAC-SHA256 shared secret on every inbound request before processing.
  • Xero PDF retrieval uses GET /api.xro/2.0/Invoices/{InvoiceID}/pdf with Accept: application/pdf header. This endpoint can be slow on large invoices; implement a 10-second timeout with one retry before logging an error.
  • Gmail send must use the authenticated Google Workspace account that is configured as the finance billing address. Confirm the Gmail OAuth scope: gmail.send. The OAuth token must belong to the designated sender account, not a personal address.
  • HubSpot deal stage update requires the target pipeline's stage ID (not the label). The FullSpec team must retrieve the correct dealstage ID from the HubSpot pipeline settings at build time and hard-code it. Confirm the pipeline name with the finance lead.
  • Google Sheets append: confirm the master log sheet ID and the exact tab name before build. The column order must match the row output spec above. If the sheet is not accessible (permissions error), log the failure and post a Slack alert rather than silently dropping the row.
  • Slack posting uses the Slack Bot Token (not a webhook URL) to allow structured Block Kit messages. The bot must be invited to #finance before testing.
  • If the Gmail send fails (e.g. invalid recipient address), the agent must not silently succeed. Log the error, post a Slack alert with the invoice number and the failed address, and halt subsequent steps to prevent a partially completed record.
  • Confirm whether the Gmail branded template should be plain HTML managed in the automation platform or a pre-saved Gmail draft template. The FullSpec team will build a parameterised HTML template inline unless the finance lead supplies a specific brand file.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow — Invoice Generation and Delivery
// ─────────────────────────────────────────────────────────────────────
// TRIGGER: HubSpot deal stage change
// ─────────────────────────────────────────────────────────────────────
HubSpot.webhook (event: deal.propertyChange, property: dealstage)
  -> filter: dealstage.value == CLOSED_WON_STAGE_ID
  -> emit: {
       deal.id,
       deal.dealname,
       deal.amount,
       deal.closedate,
       deal.hubspot_owner_id,
       deal.associated_company_id
     }

// ─────────────────────────────────────────────────────────────────────
// AGENT 1: Invoice Builder Agent — START
// ─────────────────────────────────────────────────────────────────────

// Step 1: Enrich deal with line items
HubSpot.GET /crm/v3/objects/deals/{deal.id}/associations/line_items
  -> for each line_item_id:
       HubSpot.GET /crm/v3/objects/line_items/{line_item_id}
         -> collect: { name, quantity, hs_unit_price, hs_line_item_currency_code }
  -> assembled: deal.line_items[]

// Step 2: Enrich with company-level billing data
HubSpot.GET /crm/v3/objects/companies/{deal.associated_company_id}
  -> extract: {
       company.domain,
       company.billing_contact_email,
       company.client_tier,
       company.preferred_currency
     }

// Step 3: Lookup payment terms and tax code
GoogleSheets.GET rows from [MAPPING_SHEET_ID]![TAB_NAME]
  -> filter: row.client_tier == company.client_tier
  -> extract: {
       mapping.payment_terms_code,   // e.g. 'NET30'
       mapping.xero_tax_type,        // e.g. 'OUTPUT'
       mapping.due_date_offset_days  // e.g. 30
     }

// Step 4: Resolve Xero contact
Xero.GET /api.xro/2.0/Contacts?where=Website=="{company.domain}"
  -> if match: xero_contact = result[0]
  -> if no match: Xero.GET /api.xro/2.0/Contacts?EmailAddress={company.billing_contact_email}
     -> if match: xero_contact = result[0]
     -> if no match: HALT — log error to [ERROR_TABLE], post Slack alert to #finance

// Step 5: Dedupe check
Xero.GET /api.xro/2.0/Invoices?Reference="{deal.dealname}"&Statuses=DRAFT
  -> if existing_draft found: skip creation, re-post existing deep link to Slack, EXIT agent

// Step 6: Create draft invoice in Xero
Xero.POST /api.xro/2.0/Invoices
  -> payload: {
       Type: 'ACCREC',
       Status: 'DRAFT',
       Contact: { ContactID: xero_contact.ContactID },
       LineItems: deal.line_items[].map({
         Description: name,
         Quantity: quantity,
         UnitAmount: hs_unit_price,
         TaxType: mapping.xero_tax_type
       }),
       Date: today() [ISO 8601],
       DueDate: today() + mapping.due_date_offset_days,
       CurrencyCode: company.preferred_currency ?? xero_org.base_currency,
       Reference: deal.dealname,
       LineAmountTypes: 'EXCLUSIVE'
     }
  -> receive: { xero_invoice.InvoiceID, xero_invoice.InvoiceNumber }

// Step 7: Notify finance lead
Slack.POST /chat.postMessage
  -> channel: #finance
  -> text: 'Draft invoice {InvoiceNumber} for {xero_contact.Name} — {amount} {currency}.'
  -> attachment: 'Review: https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID={InvoiceID}'

// ─────────────────────────────────────────────────────────────────────
// AGENT 1 HANDOFF: Human review checkpoint
// Finance lead reviews and approves draft in Xero.
// Agent 2 is dormant until Xero fires Invoice.Updated (Status=AUTHORISED).
// ─────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────
// AGENT 2: Delivery and CRM Sync Agent — START
// ─────────────────────────────────────────────────────────────────────

// Step 8: Receive Xero webhook
Xero.webhook (event: Invoice.Updated)
  -> validate HMAC-SHA256 signature using shared secret
  -> filter: invoice.Status == 'AUTHORISED'
  -> extract: { InvoiceID, InvoiceNumber, TenantID }

// Step 9: Enrich invoice data
Xero.GET /api.xro/2.0/Invoices/{InvoiceID}
  -> extract: {
       invoice.Contact.EmailAddress,
       invoice.Contact.Name,
       invoice.Total,
       invoice.CurrencyCode,
       invoice.DueDate,
       invoice.Reference,        // == deal.dealname from Agent 1
       invoice.OnlineInvoiceUrl
     }

// Step 10: Retrieve invoice PDF
Xero.GET /api.xro/2.0/Invoices/{InvoiceID}/pdf (Accept: application/pdf)
  -> receive: invoice_pdf [binary blob]
  -> timeout: 10s, retry: 1

// Step 11: Reverse-match HubSpot deal
HubSpot.GET /crm/v3/objects/deals/search
  -> filter: dealname == invoice.Reference
  -> extract: hubspot_deal.id, hubspot_deal.hubspot_owner_id

// Step 12: Send invoice via Gmail
Gmail.POST /gmail/v1/users/me/messages/send
  -> to: invoice.Contact.EmailAddress
  -> subject: 'Invoice {InvoiceNumber} from [YourCompany.com]'
  -> body: [branded HTML template]
     params: { contact_name, invoice_number, total, currency, due_date, payment_link: OnlineInvoiceUrl }
  -> attachment: invoice_pdf (filename: Invoice-{InvoiceNumber}.pdf, MIME: application/pdf)
  -> on failure: log error, post Slack alert, HALT remaining steps

// Step 13: Update HubSpot deal
HubSpot.PATCH /crm/v3/objects/deals/{hubspot_deal.id}
  -> properties: {
       dealstage: INVOICED_STAGE_ID,
       xero_invoice_number: InvoiceNumber,
       xero_invoice_id: InvoiceID,
       invoice_sent_date: now() [ISO 8601]
     }

// Step 14: Append row to Google Sheets invoice log
GoogleSheets.POST (append to [LOG_SHEET_ID]![LOG_TAB_NAME])
  -> row: [
       InvoiceNumber,
       Contact.Name,
       Total,
       CurrencyCode,
       now() [UTC timestamp],
       DueDate,
       OnlineInvoiceUrl,
       hubspot_deal.id
     ]
  -> on failure: log error, post Slack alert (do not halt — sheet logging is non-blocking)

// Step 15: Post Slack confirmation
Slack.POST /chat.postMessage
  -> channel: #finance
  -> text: 'Invoice {InvoiceNumber} sent to {Contact.Name} — {Total} {Currency}. Due {DueDate}.'
  -> attachment: OnlineInvoiceUrl

// ─────────────────────────────────────────────────────────────────────
// END OF FLOW
// Audit trail: Xero draft history, Gmail sent folder, HubSpot deal timeline,
// Google Sheets log row, Slack message archive, error table in [ERROR_TABLE].
// ─────────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
One workflow automation tool instance hosting two discrete workflows: Workflow 1 (Invoice Builder Agent) and Workflow 2 (Delivery and CRM Sync Agent). All credentials are stored in a shared credential store within the platform so neither workflow holds raw secrets in node configuration. Workflows are versioned and labelled with the agent name and build date.
Webhook configuration
Agent 1 listens on a HubSpot private app webhook subscription filtered to dealstage property changes on the target pipeline. Agent 2 listens on a Xero webhook endpoint registered in the Xero developer portal for the event type Invoice.Updated; the endpoint URL is the automation platform's inbound webhook URL for Workflow 2. Xero webhook payloads must be validated using HMAC-SHA256 before any processing step runs. Both webhook endpoints are served over HTTPS only.
Templating approach
The Gmail invoice email uses a parameterised HTML template stored as a text node within Workflow 2. Template variables are injected at send time: {{contact_name}}, {{invoice_number}}, {{total}}, {{currency}}, {{due_date}}, {{payment_link}}. The FullSpec team builds and styles the template to match [YourCompany.com] brand. The payment terms and tax code mapping table is maintained as a named Google Sheet (not hard-coded in the workflow) so the finance lead can update it without a build change.
Error logging
All agent errors (contact match failures, PDF retrieval timeouts, Gmail send failures, Sheets append failures) are written to a Supabase table named invoice_automation_errors with columns: id (uuid), timestamp (timestamptz), agent_name (text), error_type (text), deal_id (text), invoice_id (text), error_message (text), resolved (boolean). A Slack alert is posted to #finance for every new error row. The FullSpec team provisions the Supabase project and table schema at the start of the build phase.
Testing approach
All agent logic is built and validated against sandbox environments first: HubSpot sandbox account, Xero demo company, and a test Google Sheets log. Gmail sends during testing are routed to an internal FullSpec test inbox using a workflow-level override variable. Sandbox credentials are stored separately from production credentials in the shared credential store. No production deal records or live client contacts are used until UAT begins in week 4.
Estimated total build time
28 hours total. Invoice Builder Agent: 14 hours (includes trigger setup, HubSpot enrichment, Google Sheets lookup, Xero contact matching, dedupe logic, draft creation, Slack notification, and error handling). Delivery and CRM Sync Agent: 14 hours (includes Xero webhook listener, PDF retrieval, Gmail branded send, HubSpot deal update, Sheets append, Slack confirmation, and error handling). End-to-end integration testing, UAT support, and error table setup are included within the 28-hour total as allocated across both agent builds.
Before build begins, the following must be confirmed and supplied to the FullSpec team: (1) HubSpot private app credentials with the required CRM scopes and the exact Closed-Won dealstage ID for the target pipeline. (2) Xero OAuth 2.0 credentials, tenant ID, and confirmation of Xero plan tier. (3) The payment terms and tax code mapping table signed off by the finance lead, including the Google Sheets ID and tab name. (4) The Gmail sender account and confirmation that the gmail.send OAuth scope can be granted. (5) The Supabase project URL and service role key for error logging, or confirmation that FullSpec should provision a new Supabase project. (6) The Slack bot token and confirmation that the bot has been invited to the #finance channel. Contact support@gofullspec.com to provide these items or to ask questions before kickoff.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Invoice Generation & Delivery.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View