Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
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
02Agent specifications
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.
// 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}
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.
// 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.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────────
// 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].
// ─────────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Invoice Generation & Delivery.