Back to Subscription & Recurring Billing

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

Subscription & Recurring Billing

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

This document gives the FullSpec build team everything needed to construct, connect, and verify the three-agent Subscription and Recurring Billing automation. It covers the current-state process step map, full agent specifications with IO contracts, an end-to-end data flow trace, and the recommended build stack. The owner-side contacts (Rachel Tan, Owen Fischer, Priya Menon) handle credential provisioning and exception review; FullSpec handles all build, integration, and testing work. Refer any access or permissions questions to support@gofullspec.com.

01Process snapshot

Step
Name
Description
1
Pull Active Subscriber List
Billing Admin exports the active subscriber list from HubSpot, often cross-checked against a spreadsheet. Time cost: 25 min/cycle.
2
Check Subscription Tier and Pricing
For each subscriber the admin confirms the correct plan tier and current pricing, catching mid-cycle upgrades or downgrades. Errors cause over- or under-billing. Time cost: 20 min/cycle.
3
Create Invoices in Xero
BOTTLENECK. Admin creates each of ~120 invoices individually in Xero, entering subscriber details, line items, and amounts by hand. Time cost: 110 min/cycle.
4
Send Invoice Emails to Customers
Invoices are emailed to each subscriber from Xero or Gmail. Personalisation is minimal; the process is fully repetitive. Time cost: 30 min/cycle.
5
Process Payments via Stripe
Admin logs into Stripe to confirm which charge attempts succeeded or failed. No automated cross-reference to the Xero invoice list exists. Time cost: 20 min/cycle.
6
Identify and Log Failed Payments
BOTTLENECK. Failed payment records are noted manually in a spreadsheet and the subscriber is flagged for follow-up. Easily missed on busy days. Time cost: 20 min/cycle.
7
Send Failed Payment Notifications
Admin writes and sends a payment-failure email to each affected subscriber. Timing and tone vary by sender. Time cost: 25 min/cycle.
8
Retry Failed Payments Manually
BOTTLENECK. Admin manually retries failed charges in Stripe after an unstructured delay. No systematic retry schedule exists; some payments are retried too late. Time cost: 20 min/cycle.
9
Send Renewal Reminder Emails
Subscribers approaching annual renewal receive a reminder only if the admin notices the calendar date. Many reminders are sent late or skipped. Time cost: 20 min/cycle.
10
Reconcile Payments in Xero
Bookkeeper matches Stripe transactions to Xero invoices and marks them paid. Mismatches require manual investigation. Time cost: 45 min/cycle.
11
Update Subscriber Status in HubSpot
Admin updates each subscriber CRM record with current billing status. Often deferred, leaving records stale. Time cost: 20 min/cycle.
12
Notify Finance Team of Billing Run
Admin posts a summary to the finance team via Slack covering totals collected, failed payments, and anomalies. Time cost: 10 min/cycle.
Time cost summary: Total manual time per billing cycle is 365 minutes (approximately 6 hours). At a monthly billing frequency this represents approximately 6 hours/week of admin load when daily failed-payment follow-up is included. The automation replaces steps 1, 2, 3, 5 (Billing Cycle Agent), steps 6, 7, 8, 11 (Failed Payment Recovery Agent), and steps 4, 9, 10, 12 (Reconciliation and Notification Agent). The only remaining manual step is reviewing unmatched reconciliation items flagged by the system (mapped to automated canvas node n10).
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Billing Cycle Agent

Handles subscriber retrieval, invoice creation, and payment charging at the start of each billing cycle. The agent reads all active subscription records from HubSpot, maps each record to the correct Xero account code and tax treatment based on plan tier, generates a Xero invoice for every subscriber, and triggers a Stripe charge against the stored payment method. It captures the charge outcome (success or failure) for every invoice and passes that payload downstream. This agent must complete atomically per subscriber: if invoice creation in Xero fails for a given record, the Stripe charge for that subscriber must not fire. Estimated build time: 14 hours. Complexity: Moderate.

Trigger
Scheduled: billing period start date reached for one or more active subscriptions in HubSpot (monthly cron, configurable to day-of-month per subscriber).
Tools
HubSpot (subscriber query), Xero (invoice creation), Stripe (charge initiation).
Replaces steps
1 (Pull Active Subscriber List), 2 (Check Subscription Tier and Pricing), 3 (Create Invoices in Xero), 5 (Process Payments via Stripe).
Estimated build
14 hours / Moderate
// Input
HubSpot query filter: { lifecyclestage: 'customer', hs_lead_status: 'active', billing_cycle_date: TODAY }
Fields consumed: contact_id, email, first_name, last_name, plan_tier, plan_price_usd, stripe_customer_id, xero_contact_id, renewal_date

