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
Client Deposit & Payment Tracking
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement, wire, and deploy the three-agent Client Deposit and Payment Tracking automation. It covers the current-state step map with bottleneck flags, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. All configuration decisions, field names, matching logic, and exception handling rules are defined here. The process owner does not need to read this document; it is written for the person doing the build.
01Process snapshot
02Agent specifications
The Deposit Monitor Agent is the entry point of the automation. It listens for new invoices created in Xero that carry deposit terms, writes a structured record to the Google Sheets payment tracker, and then polls the Stripe payment feed on a daily schedule to detect incoming payments. Matching logic compares the incoming Stripe charge amount and client reference against open deposit records in Google Sheets. Confirmed matches pass downstream for reconciliation and confirmation. Unmatched payments, including partial payments, misreferenced transfers, and split payments, are flagged for bookkeeper review before any reconciliation action is taken. This agent replaces the daily manual bank feed check (step 3), the spreadsheet update (step 4), the deposit reconciliation in Xero (step 8), the final balance spreadsheet update (step 9), and the full settlement closure in Xero (step 11), as well as the initial invoice logging in step 1.
// Input xero_invoice_id: string // e.g. 'INV-0042' xero_client_name: string // matched against Stripe metadata.client_name xero_client_email: string deposit_amount_usd: float // from Xero invoice deposit line deposit_due_date: date // ISO 8601 invoice_total_usd: float invoice_currency: string // must be 'USD'; flag others for review // Stripe poll payload (daily) stripe_charge_id: string stripe_amount_cents: int // divide by 100 to compare stripe_metadata.invoice_ref: string stripe_metadata.client_name: string stripe_payment_date: date // Output (on match) match_status: 'confirmed' | 'unmatched' | 'partial' matched_charge_id: string matched_amount_usd: float sheets_row_id: string // row reference in payment tracker xero_reconciliation_status: 'applied' | 'pending_review' // Output (on no match by due date) overdue_flag: true days_overdue: int sheets_row_id: string
- Xero API: requires the xero-accounting OAuth 2.0 scope. Use the Xero webhook endpoint for invoice.created events; register the webhook URL in the Xero developer portal under the connected app. Webhook secret must be stored in the shared credential store and validated on every incoming request.
- Stripe API: use the /v1/charges/search or /v1/payment_intents/list endpoint with a date range filter. The Stripe account must be on at least the standard plan to access metadata fields on charges. Confirm metadata key naming convention (invoice_ref, client_name) with the client before build.
- Google Sheets: the payment tracker sheet must be created before build begins. Confirm the sheet ID and tab name. The agent writes one row per invoice on creation and updates columns for match status, matched amount, and reconciliation date. Use a header row lock to prevent accidental column shifts.
- Matching logic: primary match on stripe_amount_cents equals deposit_amount_usd multiplied by 100, AND stripe_metadata.invoice_ref equals xero_invoice_id. Secondary fallback: fuzzy match on stripe_metadata.client_name against xero_client_name using normalised lowercase comparison. If neither condition is met, flag as unmatched.
- Partial payment handling: if stripe_amount_cents is greater than zero but less than deposit_amount_usd multiplied by 100, set match_status to 'partial' and route to bookkeeper review queue. Do not auto-reconcile.
- Dedupe: store processed stripe_charge_id values in a Google Sheets lookup column or a lightweight key-value store in the orchestration layer. On each poll, skip any charge_id already present. This prevents double-reconciliation.
- Xero reconciliation: only trigger the Xero payment application call after match_status is 'confirmed'. For 'pending_review' or 'partial', write to Google Sheets only. The bookkeeper review step must complete before the reconciliation branch executes.
- Must confirm before build: exact deposit term field name used on Xero invoices (custom field or line item label), Stripe account live/test mode, Google Sheets file ID and tab name, and whether clients are currently instructed to quote the invoice number as payment reference.
The Client Communication Agent handles all outbound email to the client throughout the deposit and payment lifecycle. It receives triggers from the Deposit Monitor Agent, either an overdue flag or a confirmed match, and sends the appropriate email via Gmail. Overdue chasers are sent on a pre-set schedule with duplicate-send prevention. Deposit confirmations and final payment reminders are sent once per qualifying event. Every outbound email is logged automatically as a contact activity in HubSpot against the matching client record, including the date sent and the outstanding amount. This agent replaces manual steps 2, 5, 6, 7, and 10.
// Input (chaser branch) xero_client_email: string xero_client_name: string xero_invoice_id: string deposit_amount_usd: float deposit_due_date: date days_overdue: int overdue_flag: true sheets_row_id: string // Input (confirmation branch) xero_client_email: string xero_client_name: string xero_invoice_id: string matched_amount_usd: float invoice_total_usd: float remaining_balance_usd: float // invoice_total_usd minus matched_amount_usd final_payment_due_date: date match_status: 'confirmed' // Output gmail_message_id: string // stored for dedupe reference email_type: 'chaser' | 'confirmation' | 'final_reminder' hubspot_activity_id: string // created engagement record ID hubspot_contact_id: string send_timestamp: datetime sheets_row_updated: true
- Gmail API: requires the gmail.send OAuth scope. Authenticate as the sending address used by the business (e.g. accounts@[YourCompany.com]). Do not use a personal Gmail account. Confirm the sending address before build.
- Email templates: build three templates as parameterised strings within the orchestration layer: (1) overdue chaser referencing invoice_id, deposit_amount_usd, and days_overdue; (2) deposit confirmation referencing matched_amount_usd and remaining_balance_usd; (3) final payment reminder referencing remaining_balance_usd and final_payment_due_date. Store template bodies in the workflow configuration, not hardcoded in the script.
- Duplicate-send prevention: before sending any chaser, query the Gmail sent folder for messages with the subject line containing xero_invoice_id sent within the previous 5 days. If a match exists, skip and log to Google Sheets. Also store gmail_message_id in the payment tracker row after each send.
- Chaser schedule: do not send on Saturday or Sunday. Add a day-of-week check before the Gmail send action. The first chaser fires on day 1 overdue; subsequent chasers fire every 3 days. Maximum 4 chasers before escalation flag is written to Google Sheets for manual review.
- HubSpot activity logging: use the HubSpot Engagements API (POST /crm/v3/objects/emails) to create an email engagement. Match the client to a HubSpot contact using xero_client_email. If no matching contact is found, log a warning to the error table and skip the HubSpot write (do not fail the whole workflow). HubSpot tier must be at least Starter for API access.
- HubSpot contact name alignment: client names in Xero and HubSpot must match before go-live. A one-off data cleanup aligning names across both tools is required. Flag this as a pre-build dependency.
- Google Sheets update: after each email send, write email_type, send_timestamp, and gmail_message_id to the corresponding sheets_row_id. This maintains a full audit trail without relying solely on HubSpot.
- Must confirm before build: sending Gmail address and OAuth consent completed, HubSpot account tier and API key, chaser timing preferences (days overdue before first chaser, interval between chasers), and whether the client team wants a CC or BCC on any outbound email.
The Internal Notification Agent is the lightest of the three agents. It fires a single Slack message to the designated project channel when the Deposit Monitor Agent confirms that an invoice is fully paid and closed in Xero. The message includes the client name, invoice reference, and cleared amount so the project team can proceed without logging into any finance tool. This agent replaces manual step 12.
// Input xero_invoice_id: string xero_client_name: string invoice_total_usd: float cleared_date: date slack_channel_id: string // set in config, not dynamic // Output slack_message_ts: string // Slack message timestamp, used as idempotency key slack_channel_id: string notification_logged: true // written back to Google Sheets payment tracker row
- Slack API: use the chat.postMessage method with the bot token scope chat:write. The bot must be added to the target channel before deployment. Confirm the channel ID (not the display name) before build.
- Message format: use Slack Block Kit with a header block ('Payment Cleared'), a section block containing client name, invoice reference, and cleared amount, and a context block with the cleared date. Keep the message under 200 characters in the plain-text fallback field.
- Idempotency: store slack_message_ts in the Google Sheets payment tracker row after posting. Before posting, check whether a non-null value already exists in that column for the same invoice. If yes, skip. This prevents duplicate Slack messages if the workflow reruns.
- Xero read: a lightweight Xero GET /Invoices/{InvoiceID} call is used to confirm the invoice status is 'PAID' before posting. Do not rely solely on the upstream agent output as the source of truth for this check.
- Must confirm before build: Slack channel ID for the project team, whether multiple channels need notification, and Slack bot installation and permissions completed in the workspace.
03End-to-end data flow
// ────────────────────────────────────────────────────────────────��
// TRIGGER: Xero webhook fires on invoice.created
// ─────────────────────────────────────────────────────────────────
xero_webhook_payload {
xero_invoice_id: 'INV-0042'
xero_client_name: 'Acme Pty Ltd'
xero_client_email: 'finance@acme.example'
deposit_amount_usd: 1500.00
deposit_due_date: '2025-05-10'
invoice_total_usd: 6000.00
invoice_currency: 'USD'
}
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 1: Deposit Monitor Agent receives webhook payload
// ─────────────────────────────────────────────────────────────────
// ACTION: Write initial row to Google Sheets payment tracker
sheets.appendRow('PaymentTracker', {
invoice_id: xero_invoice_id, // 'INV-0042'
client_name: xero_client_name, // 'Acme Pty Ltd'
client_email: xero_client_email,
deposit_due: deposit_due_date, // '2025-05-10'
deposit_amount: deposit_amount_usd, // 1500.00
invoice_total: invoice_total_usd, // 6000.00
match_status: 'pending',
chaser_count: 0
})
// Returns: sheets_row_id -> 'row_47'
// ACTION: Daily Stripe poll (cron, every 24h)
stripe.charges.list({
created_gte: deposit_due_date - 7_days,
limit: 100
})
// Per charge, evaluate:
// PRIMARY MATCH: stripe_amount_cents == deposit_amount_usd * 100
// AND stripe_metadata.invoice_ref == xero_invoice_id
// SECONDARY MATCH: normalise(stripe_metadata.client_name)
// == normalise(xero_client_name)
// PARTIAL: 0 < stripe_amount_cents < deposit_amount_usd * 100
// UNMATCHED: no condition met
// ─────────────────────────────────────────────────────────────────
// BRANCH A: match_status == 'confirmed'
// ─────────────────────────────────────────────────────────────────
match_result {
match_status: 'confirmed'
matched_charge_id: 'ch_3PxT7A2eZvKYlo2C0abc1234'
matched_amount_usd: 1500.00
sheets_row_id: 'row_47'
xero_reconciliation_status: 'applied'
}
// ACTION: Apply payment to Xero invoice
xero.payments.create({
InvoiceID: xero_invoice_id, // 'INV-0042'
Date: stripe_payment_date,
Amount: matched_amount_usd, // 1500.00
Reference: matched_charge_id
})
// ACTION: Update Google Sheets row
sheets.updateRow('row_47', {
match_status: 'confirmed',
matched_amount: 1500.00,
reconciled_date: today,
remaining_balance: 4500.00 // invoice_total - matched_amount
})
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 2: Deposit Monitor Agent -> Client Communication Agent
// ─────────────────────────────────────────────────────────────────
communication_trigger_payload {
email_type: 'confirmation'
xero_client_email: 'finance@acme.example'
xero_client_name: 'Acme Pty Ltd'
xero_invoice_id: 'INV-0042'
matched_amount_usd: 1500.00
remaining_balance_usd: 4500.00
final_payment_due_date: '2025-06-10'
sheets_row_id: 'row_47'
}
// ACTION: Send deposit confirmation via Gmail
gmail.send({
to: xero_client_email,
subject: 'Deposit Received: INV-0042',
body: template('confirmation', {
client_name: xero_client_name,
matched_amount: matched_amount_usd,
remaining_balance: remaining_balance_usd,
due_date: final_payment_due_date
})
})
// Returns: gmail_message_id -> 'msg_abc9876'
// ACTION: Log activity in HubSpot
hubspot.engagements.create({
type: 'EMAIL',
contact_id: hubspot_contact_id, // resolved via xero_client_email
metadata: {
subject: 'Deposit Received: INV-0042',
sent_at: send_timestamp,
amount_outstanding: remaining_balance_usd
}
})
// Returns: hubspot_activity_id -> 'eng_112233'
// ─────────────────────────────────────────────────────────────────
// BRANCH B: overdue_flag == true (no match by deposit_due_date)
// ─────────────────────────────────────────────────────────────────
overdue_payload {
email_type: 'chaser'
xero_client_email: 'finance@acme.example'
xero_invoice_id: 'INV-0042'
deposit_amount_usd: 1500.00
days_overdue: 1
sheets_row_id: 'row_47'
}
// Day-of-week check: skip Saturday (6) and Sunday (0)
// Dedupe check: query Gmail sent for subject containing 'INV-0042' within last 5 days
// If no duplicate found, send chaser and increment sheets[row_47].chaser_count
// Max chasers: 4. On chaser_count == 4, set sheets[row_47].escalation_flag = true
// ─────────────────────────────────────────────────────────────────
// BRANCH C: match_status == 'partial' or 'unmatched' -> manual review
// ─────────────────────────────────────────────────────────────────
// sheets.updateRow('row_47', { match_status: 'pending_review' })
// No Xero reconciliation call. No Gmail send.
// Bookkeeper reviews Google Sheets flag and confirms allocation manually.
// On bookkeeper confirmation, workflow resumes from BRANCH A.
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 3: Full settlement confirmed -> Internal Notification Agent
// ─────────────────────────────────────────────────────────────────
settlement_payload {
xero_invoice_id: 'INV-0042'
xero_client_name: 'Acme Pty Ltd'
invoice_total_usd: 6000.00
cleared_date: '2025-06-11'
slack_channel_id: 'C04XXXXABCD' // set in config
}
// Pre-post check: GET /Invoices/INV-0042 -> Status == 'PAID'
// Idempotency check: sheets[row_47].slack_message_ts is null
// ACTION: Post to Slack
slack.chat.postMessage({
channel: slack_channel_id,
blocks: [
{ type: 'header', text: 'Payment Cleared' },
{ type: 'section', text: 'Acme Pty Ltd | INV-0042 | $6,000.00' },
{ type: 'context', text: 'Cleared: 2025-06-11' }
],
text: 'Payment cleared: Acme Pty Ltd INV-0042 $6,000.00'
})
// Returns: slack_message_ts -> '1749000000.123456'
// ACTION: Write slack_message_ts to Google Sheets
sheets.updateRow('row_47', {
slack_notified: true,
slack_message_ts: '1749000000.123456'
})04Recommended build stack
More documents for this process
Every document generated for Client Deposit & Payment Tracking.