Back to Expense Management

Integration and API Spec

The reference during the build: every tool connection, auth scope, field mapping, and error-handling rule.

4 pagesPDF · Technical
FS-DOC-05Technical

Integration and API Spec

Expense Management Automation

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

This document provides the complete integration and API reference for the Expense Management automation. It covers every tool in the stack, the exact authentication scopes and configuration required for each, field-level mappings between agent handoffs, orchestration layout, credential store contents, and defined error handling behaviour for every integration point. This document is intended for the FullSpec build team. [YourCompany.com] is responsible for provisioning API credentials and confirming that plan tiers for each tool meet the minimums stated below.

01Tool inventory

Tool
Role in automation
Auth method
Min plan required
Used by agents
Expensify
Receipt capture, claim submission trigger, claim status updates
API key + partner user token (Basic Auth over HTTPS)
Collect plan ($18/month, 1 submitter minimum)
Agent 1
Xero
GL code assignment, draft spend money record creation, finalisation on approval
OAuth 2.0 (authorization code flow)
Starter plan or above (API access included on all paid plans)
Agents 2, 3
Slack
Manager approval routing, interactive approve/reject actions, 24-hour reminders
OAuth 2.0 (Bot Token via Slack App)
Pro plan or above for workflow steps; Free plan sufficient for basic messaging
Agent 3
Gmail
Receipt chase emails to employees, confirmation emails on approval or rejection
OAuth 2.0 (authorization code flow, Google Workspace account)
Google Workspace Business Starter or above
Agents 1, 3
Google Drive
Receipt file storage, temporary holding of receipt attachments before Xero sync
OAuth 2.0 (authorization code flow, same Google Workspace credential as Gmail)
Google Workspace Business Starter (shared credential with Gmail)
Agent 1
Gusto
Reimbursement queue entry per approved claim in the active payroll run
OAuth 2.0 (authorization code flow, Gusto admin credential)
Gusto Core plan or above (API access requires admin role)
Agent 3
Automation platform (orchestration layer)
Workflow orchestration, agent sequencing, credential store, retry and error handling
Internal service account; external tool connections use stored OAuth tokens and API keys
Plan with webhook support and multi-step workflows (see Section 04)
All agents
Before you connect anything: every integration must be tested against a sandbox or development environment before production credentials are entered into the credential store. Expensify provides a sandbox via the partner portal. Xero provides a demo company. Gusto provides a sandbox environment accessible through the Gusto developer portal. Slack and Gmail should be tested against a dedicated test workspace and a test Google Workspace account respectively. Do not use live payroll or accounting credentials during the build or QA phases.
Integration and API SpecPage 1 of 4
FS-DOC-05Technical

02Per-tool integration detail

Expensify

Expensify is the entry point for the automation. Every new expense claim submitted by an employee triggers Agent 1. The integration uses the Expensify Integration Server API (JSON API). The automation platform polls the Expensify report list endpoint on a defined interval to detect newly submitted reports, then retrieves the full report payload including receipt attachments.

Auth method
HTTP Basic Auth using partnerUserID (API key) and partnerUserSecret (partner user token). Both values stored in the credential store as EXPENSIFY_PARTNER_ID and EXPENSIFY_PARTNER_SECRET. Never hardcode.
Required scopes
Expensify does not use OAuth scopes. Access is controlled by the partner user token. The partner account must be added as a domain admin or copilot on the [YourCompany.com] Expensify policy to read and update reports.
Trigger setup
Poll mode: the orchestration layer calls POST https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations with type=report and filters on reportState=SUBMITTED and startDate set to the last successful poll timestamp. Poll interval: 5 minutes. No native webhook available on the Collect plan; polling is the required approach.
Required configuration
Policy ID for [YourCompany.com] Expensify policy stored as EXPENSIFY_POLICY_ID in the credential store. Report fields to extract: reportID, reportName, total, currency, submitterEmail, receiptFilenames, categories, submittedDate. Receipt image URLs fetched via the downloadFile endpoint using the same credential pair.
Rate limits
Expensify Integration Server enforces no published hard rate limit but recommends fewer than 10 requests per minute per partner account. At 120 claims per month (~4 per day, typically batched), a 5-minute poll interval generates well under 300 requests per day. Throttling is not required at current volume.
Constraints
Receipts are only accessible via authenticated download URL for 24 hours after report submission. The automation must download and store the receipt to Google Drive within that window. Receipt filenames may include spaces and special characters; the download step must URL-encode filenames before requesting.
// Trigger payload (selected fields)
reportID: string
reportName: string
total: float (USD cents)
currency: string (ISO 4217)
submitterEmail: string
receiptFilenames: string[]
categories: string[]
submittedDate: string (ISO 8601)
// Output to Agent 1
Structured claim record with all fields above plus downloaded receipt file path
Gmail (Google Workspace)