// Output
Per subscriber: { subscriber_id, xero_invoice_id, stripe_payment_intent_id, charge_status: 'succeeded'|'failed', amount_usd, currency: 'USD', timestamp_utc }
Batch summary: { total_invoices_created, total_charged, total_failed, billing_run_id }
  • HubSpot tier must be Sales Hub Starter or above to allow API-level contact querying with custom property filters. Confirm the billing_cycle_date and plan_tier custom properties exist before build; create them if absent.
  • Xero plan requires Xero Standard or above for API invoice creation. Confirm account codes per plan tier (e.g. 200 for standard subscriptions, 201 for premium) and tax type identifiers (e.g. OUTPUT, OUTPUT2) before mapping is finalised.
  • Stripe charge uses the PaymentIntents API with confirm: true and the subscriber's stored default PaymentMethod. Do not use the legacy Charges API. Confirm that each subscriber record holds a valid stripe_customer_id with a default payment method attached.
  • Invoice creation in Xero must precede the Stripe charge attempt in the execution order. Use a sequential per-subscriber loop, not a parallel batch, to enforce this dependency.
  • Deduplication: check for an existing Xero invoice with the same xero_contact_id and invoice date before creating a new one. If a draft invoice already exists, update it rather than creating a duplicate.
  • Proration is out of scope for the initial build. Fixed monthly pricing only. Flag any subscriber with a non-standard plan_price_usd value for manual review rather than attempting automated proration.
  • Confirm with Rachel Tan (Billing Admin) that the HubSpot active subscriber list is clean and that no test or churned contacts remain tagged as active before the first live run.
Failed Payment Recovery Agent

Monitors Stripe for failed payment events, sends recovery emails via Gmail, schedules retry attempts at a fixed 72-hour interval, and updates subscriber status in HubSpot after each attempt outcome. The agent is event-driven: it activates on receipt of a Stripe payment_intent.payment_failed webhook rather than on a schedule. It maintains a retry counter per subscriber and stops automated retries after two attempts (72 hours and 144 hours post-failure), at which point the subscriber record is flagged in HubSpot for manual escalation and no further automated emails are sent. Estimated build time: 12 hours. Complexity: Moderate.

Trigger
Stripe webhook: payment_intent.payment_failed event received by the automation platform's webhook endpoint.
Tools
Stripe (webhook source and retry execution), Gmail (failure notification and follow-up emails), HubSpot (subscriber status updates).
Replaces steps
6 (Identify and Log Failed Payments), 7 (Send Failed Payment Notifications), 8 (Retry Failed Payments Manually), 11 (Update Subscriber Status in HubSpot).
Estimated build
12 hours / Moderate
// Input
Stripe webhook payload: { type: 'payment_intent.payment_failed', data.object: { id: payment_intent_id, customer: stripe_customer_id, amount: int, currency: 'usd', last_payment_error: { code, message } } }
Lookup: HubSpot contact by stripe_customer_id -> { contact_id, email, first_name, plan_tier, retry_count, xero_invoice_id }

// Output (attempt 1 and attempt 2)
Gmail: recovery email sent to subscriber email with card-update link and personalised greeting
Stripe: PaymentIntent retry scheduled at T+72h and T+144h via the automation platform's delay node
HubSpot contact updated: { billing_status: 'payment_failed', retry_count: N, last_retry_attempt_utc: timestamp }

// On max retries exceeded
HubSpot contact updated: { billing_status: 'escalated', retry_count: 2 }
No further automated emails sent; record surfaced in finance team review queue
  • Stripe webhook endpoint must be registered in the Stripe Dashboard under Developers > Webhooks before build testing begins. The endpoint URL is generated by the automation platform and must be added to the Stripe account's live and test webhook listeners.
  • Listen specifically for payment_intent.payment_failed and payment_intent.succeeded events. Do not use the charge.failed event as it does not carry PaymentIntent metadata reliably.
  • Gmail send must use a verified sender address configured in the automation platform's Gmail credential. Confirm the sender address (e.g. billing@[YourCompany.com]) with Rachel Tan before build; it must match the domain's SPF and DKIM records.
  • The retry counter (retry_count) must be stored as a HubSpot custom numeric property on the contact object. Confirm this property exists or create it. Reset retry_count to 0 on successful payment confirmation from the Reconciliation Agent.
  • Retry timing must comply with Stripe's guidelines: do not retry sooner than 24 hours after a failure. The 72-hour interval satisfies this requirement. Confirm with Stripe account settings that automatic retries (Smart Retries) are disabled on the Stripe side to avoid double-retry conflicts.
  • If a subscriber's payment succeeds on retry, the Stripe payment_intent.succeeded webhook fires and the Reconciliation and Notification Agent takes over. Ensure the two agents do not both write to the same HubSpot billing_status field simultaneously; use idempotent writes keyed on payment_intent_id.
