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
Accounts Payable Management
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Accounts Payable Management automation. It covers the full current-state step map, per-agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. Everything the FullSpec team needs to begin development is contained here. Your team's role at this stage is to confirm access credentials, approve the Xero chart-of-accounts mapping, and designate the Slack approver channel before the first agent build begins.
01Process snapshot
02Agent specifications
Monitors the designated AP Gmail inbox and the Dext document queue for new invoice documents. When a document is detected, the agent triggers the Dext OCR extraction pipeline to pull structured fields including supplier name, invoice number, invoice date, due date, currency, line item descriptions, quantities, unit prices, and totals. The extracted record is then compared against existing Xero bills using the invoice number and supplier name as a composite key. If no duplicate is found the record is passed downstream with a duplicate-clear flag. If a duplicate is detected the record is flagged and a Slack notification is sent to the bookkeeper for manual resolution. This agent handles all intake volume without any manual sorting. Estimated build time: 10 hours. Complexity: Moderate.
// Input
dext.webhook_payload {
document_id: string,
supplier_name: string,
invoice_number: string,
invoice_date: ISO8601,
due_date: ISO8601,
currency: string, // default 'USD'
line_items: [
{ description: string, quantity: number, unit_price: number, total: number }
],
total_amount: number,
document_url: string // hosted PDF link from Dext
}
// OR gmail.trigger_payload {
message_id: string,
from_email: string,
subject: string,
attachment_name: string,
attachment_mime: string, // must be 'application/pdf'
attachment_base64: string
}
// Output (duplicate-clear)
invoice_record {
source: 'dext' | 'gmail',
document_id: string,
supplier_name: string,
invoice_number: string,
invoice_date: ISO8601,
due_date: ISO8601,
currency: string,
line_items: [ { description, quantity, unit_price, total } ],
total_amount: number,
document_url: string,
duplicate_flag: false
}
// Output (duplicate detected, routed to Slack exception)
exception_record {
invoice_number: string,
supplier_name: string,
duplicate_flag: true,
existing_xero_bill_id: string,
slack_alert_sent: true
}- Dext tier must be Dext Prepare (previously Receipt Bank) with API access enabled. Confirm the API key and organisation ID before build begins.
- Gmail monitor must use a dedicated AP label (e.g. 'AP-Invoices') applied by a Gmail filter on the shared inbox. The automation polls this label only, not the full inbox, to avoid processing non-invoice mail.
- Duplicate check in Xero uses GET /api.xro/2.0/Invoices?InvoiceNumber={invoice_number}&Contact.Name={supplier_name}. A result count greater than zero triggers the exception branch.
- If the Dext webhook and the Gmail monitor fire for the same physical document within a 5-minute window (a supplier sends by email AND Dext also picks it up), the flow must deduplicate on invoice_number and suppress the second trigger. Implement a short-term idempotency cache keyed on invoice_number with a 10-minute TTL.
- Dext extraction accuracy must be validated against a batch of 20 historical invoices before go-live. Any field with extraction confidence below 85% must be flagged for human review rather than passed downstream.
- Gmail attachment filter must reject non-PDF attachments (images, .xls, .docx) and send a soft alert to the bookkeeper's Slack DM listing the unprocessed file name.
- Confirm whether the business uses a shared AP Gmail alias (e.g. ap@company.com) or a personal bookkeeper inbox. The monitor label and OAuth scope differ between the two.
Receives the duplicate-clear invoice record from the Invoice Intake Agent. The agent queries Xero's transaction history for the identified supplier and uses description-matching logic against the chart of accounts to suggest the most probable account code for each line item. The suggestion is scored by frequency of prior use for that supplier-description pair. The structured invoice record, suggested codes, and a confidence label (High, Medium, or Low) are assembled into a Slack Block Kit approval message and posted to the configured approver channel. If no response is received within the configured window (default 24 hours) the agent sends one automated reminder. A second non-response after 48 hours routes the invoice to the bookkeeper as a priority exception. Approved responses pass the confirmed codes downstream. Rejected or queried responses route to a bookkeeper exception thread. Estimated build time: 16 hours. Complexity: Complex.
// Input
invoice_record {
supplier_name: string,
invoice_number: string,
due_date: ISO8601,
currency: string,
line_items: [ { description, quantity, unit_price, total } ],
total_amount: number,
document_url: string,
duplicate_flag: false
}
// Coding lookup (internal, Xero GET /Reports/AgedPayablesByContact)
xero_history {
contact_id: string,
prior_line_items: [
{ description: string, account_code: string, frequency: number }
]
}
// Output (approved)
approved_invoice {
invoice_record: { ...all fields above },
coded_lines: [
{ description, account_code, account_name, confidence: 'High'|'Medium'|'Low' }
],
approver_slack_id: string,
approved_at: ISO8601,
approval_status: 'approved'
}
// On approval rejection or query
exception_record {
invoice_record: { ...all fields },
coded_lines: [ ... ],
approval_status: 'rejected' | 'queried',
approver_note: string,
routed_to_bookkeeper: true
}
// On approval timeout (48 hours, no response)
timeout_record {
invoice_record: { ...all fields },
approval_status: 'timeout',
reminders_sent: number,
routed_to_bookkeeper: true
}- Slack app must be installed into the workspace with scopes: chat:write, chat:write.public, reactions:read, channels:read, users:read. The bot token must be stored in the shared credential store, not hardcoded.
- Block Kit approval message must include: supplier name, invoice number, total amount, due date, each line item with suggested account code and confidence label, a link to the hosted PDF, and two action buttons: Approve and Query/Reject.
- Approver routing logic must reference a configurable approval-authority matrix (JSON object or lookup table) mapping invoice total bands to Slack user IDs. Confirm the matrix with the finance manager before build: default threshold is invoices above $2,000 route to the Finance Manager; below $2,000 route to the Bookkeeper.
- Reminder timing is configurable: default is first reminder at 24 hours, exception escalation at 48 hours. These values must be stored as environment variables so they can be changed without redeploying the workflow.
- Coding confidence is calculated as: High = account code used for this supplier-description pair in 5 or more prior bills; Medium = 2 to 4 prior uses; Low = 0 to 1 prior uses or new supplier. Low-confidence codes must be highlighted in amber in the Slack message.
- PO matching: if invoice_number contains a PO reference prefix (configurable regex), the agent queries a Google Drive folder for a matching PO document. If the PO is not found or amounts do not match within a 2% tolerance, the invoice is routed as an exception regardless of approval status.
- All Slack interaction payloads (button clicks) must be verified using the Slack signing secret before processing to prevent replay attacks. Confirm the signing secret is stored as an environment variable.
Receives the approved invoice record from the Coding and Approval Agent and executes the final three steps of the AP cycle without manual intervention. The agent creates the bill in Xero using the Xero Bills API, attaching the original invoice PDF and all confirmed line codes. It then schedules the payment against the business's configured payment run date, using the due date from the invoice record and a configurable payment-run-day offset. If the due date falls within 48 hours of the current date, the bill is flagged as urgent in Xero and a Slack alert is sent to the bookkeeper. Once Xero confirms the payment record, the agent composes and sends a remittance advice email via Gmail to the supplier's email address on file in Xero, referencing the invoice number and amount paid. Estimated build time: 12 hours. Complexity: Moderate.
// Input
approved_invoice {
supplier_name: string,
invoice_number: string,
invoice_date: ISO8601,
due_date: ISO8601,
currency: string,
coded_lines: [
{ description, quantity, unit_price, total, account_code }
],
total_amount: number,
document_url: string,
approver_slack_id: string,
approved_at: ISO8601
}
// Xero bill creation (POST /api.xro/2.0/Invoices)
xero_bill_payload {
Type: 'ACCPAY',
Contact: { Name: supplier_name },
Date: invoice_date,
DueDate: due_date,
LineItems: [ { Description, Quantity, UnitAmount, AccountCode } ],
Attachments: [ { FileName, MimeType: 'application/pdf', Content: base64 } ]
}
// Output: Xero bill created
xero_bill_result {
InvoiceID: string,
Status: 'AUTHORISED',
AmountDue: number,
DueDate: ISO8601
}
// Output: payment scheduled
xero_payment_result {
PaymentID: string,
Date: ISO8601, // next configured payment run date
Amount: number,
Status: 'AUTHORISED'
}
// Output: remittance email (Gmail send)
gmail_send_payload {
to: supplier_email, // from Xero Contact.EmailAddress
subject: 'Remittance Advice: Invoice {invoice_number}',
body_template: 'remittance_advice_v1',
template_vars: { supplier_name, invoice_number, amount_paid, payment_date, payer_name }
}
// On urgent invoice (due within 48 hours)
urgent_flag {
invoice_number: string,
due_date: ISO8601,
hours_until_due: number,
slack_alert_sent: true,
xero_bill_id: string
}- Xero OAuth 2.0 scopes required: accounting.transactions, accounting.contacts.read, accounting.attachments, accounting.settings.read. The token must be stored in the shared credential store and refreshed automatically using the Xero refresh token flow (token expiry: 30 minutes, refresh token expiry: 60 days).
- Bill creation must use Type: ACCPAY (accounts payable). Do not use ACCREC or DRAFT status. The bill must be posted as AUTHORISED so it is immediately visible in the Xero AP ledger.
- PDF attachment to the Xero bill must be base64-encoded and sent in the same API call as bill creation. If the document_url from Dext returns a 403 or 404 at build time, implement a retry with exponential back-off (3 attempts, 5-second initial delay) before routing to a bookkeeper exception.
- Payment scheduling uses POST /api.xro/2.0/Payments. The payment Date field must be set to the next configured payment run day (stored as an environment variable, e.g. 'FRIDAY'). If due_date is earlier than the next payment run day, flag as urgent and alert bookkeeper via Slack.
- Supplier email address is retrieved from the Xero Contact record (Contact.EmailAddress). If the field is empty the remittance step is skipped and a Slack alert is sent to the bookkeeper to add the supplier email in Xero before the next run.
- Gmail send must use a dedicated sending identity (e.g. ap@company.com). Confirm whether a Gmail alias or a separate Google Workspace account is in use. OAuth scope required: gmail.send. The remittance email template must be stored as a named template in the automation platform and versioned.
- All Xero API calls must include the Xero-tenant-id header. Confirm the tenant ID during stack audit. Multi-entity configurations require a separate tenant ID per entity and a routing field added to the invoice record.
03End-to-end data flow
// ============================================================
// TRIGGER: New invoice detected
// ============================================================
SOURCE: dext.webhook OR gmail.label_monitor('AP-Invoices')
IF source == 'dext':
raw_payload = dext.webhook_payload {
document_id, supplier_name, invoice_number, invoice_date,
due_date, currency, line_items[], total_amount, document_url
}
IF source == 'gmail':
raw_payload = gmail.message {
message_id, from_email, subject,
attachment_name, attachment_mime, // must be 'application/pdf'
attachment_base64
}
-> forward to dext.upload_document(attachment_base64)
-> await dext.extraction_result { document_id, ...fields }
// ============================================================
// AGENT 1: Invoice Intake Agent
// ============================================================
idempotency_check(invoice_number, TTL=10min) -> SUPPRESS if duplicate trigger
xero_duplicate_query = GET /api.xro/2.0/Invoices
?InvoiceNumber={invoice_number}
&Contact.Name={supplier_name}
-> result_count: number
IF result_count > 0:
-> exception_record { invoice_number, supplier_name,
duplicate_flag: true, existing_xero_bill_id }
-> slack.post_dm(bookkeeper_slack_id,
text: 'Duplicate invoice detected: {invoice_number} from {supplier_name}')
-> HALT flow for this invoice
IF result_count == 0:
-> invoice_record {
source, document_id, supplier_name, invoice_number,
invoice_date, due_date, currency, line_items[],
total_amount, document_url, duplicate_flag: false
}
-> PASS to Agent 2
// ============================================================
// AGENT 2: Coding and Approval Agent
// ============================================================
xero_history_query = GET /api.xro/2.0/Reports/AgedPayablesByContact
+ GET /api.xro/2.0/Invoices?ContactID={contact_id}&Status=AUTHORISED
-> prior_line_items[] { description, account_code, frequency }
FOR each line_item in invoice_record.line_items[]:
match = find(prior_line_items, where description ~= line_item.description)
coded_line = {
description: line_item.description,
account_code: match.account_code OR 'REVIEW_REQUIRED',
account_name: xero.chart_of_accounts[account_code].name,
confidence: match.frequency >= 5 ? 'High'
: match.frequency >= 2 ? 'Medium' : 'Low'
}
// PO matching check (optional, if invoice contains PO reference)
IF invoice_number MATCHES po_prefix_regex:
po_doc = google_drive.search(folder_id=AP_PO_FOLDER,
query: invoice_number)
IF po_doc NOT FOUND OR ABS(po_doc.total - total_amount) / total_amount > 0.02:
-> exception_record { invoice_record, reason: 'PO mismatch or not found' }
-> slack.post_dm(bookkeeper_slack_id) -> HALT
// Approval routing
approver_slack_id = approval_matrix[
total_amount >= 2000 ? 'finance_manager' : 'bookkeeper'
]
slack.post_block_kit(channel=approver_slack_id, blocks={
supplier_name, invoice_number, total_amount, due_date,
coded_lines[], document_url,
actions: [ { action_id: 'approve' }, { action_id: 'query_reject' } ]
})
// Reminder and timeout logic
WAIT 24h -> IF no response: slack.post_reminder(approver_slack_id)
WAIT 48h -> IF no response: timeout_record -> slack.post_dm(bookkeeper_slack_id) -> HALT
// Handle Slack interaction callback
ON slack.action_callback { action_id, user_id, response_text }:
IF action_id == 'approve':
-> approved_invoice {
invoice_record, coded_lines[], approver_slack_id,
approved_at: ISO8601, approval_status: 'approved'
}
-> PASS to Agent 3
IF action_id == 'query_reject':
-> exception_record { invoice_record, coded_lines[],
approval_status: 'rejected' | 'queried',
approver_note: response_text,
routed_to_bookkeeper: true }
-> slack.post_dm(bookkeeper_slack_id) -> HALT
// ============================================================
// AGENT 3: Payment and Remittance Agent
// ============================================================
// Step 1: Create bill in Xero
xero_bill = POST /api.xro/2.0/Invoices {
Type: 'ACCPAY',
Contact: { Name: supplier_name },
Date: invoice_date,
DueDate: due_date,
LineItems: coded_lines[],
Status: 'AUTHORISED',
Attachments: [ { FileName, MimeType: 'application/pdf',
Content: base64(document_url) } ]
}
-> xero_bill_result { InvoiceID, Status, AmountDue, DueDate }
// Step 2: Urgent check
hours_until_due = (due_date - now()) / 3600
IF hours_until_due < 48:
-> slack.post_dm(bookkeeper_slack_id,
text: 'Urgent: invoice {invoice_number} due in {hours_until_due}h')
// Step 3: Schedule payment
next_payment_run_date = next_occurrence(PAYMENT_RUN_DAY env var)
xero_payment = POST /api.xro/2.0/Payments {
Invoice: { InvoiceID: xero_bill_result.InvoiceID },
Account: { Code: BANK_ACCOUNT_CODE env var },
Date: next_payment_run_date,
Amount: xero_bill_result.AmountDue
}
-> xero_payment_result { PaymentID, Date, Amount, Status }
// Step 4: Fetch supplier email from Xero
supplier_email = GET /api.xro/2.0/Contacts
?Name={supplier_name} -> Contact.EmailAddress
IF supplier_email IS EMPTY:
-> slack.post_dm(bookkeeper_slack_id,
text: 'Remittance skipped: no email on file for {supplier_name}')
-> HALT remittance only; payment proceeds
// Step 5: Send remittance via Gmail
gmail.send {
to: supplier_email,
subject: 'Remittance Advice: Invoice {invoice_number}',
template: 'remittance_advice_v1',
vars: { supplier_name, invoice_number, amount_paid: xero_payment_result.Amount,
payment_date: xero_payment_result.Date, payer_name: PAYER_NAME env var }
}
// ============================================================
// END STATE: Bill posted, payment scheduled, remittance sent
// ============================================================
audit_log.write {
invoice_number, supplier_name, total_amount,
xero_bill_id: xero_bill_result.InvoiceID,
xero_payment_id: xero_payment_result.PaymentID,
remittance_sent_to: supplier_email,
completed_at: ISO8601
}04Recommended build stack
More documents for this process
Every document generated for Accounts Payable Management.