Gmail is used in two places: Agent 1 sends receipt chase emails when a receipt is missing or unreadable, and Agent 3 sends the employee confirmation email on approval or rejection. Both uses share the same OAuth 2.0 credential. The integration targets the Gmail API v1.

Auth method
OAuth 2.0 authorization code flow. Refresh token stored as GMAIL_REFRESH_TOKEN. Access tokens are short-lived (1 hour) and must be refreshed automatically by the orchestration layer. Client ID and secret stored as GMAIL_CLIENT_ID and GMAIL_CLIENT_SECRET.
Required scopes
https://www.googleapis.com/auth/gmail.send (send-only; do not request broader mail read access for this integration)
Webhook / trigger setup
Gmail is outbound-only in this automation. No inbound trigger or watch subscription is required. Agent 1 calls the users.messages.send endpoint when a receipt chase is needed. Agent 3 calls the same endpoint for confirmation emails.
Required configuration
GMAIL_SENDER_ADDRESS stored in credential store (the From address used for all automated emails). Two email templates stored as named configuration entries: TEMPLATE_RECEIPT_CHASE and TEMPLATE_CLAIM_CONFIRMATION. Templates use {{employee_name}}, {{claim_amount}}, {{claim_date}}, {{rejection_reason}} as placeholders. Do not hardcode sender address or template content.
Rate limits
Gmail API quota: 250 quota units per second per user; users.messages.send costs 100 units. At current volume (120 claims/month) the maximum send volume is well under 1 request per minute on average. No throttling required.
Constraints
The OAuth consent screen must be published (not in test mode) for the refresh token to remain valid beyond 7 days. Ensure the Google Workspace admin has verified the application or set it to internal use only within the organisation.
// Input to send endpoint (receipt chase)
to: submitterEmail
subject: 'Action required: missing receipt for your expense claim'
body: rendered TEMPLATE_RECEIPT_CHASE with {{employee_name}}, {{claim_amount}}, {{claim_date}}
// Input to send endpoint (confirmation)
to: submitterEmail
subject: 'Your expense claim has been approved'
body: rendered TEMPLATE_CLAIM_CONFIRMATION with {{employee_name}}, {{claim_amount}}, {{payroll_date}}
Google Drive

Google Drive is used by Agent 1 to store downloaded receipt files before they are attached to the Xero spend money record. Files are written to a dedicated automation folder and named with a canonical format to prevent collisions. The Drive API v3 is used.

Auth method
OAuth 2.0 authorization code flow, same Google Workspace credential as Gmail. Shared refresh token GMAIL_REFRESH_TOKEN covers both scopes. Client ID and secret are the same as Gmail (GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET).
Required scopes
https://www.googleapis.com/auth/drive.file (create and access only files created by this app; do not request drive or drive.readonly which grant access to all files)
Webhook / trigger setup
Google Drive is outbound-write-only in this automation. No watch or push notification is configured.
Required configuration
GDRIVE_RECEIPTS_FOLDER_ID stored in credential store. This is the ID of the folder named 'Expense Receipts (Automation)' created during setup. Files are named using the format: {submittedDate}_{reportID}_{submitterEmail}.{ext}. Do not hardcode the folder ID.
Rate limits
Drive API: 12,000 requests per minute per project (shared across all users). At 120 receipts per month the load is negligible. No throttling required.
Constraints
Files uploaded to Drive must be under 5 MB for standard receipts. If a receipt exceeds this (e.g. a multi-page PDF), the automation must split or compress before upload and log a warning. Uploaded files are not publicly shared; Xero attachment upload uses a file stream, not a public URL.
// Input
receipt_binary: file stream (JPEG, PNG, or PDF)
file_name: string (canonical format as above)
folder_id: GDRIVE_RECEIPTS_FOLDER_ID
// Output
drive_file_id: string
drive_file_name: string
Xero

Xero is used by Agent 2 to create a draft spend money record with the correct GL code and attached receipt, and by Agent 3 to finalise (approve) that record on manager approval, or void it on rejection. The Xero Accounting API v2 is used.