Reconciliation and Notification Agent

Matches Stripe payment confirmations to open Xero invoices, marks them as paid, sends payment confirmation emails to successful payers, queues renewal reminder emails for subscribers approaching their renewal date, and posts the billing run summary to the designated Slack finance channel. This agent handles both the initial successful payment path (triggered immediately after the Billing Cycle Agent) and the successful retry path (triggered when the Failed Payment Recovery Agent's retry clears). Unmatched items, where no open Xero invoice can be found for a given Stripe payment, are flagged in a Supabase exceptions table and surfaced to the bookkeeper (Owen Fischer) for manual review. Estimated build time: 12 hours. Complexity: Moderate.

Trigger
Stripe webhook: payment_intent.succeeded event received (covers both initial charges and retry successes).
Tools
Xero (invoice matching and mark-as-paid), Stripe (payment confirmation source), Gmail (confirmation emails and renewal reminders), Slack (billing run summary post).
Replaces steps
4 (Send Invoice Emails to Customers), 9 (Send Renewal Reminder Emails), 10 (Reconcile Payments in Xero), 12 (Notify Finance Team of Billing Run).
Estimated build
12 hours / Moderate
// Input
Stripe webhook payload: { type: 'payment_intent.succeeded', data.object: { id: payment_intent_id, customer: stripe_customer_id, amount_received: int, currency: 'usd' } }
Xero lookup: open invoice where Contact.ContactID = xero_contact_id AND Status = 'AUTHORISED' AND AmountDue = (amount_received / 100)
HubSpot lookup by stripe_customer_id: { contact_id, email, first_name, renewal_date, plan_tier }

// Output
Xero: invoice Status set to 'PAID', payment record created with { Date: today, Amount: amount_received_usd, Account: bank_account_id }
Gmail: confirmation email sent to subscriber email with invoice PDF link and payment amount
Gmail (conditional): renewal reminder email queued if renewal_date is within 30 days
Slack: message posted to #finance-billing channel: { billing_run_id, total_invoices, total_paid_usd, total_failed, retry_successes, unmatched_count }

// On unmatched invoice
Supabase exceptions table insert: { payment_intent_id, stripe_customer_id, amount_usd, error: 'no_matching_xero_invoice', flagged_at_utc: timestamp }
Slack alert posted to #finance-billing: 'Unmatched payment flagged for review: [stripe_customer_id]'
  • Xero invoice matching must use both xero_contact_id and AmountDue as match criteria to prevent incorrect reconciliation where a subscriber has multiple open invoices. If more than one match is found, write to the exceptions table rather than guessing.
  • The Xero payment record must reference the correct Xero bank account ID corresponding to the Stripe settlement account. Confirm this account ID with Owen Fischer (Bookkeeper) before the reconciliation build begins.
  • The Slack summary should be posted once per billing run, not once per subscriber payment. Accumulate the run totals in a temporary data store (a Supabase billing_run_summary row keyed on billing_run_id) and post to Slack only when the full run is complete or a configurable timeout (e.g. 4 hours) is reached.
  • Renewal reminder emails should only fire if renewal_date is within the next 30 days and the subscriber's billing_status in HubSpot is 'active'. Do not send reminders to subscribers currently in a 'payment_failed' or 'escalated' state.
  • Gmail invoice PDF link should point to the Xero invoice online view URL (available from the Xero API response as OnlineInvoiceUrl). Confirm Xero online invoice sharing is enabled for the organisation before referencing this URL in email templates.
  • Confirm the target Slack channel name with Priya Menon (Finance Manager). The automation platform credential must have the channels:write and chat:write OAuth scopes for the target workspace.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full end-to-end data flow: trigger through all three agent handoffs, exact field names at each stage.
// ============================================================
// TRIGGER
// ============================================================
SCHEDULE: cron(0 6 1 * *)  // 06:00 UTC on billing period start date
  -> billing_run_id = uuid()
  -> date_today = ISO8601 date

// ============================================================
// BILLING CYCLE AGENT
// ============================================================
HUBSPOT_QUERY:
  endpoint: GET /crm/v3/objects/contacts/search
  filter: { lifecyclestage = 'customer', hs_lead_status = 'active', billing_cycle_date = date_today }
  fields_returned: [contact_id, email, first_name, last_name, plan_tier,
                    plan_price_usd, stripe_customer_id, xero_contact_id, renewal_date]
  -> subscriber_list[]

FOR EACH subscriber IN subscriber_list:

  // Dedupe check
  XERO_CHECK:
    endpoint: GET /api.xro/2.0/Invoices
    filter: ContactID = subscriber.xero_contact_id AND Date = date_today AND Status = 'DRAFT'|'AUTHORISED'
    IF existing_invoice FOUND -> UPDATE existing_invoice
    ELSE -> CREATE new invoice

  XERO_CREATE_INVOICE:
    endpoint: POST /api.xro/2.0/Invoices
    body: { Type: 'ACCREC', Contact: { ContactID: subscriber.xero_contact_id },
            Date: date_today, DueDate: date_today,
            LineItems: [{ Description: subscriber.plan_tier, UnitAmount: subscriber.plan_price_usd,
                          AccountCode: '200'|'201', TaxType: 'OUTPUT' }],
            Status: 'AUTHORISED' }
    -> xero_invoice_id, xero_invoice_url

  STRIPE_CHARGE:
    endpoint: POST /v1/payment_intents
    body: { amount: subscriber.plan_price_usd * 100, currency: 'usd',
            customer: subscriber.stripe_customer_id,
            payment_method: default_payment_method,
            confirm: true, off_session: true,
            metadata: { xero_invoice_id: xero_invoice_id, billing_run_id: billing_run_id } }
    -> payment_intent_id, charge_status ('succeeded'|'requires_payment_method'|'failed')

// ============================================================
// DECISION: charge_status
// ============================================================
IF charge_status = 'succeeded':
  -> EMIT payment_intent.succeeded -> Reconciliation and Notification Agent

IF charge_status = 'failed' OR 'requires_payment_method':
  -> EMIT payment_intent.payment_failed -> Failed Payment Recovery Agent

// ============================================================
// FAILED PAYMENT RECOVERY AGENT
// ============================================================
ON_EVENT: payment_intent.payment_failed
  stripe_payload: { payment_intent_id, stripe_customer_id, amount, last_payment_error.code }

  HUBSPOT_LOOKUP:
    endpoint: GET /crm/v3/objects/contacts?filter=stripe_customer_id={stripe_customer_id}
    -> contact_id, email, first_name, plan_tier, retry_count, xero_invoice_id

  GMAIL_SEND (attempt 1 email):
    to: subscriber.email
    subject: 'Action required: payment for your [YourCompany.com] subscription'
    body_fields: [first_name, plan_tier, amount_usd, card_update_url]
    -> message_id

  HUBSPOT_UPDATE:
    endpoint: PATCH /crm/v3/objects/contacts/{contact_id}
    body: { billing_status: 'payment_failed', retry_count: 1,
            last_retry_attempt_utc: timestamp_utc }

  DELAY: 72 hours

  STRIPE_RETRY:
    endpoint: POST /v1/payment_intents/{payment_intent_id}/confirm
    -> charge_status

  IF charge_status = 'succeeded': -> EMIT payment_intent.succeeded -> Reconciliation Agent
  IF charge_status = 'failed' AND retry_count < 2:
    GMAIL_SEND (attempt 2 email): same template, updated tone
    HUBSPOT_UPDATE: { retry_count: 2 }
    DELAY: 72 hours
    STRIPE_RETRY (attempt 2) -> charge_status
    IF 'succeeded': -> EMIT payment_intent.succeeded -> Reconciliation Agent
    IF 'failed': HUBSPOT_UPDATE { billing_status: 'escalated' } // no further automated action

// ============================================================
// RECONCILIATION AND NOTIFICATION AGENT
// ============================================================
ON_EVENT: payment_intent.succeeded
  stripe_payload: { payment_intent_id, stripe_customer_id, amount_received, metadata.xero_invoice_id, metadata.billing_run_id }

  XERO_MATCH:
    endpoint: GET /api.xro/2.0/Invoices/{xero_invoice_id}
    validate: Status = 'AUTHORISED' AND AmountDue = amount_received / 100
    IF match: PROCEED
    IF no match: INSERT exceptions table { payment_intent_id, stripe_customer_id, amount_usd, error: 'no_matching_xero_invoice' }
               POST Slack alert -> HALT for this subscriber

  XERO_MARK_PAID:
    endpoint: PUT /api.xro/2.0/Invoices/{xero_invoice_id}/Payments
    body: { Date: date_today, Amount: amount_received_usd, Account: { Code: 'STRIPE-BANK' } }
    -> xero_payment_id, xero_invoice_url (OnlineInvoiceUrl)

  HUBSPOT_LOOKUP: stripe_customer_id -> { contact_id, email, first_name, renewal_date, billing_status }

  GMAIL_SEND (confirmation):
    to: subscriber.email
    body_fields: [first_name, amount_usd, date_today, xero_invoice_url]

  IF renewal_date <= date_today + 30 days AND billing_status = 'active':
    GMAIL_SEND (renewal reminder):
      to: subscriber.email
      body_fields: [first_name, renewal_date, plan_tier]

  HUBSPOT_UPDATE: { billing_status: 'active', retry_count: 0 }

  SUPABASE_UPSERT billing_run_summary:
    { billing_run_id, total_paid_count++, total_paid_usd += amount_received_usd }

  // After all events processed or timeout reached:
  SLACK_POST:
    channel: '#finance-billing'
    fields: [billing_run_id, total_invoices_created, total_paid_usd, total_failed_count,
             retry_success_count, unmatched_count]
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool with one workflow per agent (three workflows total): Billing Cycle Agent Workflow, Failed Payment Recovery Agent Workflow, and Reconciliation and Notification Agent Workflow. All three workflows share a single credential store configured at the platform level. Credentials are referenced by name (e.g. STRIPE_API_KEY, XERO_OAUTH2, HUBSPOT_PRIVATE_APP_TOKEN, GMAIL_OAUTH2, SLACK_BOT_TOKEN) and never hard-coded in workflow nodes. The orchestration layer must support scheduled triggers (cron), webhook listeners, delay nodes of up to 144 hours, and Supabase HTTP calls or a native Supabase node.
Webhook configuration
Two Stripe webhook endpoints must be registered: one listening for payment_intent.payment_failed (routes to Failed Payment Recovery Agent Workflow) and one listening for payment_intent.succeeded (routes to Reconciliation and Notification Agent Workflow). Both endpoints are generated by the automation platform and registered in the Stripe Dashboard under Developers > Webhooks. Each endpoint must use a Stripe webhook signing secret, validated in the workflow's first node before any processing occurs. Use the Stripe test-mode webhook listener during sandbox testing; switch to live-mode endpoints at go-live.
Templating approach
All outbound Gmail emails use HTML templates stored as named template strings within the workflow credential/environment store. Templates are parameterised with subscriber field variables (first_name, plan_tier, amount_usd, xero_invoice_url, renewal_date, card_update_url). Do not compose email body content inline in workflow nodes. The Slack billing summary uses a Block Kit JSON payload to ensure consistent formatting in the #finance-billing channel. Maintain one template file per email type: payment-confirmation, failed-payment-recovery, renewal-reminder.
Error logging
A Supabase PostgreSQL instance hosts two tables: billing_exceptions (columns: id uuid, payment_intent_id text, stripe_customer_id text, amount_usd numeric, error text, flagged_at_utc timestamptz, resolved bool) and billing_run_summary (columns: billing_run_id uuid, run_date date, total_invoices int, total_paid int, total_paid_usd numeric, total_failed int, retry_success int, unmatched int, slack_posted bool). Any unhandled workflow error triggers a Slack alert to #finance-billing and inserts a row into billing_exceptions. The alert must include the workflow name, the failing node name, the subscriber identifier, and a UTC timestamp.
Testing approach
All three workflows are built and verified in sandbox-first mode: Stripe test mode (use test card 4000000000000002 for failure scenarios and 4242424242424242 for success), Xero demo company, HubSpot sandbox portal, and Gmail send to an internal test address only. No live subscriber data or real Stripe charges are used until the parallel-run phase. A parallel run of one full billing cycle alongside the manual process is required before cutover, as specified in the delivery plan.
Estimated total build time
Billing Cycle Agent: 14 hours. Failed Payment Recovery Agent: 12 hours. Reconciliation and Notification Agent: 12 hours. End-to-end integration testing and parallel run support: 0 hours allocated separately (included in the 38-hour total build effort). Total: 38 hours across the 5-week delivery window.
Before any workflow node is built against a live credential, confirm the following with the owner-side team: (1) HubSpot custom properties billing_cycle_date, plan_tier, billing_status, and retry_count exist on the Contact object. (2) Xero account codes for each plan tier and the Stripe settlement bank account ID are documented and signed off by Owen Fischer. (3) The Stripe account has Smart Retries disabled to prevent conflicts with the 72-hour retry logic. (4) The Gmail sender address is verified and SPF/DKIM records are in place. (5) The Slack bot has been added to the #finance-billing channel with chat:write scope. Raise any blockers to support@gofullspec.com immediately.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Subscription & Recurring Billing.

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