Auth method
OAuth 2.0 authorization code flow with PKCE. Refresh token stored as XERO_REFRESH_TOKEN. Access tokens expire after 30 minutes and must be refreshed. Client ID and secret stored as XERO_CLIENT_ID and XERO_CLIENT_SECRET. Tenant (organisation) ID stored as XERO_TENANT_ID.
Required scopes
accounting.transactions (create and update bank transactions and spend money records), accounting.attachments (attach receipt files to transactions), accounting.settings.read (read chart of accounts and GL codes), offline_access (required to obtain a refresh token)
Webhook / trigger setup
Agent 3 listens for the draft record creation event from Agent 2 via the internal orchestration event bus, not via a Xero webhook. No Xero webhook subscription is required.
Required configuration
XERO_BANK_ACCOUNT_ID: the bank account ID used for spend money transactions (the company's expense clearing account). GL_CODE_MAPPING: a configuration table (JSON object) stored as XERO_GL_MAP in the credential store, mapping Expensify merchant categories to Xero account codes. Example entry: { 'Travel': '400', 'Meals': '420', 'Office Supplies': '411' }. This mapping must be curated before go-live. Do not hardcode account codes.
Rate limits
Xero API: 60 calls per minute per connection, 5,000 calls per day. At 120 claims per month (peak ~10 per day) the automation generates approximately 40 to 60 API calls per day (create, attach, update). Well within limits. No throttling required.
Constraints
Spend money records created via API are set to DRAFT status. Only the Xero user associated with the OAuth token can approve them via API (status update to AUTHORISED). Ensure the OAuth token is issued by a Xero user with the Adviser or Standard role. The Xero Starter plan allows a maximum of 20 bank transactions per month; the Standard plan or above is required if claim volume exceeds this threshold in any calendar month.
// Input to create spend money (Agent 2)
type: 'SPEND'
status: 'DRAFT'
date: claim_date (ISO 8601)
amount: claim_amount (float)
reference: reportID
lineItems[0].accountCode: resolved GL code from XERO_GL_MAP
lineItems[0].description: merchant_name + ' - ' + category
lineItems[0].unitAmount: claim_amount
bankAccount.accountID: XERO_BANK_ACCOUNT_ID
attachment: drive_file_id (uploaded as multipart/form-data)
// Output from create
bankTransactionID: string
status: 'DRAFT'
// Input to finalise (Agent 3, on approval)
bankTransactionID: string
status: 'AUTHORISED'
// Input to void (Agent 3, on rejection)
bankTransactionID: string
status: 'VOIDED'
Slack

Slack is used exclusively by Agent 3 to send the approval request message to the relevant department manager, render the approve and reject interactive buttons, and fire a 24-hour reminder if no action is taken. The Slack Web API and Block Kit are used. A dedicated Slack App must be created in the [YourCompany.com] Slack workspace.

Auth method
OAuth 2.0 Bot Token. SLACK_BOT_TOKEN stored in the credential store. The bot token is issued when the Slack App is installed into the workspace. Do not use a legacy API token or personal user token.
Required scopes
chat:write (post messages to channels and DMs), im:write (open direct message channels with users), users:read (resolve manager Slack user ID from email address), users:read.email (required to look up user by email)
Webhook / trigger setup
Interactive components (approve/reject button clicks) require the Slack App to have an Interactivity request URL configured. This URL is an endpoint exposed by the orchestration platform that receives the action payload and resumes the paused workflow. The endpoint must be HTTPS with a valid certificate. Slack signs all interaction payloads with a signing secret; the orchestration layer must validate the X-Slack-Signature header using HMAC-SHA256 and SLACK_SIGNING_SECRET before processing any action. SLACK_SIGNING_SECRET stored in the credential store.
Required configuration
SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in the credential store. MANAGER_ROUTING_MAP: a configuration object stored as SLACK_MANAGER_MAP mapping submitterEmail domain or department to the manager's Slack user ID or email. Example: { 'finance': 'U012AB3CD', 'operations': 'U045EF6GH' }. This map must be populated before go-live. The approval message uses a Block Kit template with the claim amount, merchant, date, GL code, and receipt thumbnail URL as dynamic values.
Rate limits
Slack Web API: Tier 3 methods (chat.postMessage) allow 50 requests per minute. At 120 claims per month the load is negligible. No throttling required.
Constraints
Interactive button payloads expire after 30 minutes by default. The orchestration layer must store the workflow execution context keyed on the action_id and resume it when the callback arrives, which may come minutes or hours later. The workflow must remain in a waiting state (not time out) for up to 48 hours to accommodate the 24-hour reminder cycle plus manager response time.
// Input to chat.postMessage (approval request)
channel: resolved manager Slack user ID
blocks: Block Kit array with claim_amount, merchant_name, claim_date, gl_code, receipt_thumbnail_url
attachments: approve button (action_id: 'approve_claim'), reject button (action_id: 'reject_claim')
metadata: { report_id, bank_transaction_id, submitter_email }
// Incoming interaction payload (approve or reject)
action_id: 'approve_claim' | 'reject_claim'
metadata: { report_id, bank_transaction_id, submitter_email }
user.id: manager Slack user ID
// Output to orchestration layer
approval_decision: 'approved' | 'rejected'
bank_transaction_id: string
report_id: string
submitter_email: string
Gusto

Gusto is used by Agent 3 to add the approved reimbursement amount as an expense line on the employee's next payroll run. The Gusto API v1 is used. Admin credentials are required.

Auth method
OAuth 2.0 authorization code flow. Refresh token stored as GUSTO_REFRESH_TOKEN. Access tokens expire after 2 hours. Client ID and secret stored as GUSTO_CLIENT_ID and GUSTO_CLIENT_SECRET. Company ID stored as GUSTO_COMPANY_ID.
Required scopes
employees:read (resolve employee ID from submitter email), payrolls:read (identify the next open payroll run ID), payrolls:write (add reimbursement line to the payroll run)
Webhook / trigger setup
Gusto is outbound-write-only in this automation. No webhook subscription is required.
Required configuration
GUSTO_COMPANY_ID in the credential store. The reimbursement type is 'Expense Reimbursement' (Gusto built-in type; no custom earning type required). The employee is looked up by email via GET /v1/companies/{company_id}/employees?email={submitter_email}. The next open payroll run is retrieved via GET /v1/companies/{company_id}/payrolls?processing_statuses=unprocessed and selecting the earliest start_date entry. FullSpec recommends a manual review of the Gusto reimbursement batch for the first two to three pay runs before finalisation.
Rate limits
Gusto API: 100 requests per minute per access token. At current volume the automation makes 3 to 4 API calls per approved claim (lookup employee, lookup payroll, add reimbursement). Peak estimated at under 50 calls per day. No throttling required.
Constraints
Reimbursements can only be added to payroll runs with processing_status of 'unprocessed'. If the payroll run has already been submitted for processing when an approval arrives, the orchestration layer must flag the claim for manual addition and alert finance via Gmail. Gusto sandbox is available at https://api.gusto-demo.com and uses separate OAuth credentials; do not use production credentials during build or QA.
// Input to add reimbursement
POST /v1/companies/{GUSTO_COMPANY_ID}/payrolls/{payroll_id}/employees/{employee_id}/reimbursements
amount: claim_amount (string, two decimal places)
description: 'Expense reimbursement: ' + merchant_name + ' ' + claim_date
// Output
reimbursement_id: string
payroll_id: string
employee_id: string
status: 'pending'
Integration and API SpecPage 2 of 4
FS-DOC-05Technical

03Field mappings between tools

The following tables define the exact field mappings at each agent handoff. Source and destination field names are given in monospace format exactly as they appear in the API response or request body. Where a value is derived or transformed, the transformation is noted in the destination field column.

Agent 1 handoff: Expensify to Agent 1 (Receipt Extraction and Policy Agent) internal claim record

Source tool
Source field
Destination tool
Destination field
Expensify
`reportID`
Agent 1 claim record
`claim_id`
Expensify
`submitterEmail`
Agent 1 claim record
`submitter_email`
Expensify
`total` (integer, cents)
Agent 1 claim record
`claim_amount` (float, divide by 100)
Expensify
`currency`
Agent 1 claim record
`claim_currency`
Expensify
`submittedDate`
Agent 1 claim record
`claim_date` (ISO 8601)
Expensify
`reportName`
Agent 1 claim record
`claim_description`
Expensify
`categories[0]`
Agent 1 claim record
`expense_category`
Expensify
`receiptFilenames[0]`
Agent 1 claim record
`receipt_filename`
Google Drive
`id` (uploaded file)
Agent 1 claim record
`drive_file_id`
Agent 1 (derived)
OCR extracted merchant name
Agent 1 claim record
`merchant_name`
Agent 1 (derived)
Policy check result
Agent 1 claim record
`policy_verdict` ('pass'|'fail')
Agent 1 (derived)
Policy failure reason string
Agent 1 claim record
`policy_flag_reason` (null if pass)

Agent 2 handoff: Agent 1 claim record to Xero (GL Coding and Xero Sync Agent)

Source tool
Source field
Destination tool
Destination field
Agent 1 claim record
`claim_date`
Xero spend money
`date`
Agent 1 claim record
`claim_amount`
Xero spend money
`lineItems[0].unitAmount`
Agent 1 claim record
`claim_currency`
Xero spend money
`currencyCode`
Agent 1 claim record
`claim_id`
Xero spend money
`reference`
Agent 1 claim record
`merchant_name` + `expense_category`
Xero spend money
`lineItems[0].description` (concatenated with ' - ')
XERO_GL_MAP lookup
`expense_category` -> account code
Xero spend money
`lineItems[0].accountCode`
Credential store
`XERO_BANK_ACCOUNT_ID`
Xero spend money
`bankAccount.accountID`
Agent 1 claim record
`drive_file_id`
Xero attachment
multipart file upload keyed to `bankTransactionID`
Xero API response
`bankTransactionID`
Agent 2 output record
`xero_transaction_id`

Agent 3 handoff: Agent 2 output to Slack, Xero finalisation, Gusto, and Gmail (Approval Routing and Notification Agent)

Source tool
Source field
Destination tool
Destination field
Agent 2 output
`claim_id`
Slack Block Kit message
`metadata.report_id`
Agent 2 output
`xero_transaction_id`
Slack Block Kit message
`metadata.bank_transaction_id`
Agent 2 output
`submitter_email`
Slack Block Kit message
`metadata.submitter_email`
Agent 2 output
`claim_amount`
Slack Block Kit message
block text: 'Amount: ${{claim_amount}}'
Agent 2 output
`merchant_name`
Slack Block Kit message
block text: 'Merchant: {{merchant_name}}'
Agent 2 output
`claim_date`
Slack Block Kit message
block text: 'Date: {{claim_date}}'
Agent 2 output
`lineItems[0].accountCode`
Slack Block Kit message
block text: 'GL Code: {{account_code}}'
Slack interaction payload
`action_id`
Xero update
`status` ('AUTHORISED' if approve, 'VOIDED' if reject)
Slack interaction payload
`metadata.bank_transaction_id`
Xero update
`bankTransactionID` (URL path param)
Agent 2 output
`claim_amount`
Gusto reimbursement
`amount`
Agent 2 output
`merchant_name` + `claim_date`
Gusto reimbursement
`description`
Agent 2 output
`submitter_email`
Gusto employee lookup
GET employees?email={submitter_email}
Agent 2 output
`submitter_email`
Gmail confirmation
`to`
Agent 2 output
`claim_amount`
Gmail confirmation
template placeholder `{{claim_amount}}`
Gusto API response
next open payroll `period_end`
Gmail confirmation
template placeholder `{{payroll_date}}`
Integration and API SpecPage 3 of 4
FS-DOC-05Technical

04Build stack and orchestration

Orchestration layout
One workflow per agent: Workflow 1 (Receipt Extraction and Policy Agent), Workflow 2 (GL Coding and Xero Sync Agent), Workflow 3 (Approval Routing and Notification Agent). Workflows are chained via the internal event bus: Workflow 1 emits a 'claim.policy.passed' event consumed by Workflow 2; Workflow 2 emits a 'draft.record.created' event consumed by Workflow 3. A single shared credential store holds all API keys, OAuth tokens, and configuration maps. No credentials are stored in workflow logic or environment variables.
Agent 1 trigger mechanism
Poll. The orchestration platform executes a scheduled poll every 5 minutes against the Expensify Integration Server API. On each poll, it requests reports with status SUBMITTED and submittedDate greater than the last successful poll timestamp (stored as a persistent workflow variable EXPENSIFY_LAST_POLL_TS). Duplicate detection uses reportID as a unique key stored in a processed-IDs register to prevent reprocessing on retry.
Agent 2 trigger mechanism
Internal event. Workflow 2 is triggered by the 'claim.policy.passed' event emitted by Workflow 1. The event payload carries the full claim record including drive_file_id. No external webhook is involved.
Agent 3 trigger mechanism
Internal event plus external webhook. Workflow 3 is triggered by the 'draft.record.created' event from Workflow 2. The workflow sends the Slack approval message and then pauses, waiting for an inbound HTTP POST from the Slack interactivity endpoint. The endpoint URL is registered in the Slack App configuration under Interactivity. The orchestration platform validates the X-Slack-Signature header using HMAC-SHA256 with SLACK_SIGNING_SECRET before resuming the workflow. If no Slack interaction is received within 24 hours, the workflow fires a reminder message and continues to wait. Maximum wait is 48 hours before escalation.
Slack signature validation
On receipt of a Slack interaction POST: compute HMAC-SHA256 of 'v0:' + X-Slack-Request-Timestamp + ':' + raw request body using SLACK_SIGNING_SECRET as the key. Compare to the v0= prefix stripped value of X-Slack-Signature using a constant-time comparison. Reject and log any request where the computed signature does not match, or where X-Slack-Request-Timestamp is more than 5 minutes old (replay protection).
Credential store type
The automation platform's native encrypted credential or secrets store. All values referenced by key name only in workflow logic. Rotation of OAuth refresh tokens is handled automatically by the orchestration layer on each token refresh cycle. Manual rotation is required for API keys (Expensify) on a 90-day schedule.
All values are injected at runtime from the credential store. No value is hardcoded in workflow logic.
// Credential store contents (key name : type : description)

EXPENSIFY_PARTNER_ID          : string   : Expensify partnerUserID (API key)
EXPENSIFY_PARTNER_SECRET      : string   : Expensify partnerUserSecret (partner user token)
EXPENSIFY_POLICY_ID           : string   : [YourCompany.com] Expensify policy ID

XERO_CLIENT_ID                : string   : Xero OAuth 2.0 application client ID
XERO_CLIENT_SECRET            : string   : Xero OAuth 2.0 application client secret
XERO_REFRESH_TOKEN            : string   : Xero OAuth 2.0 refresh token (auto-rotated)
XERO_TENANT_ID                : string   : Xero organisation (tenant) ID
XERO_BANK_ACCOUNT_ID          : string   : Xero bank account ID for spend money transactions
XERO_GL_MAP                   : JSON obj : Merchant category to Xero account code mapping table

GMAIL_CLIENT_ID               : string   : Google OAuth 2.0 client ID (shared with Drive)
GMAIL_CLIENT_SECRET           : string   : Google OAuth 2.0 client secret (shared with Drive)
GMAIL_REFRESH_TOKEN           : string   : Google OAuth 2.0 refresh token (auto-rotated)
GMAIL_SENDER_ADDRESS          : string   : From address for all automated emails
TEMPLATE_RECEIPT_CHASE        : string   : HTML email template for receipt chase
TEMPLATE_CLAIM_CONFIRMATION   : string   : HTML email template for approval confirmation
TEMPLATE_CLAIM_REJECTION      : string   : HTML email template for rejection notification

GDRIVE_RECEIPTS_FOLDER_ID     : string   : Google Drive folder ID for receipt storage

SLACK_BOT_TOKEN               : string   : Slack Bot Token (xoxb-...)
SLACK_SIGNING_SECRET          : string   : Slack App signing secret for payload validation
SLACK_MANAGER_MAP             : JSON obj : Department / email to manager Slack user ID mapping

GUSTO_CLIENT_ID               : string   : Gusto OAuth 2.0 client ID
GUSTO_CLIENT_SECRET           : string   : Gusto OAuth 2.0 client secret
GUSTO_REFRESH_TOKEN           : string   : Gusto OAuth 2.0 refresh token (auto-rotated)
GUSTO_COMPANY_ID              : string   : Gusto company ID for [YourCompany.com]

EXPENSIFY_LAST_POLL_TS        : string   : ISO 8601 timestamp of last successful Expensify poll
PROCESSED_REPORT_IDS          : string[] : Register of Expensify reportIDs already processed

05Error handling and retry logic

Every integration point has a defined failure behaviour. Unhandled exceptions must never fail silently. If an error cannot be resolved by the retry logic below, the orchestration layer must log the full error payload, halt the affected workflow branch, and send an alert to the finance admin via Gmail using GMAIL_SENDER_ADDRESS.

Integration
Scenario
Required behaviour
Expensify (poll)
Poll returns HTTP 5xx or network timeout
Retry 3 times with exponential backoff: 30 s, 2 min, 5 min. If all retries fail, skip this poll cycle, log the error, and resume on the next scheduled poll interval. Do not advance EXPENSIFY_LAST_POLL_TS until a successful poll completes.
Expensify (receipt download)
Receipt download URL returns 403 or 404 (URL expired)
Immediately trigger the Gmail receipt chase flow (same path as a missing receipt). Log the original download URL and the reportID. Mark the claim as 'receipt_expired' in the processed-IDs register to prevent re-triggering the chase on the next poll.
Google Drive (receipt upload)
Upload fails due to quota exceeded or network error
Retry 3 times with backoff: 1 min, 3 min, 10 min. If all retries fail, halt Workflow 1 for this claim and alert the finance admin with the reportID and error detail. Do not proceed to Xero without a valid drive_file_id.
Xero (create spend money)
API returns 429 Too Many Requests
Honour the Retry-After header if present; otherwise wait 60 seconds before retrying. Retry up to 5 times. If rate limit persists beyond 5 retries, halt and alert finance admin. Log the reportID and claim_amount for manual creation.
Xero (create spend money)
API returns 400 validation error (e.g. unrecognised account code)
Do not retry. Log the full Xero error response and the resolved GL code. Alert the finance admin with the reportID, the attempted account code, and a link to update XERO_GL_MAP. Halt this claim branch; it requires manual intervention.
Xero (attach receipt)
Attachment upload fails (file too large or format rejected)
Retry once after 30 seconds. If the retry fails, create the spend money record without the attachment, flag the transaction with the note 'ATTACHMENT_FAILED: manual upload required', and alert the finance admin with the drive_file_id.
Xero (finalise on approval)
bankTransactionID not found or record is no longer in DRAFT status
Do not retry status updates. Log the full Xero response and the bankTransactionID. Alert the finance admin immediately. The claim is considered in an indeterminate state and requires manual resolution in Xero before Gusto entry proceeds.
Slack (send approval message)
chat.postMessage returns error (e.g. channel_not_found, user_not_found)
Retry once after 60 seconds with a fresh user ID lookup via users.lookupByEmail. If the second attempt fails, fall back to sending the approval request via Gmail to the manager's email address (derived from SLACK_MANAGER_MAP). Log the Slack error and the fallback action taken.
Slack (interaction webhook)
No approve or reject action received within 24 hours
Send a reminder Slack message to the manager (same Block Kit layout, new message in the same DM thread). Reset the 24-hour timer. If no action is received within a further 24 hours (48 hours total), escalate by sending a Gmail alert to the finance admin and the manager. Do not auto-approve or auto-reject.
Slack (signature validation)
Inbound interaction payload fails HMAC-SHA256 signature check or timestamp is more than 5 minutes old
Reject the request immediately with HTTP 400. Log the source IP, the provided signature, and the timestamp. Do not resume the workflow. Alert the FullSpec team via support@gofullspec.com if more than 3 validation failures occur within 10 minutes (potential replay attack).
Gusto (employee lookup)
No employee found for submitter_email
Do not proceed with reimbursement. Halt the Gusto step and alert the finance admin with the submitter_email and the claim_id. Log as 'GUSTO_EMPLOYEE_NOT_FOUND'. The Xero record may still be finalised; the reimbursement requires manual addition to Gusto.
Gusto (add reimbursement)
Payroll run is already in 'processed' or 'paid' status
Do not attempt to add the reimbursement line. Log the payroll_id and status. Alert the finance admin with the claim_id and amount for manual addition to the next payroll run. Send the employee confirmation email with a note that payment will appear in the following payroll cycle.
Gmail (send any email)
API returns 429 or send quota exceeded
Retry after 60 seconds, then 5 minutes. If both retries fail, log the intended recipient, subject, and template name. Alert the finance admin via the platform's internal notification mechanism. The claim workflow continues; the missing notification is logged for manual follow-up.
Global rule: any unhandled exception not covered by the table above must be caught by a top-level error handler in each workflow. The handler must log the full exception payload (timestamp, workflow ID, step name, error message, input payload), increment a run-failure counter, and send an alert to support@gofullspec.com. The claim must never be silently dropped. All failed claim IDs must be recoverable from the run log for manual reprocessing.
Integration and API SpecPage 4 of 4

More documents for this process

Every document generated for Expense Management.

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