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
Refund & Returns Processing
[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the Refund and Returns Processing automation. It covers the current-state step map, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The FullSpec team owns everything described here through to deployment. Your team's responsibilities are limited to supplying confirmed eligibility rules, tool credentials, and approval for edge-case behaviour before build begins. All questions during build should go to support@gofullspec.com.
01Process snapshot
02Agent specifications
Listens for new or updated Zendesk tickets carrying a return or refund label. Extracts the order reference and customer email from the ticket body or custom fields, calls the Shopify Orders API to retrieve full order data (date, line items, fulfilment status, total amount), and evaluates eligibility against the configured rule set: return window in days, excluded product categories, and any partial-refund logic. Produces a structured decision object containing an approved or flagged status, a reason code, the calculated refund amount, and the enriched order payload. This decision object is the sole input consumed by the Refund Execution Agent. The agent does not take any action on Zendesk or Shopify; it is read-only except for writing the decision record to the shared workflow state.
// Input
zendesk.ticket.id : string // Zendesk ticket ID
zendesk.ticket.tags : string[] // must include 'return' or 'refund'
zendesk.ticket.description : string // raw customer message body
zendesk.ticket.custom_fields.order_reference : string // extracted or parsed
zendesk.requester.email : string // customer email address
// Output
decision.status : 'approved' | 'flagged_for_review'
decision.reason_code : string // e.g. 'WITHIN_WINDOW', 'EXCLUDED_CATEGORY', 'PARTIAL_ELIGIBLE'
decision.refund_amount_usd : number // calculated refund in USD
decision.order.id : string // Shopify order ID
decision.order.created_at : ISO8601 // order creation date
decision.order.line_items : object[] // [{id, title, quantity, price}]
decision.order.total_price : string // original order total
decision.order.fulfillment_status : string
decision.zendesk_ticket_id : string // passed forward for downstream steps
decision.customer_email : string- Zendesk plan must be Suite Team or above; the webhook targets feature is required to push ticket events in real time. If the account is on a lower tier, fall back to polling the Zendesk Tickets API (GET /api/v2/tickets.json?status=new) every 2 minutes.
- Shopify API key must have the read_orders scope. Confirm this scope is present on the private app or custom app credential before build. Do not use the storefront API; it does not expose order details.
- The eligibility rule set must be finalised in writing before this agent is configured. Required parameters: return_window_days (integer), excluded_category_ids (string array mapped to Shopify product_type or tag values), partial_refund_rules (object), and high_value_threshold_usd (number).
- Order reference extraction from the ticket body should use a regex pattern matching Shopify's default order name format (#\d{4,6}). Confirm the store's actual order name format before finalising the pattern.
- If no order reference is found in the ticket body or custom fields, the agent must set decision.status to 'flagged_for_review' with reason_code 'NO_ORDER_REFERENCE' and halt. Do not attempt a customer-email-only Shopify search in production without explicit sign-off, as it can return multiple orders.
- Dedupe: check the orchestration layer's execution log for the zendesk_ticket_id before processing. If a run for that ticket ID already completed within the last 60 minutes, skip and log 'DUPLICATE_SKIP'.
- Confirm with the business owner whether Zendesk ticket tags are added manually by agents or automatically by a Zendesk trigger rule. This determines whether the webhook fires reliably or whether a polling fallback is required.
Receives the approved decision object from the Returns Triage Agent and executes all downstream fulfilment actions in sequence: sends a personalised approval email to the customer via Gmail, calls the Shopify Refunds API to process the refund against the correct order and line items, creates a matching credit note in Xero linked to the customer account and original invoice, evaluates whether the refund amount exceeds the configured high-value threshold and posts a structured Slack alert if so, and finally updates the Zendesk ticket with outcome notes, resolution tags, and solved status. Each action is executed sequentially with a success check before proceeding to the next. If any action fails, the agent logs the failure, halts further execution for that ticket, and posts an error alert to the designated Slack error channel without marking the Zendesk ticket as resolved.
// Input (from Returns Triage Agent decision object)
decision.status : 'approved'
decision.refund_amount_usd : number
decision.reason_code : string
decision.order.id : string
decision.order.line_items : object[]
decision.customer_email : string
decision.zendesk_ticket_id : string
// Output
gmail.message_id : string // sent email message ID for audit
shopify.refund.id : string // created refund ID
shopify.refund.status : 'success' | 'error'
xero.credit_note.id : string // CreditNoteID from Xero
xero.credit_note.status : 'AUTHORISED' | 'error'
slack.alert_posted : boolean // true if refund >= high_value_threshold_usd
slack.message_ts : string // Slack message timestamp if posted
zendesk.ticket.status : 'solved'
zendesk.ticket.tags_added : string[] // e.g. ['refund_processed', 'auto_resolved']
// On approval (internal state written to orchestration log)
execution_log.ticket_id : string
execution_log.completed_at : ISO8601
execution_log.actions : string[] // ordered list of completed action names
execution_log.errors : object[] // [{action, error_message, timestamp}]- Gmail must be connected via OAuth 2.0 using the https://www.googleapis.com/auth/gmail.send scope on the service account or delegated user. The sending address must match the support email address customers expect. Confirm this address before build.
- Shopify Refunds API requires the write_orders scope in addition to read_orders. The POST body must include the order ID, refund line items (line_item_id, quantity, restock: true|false), and the shipping refund amount if applicable. Confirm restock behaviour with the business owner before build.
- Xero integration requires a connected app with the accounting.transactions scope (write) and accounting.contacts scope (read). The credit note must reference the Xero ContactID matched by customer email. If no ContactID is found, flag for manual creation and halt Xero step only; continue remaining actions.
- The high-value Slack alert threshold (high_value_threshold_usd) must be confirmed in writing. Until confirmed, use a default of $150. The Slack channel name and channel ID must be provided before build. Post to channel ID, not channel name, to survive channel renames.
- Gmail approval email must use a template stored in the orchestration layer's credential/template store. The template must be confirmed and signed off before go-live. Required merge fields: customer_first_name, order_id, refund_amount_usd, estimated_processing_days.
- Zendesk ticket update must use PUT /api/v2/tickets/{ticket_id}.json with status: 'solved', tags appended (not replaced), and an internal comment recording the refund ID, Xero credit note ID, and execution timestamp.
- If the Xero API returns a rate-limit error (HTTP 429), implement an exponential backoff retry: 5 seconds, 15 seconds, 45 seconds. After three failures, log and alert without blocking the remaining actions (Gmail, Shopify, Slack, Zendesk are not dependent on Xero success).
- Disputed or out-of-policy tickets (decision.status = 'flagged_for_review') must not enter this agent. The orchestration layer routes them directly to a Zendesk ticket update that assigns the ticket to the Support Lead queue and adds the tag 'pending_manual_review'. No email or refund action is taken.
03End-to-end data flow
// ── TRIGGER ──────────────────────────────────────────────────────────────────
// Source: Zendesk webhook event OR polling fallback every 2 minutes
INPUT {
zendesk.ticket.id // e.g. '98234'
zendesk.ticket.tags // must include 'return' or 'refund'
zendesk.ticket.description // raw customer message
zendesk.ticket.custom_fields // includes order_reference if set
zendesk.requester.email // customer email
}
// ── DEDUPE CHECK ─────────────────────────────────────────────────────────────
// Orchestration layer checks execution_log for ticket.id within last 60 minutes
// If found: log 'DUPLICATE_SKIP', halt execution
// If not found: continue
// ── AGENT HANDOFF 1: Returns Triage Agent ────────────────────────────────────
// Step A: Extract order reference from ticket.description using regex #\d{4,6}
// Fallback: ticket.custom_fields.order_reference
// If no reference found: set decision.status = 'flagged_for_review'
// reason_code = 'NO_ORDER_REFERENCE', halt
// Step B: Shopify Orders API lookup
GET /admin/api/2024-01/orders.json?name={order_reference}&status=any
RESPONSE {
order.id // Shopify internal order ID
order.name // e.g. '#10042'
order.created_at // ISO8601 order date
order.financial_status // 'paid' | 'partially_refunded' | etc.
order.fulfillment_status // 'fulfilled' | 'partial' | null
order.total_price // string, e.g. '89.00'
order.line_items[] // [{id, title, quantity, price, product_type}]
order.customer.email // cross-check against zendesk.requester.email
}
// Step C: Eligibility rule evaluation
// rule.return_window_days : integer (e.g. 30)
// rule.excluded_category_ids : string[] (Shopify product_type values)
// rule.partial_refund_rules : object (item-level override map)
// rule.high_value_threshold_usd : number (e.g. 150)
//
// Compute: days_since_order = today - order.created_at (in days)
// If days_since_order > rule.return_window_days -> reason_code = 'OUTSIDE_WINDOW'
// If order.line_items[].product_type in excluded_category_ids -> reason_code = 'EXCLUDED_CATEGORY'
// Else -> reason_code = 'WITHIN_WINDOW', status = 'approved'
// Calculate: decision.refund_amount_usd from eligible line items
DECISION_OBJECT {
decision.status // 'approved' | 'flagged_for_review'
decision.reason_code // string
decision.refund_amount_usd // number
decision.order.id // string
decision.order.created_at // ISO8601
decision.order.line_items // object[]
decision.order.total_price // string
decision.zendesk_ticket_id // string
decision.customer_email // string
}
// ── ROUTING DECISION ─────────────────────────────────────────────────────────
// IF decision.status == 'flagged_for_review':
// PUT /api/v2/tickets/{zendesk_ticket_id}.json
// -> assignee: Support Lead group ID
// -> tags: append ['pending_manual_review']
// -> internal comment: reason_code + order.id
// HALT: no further automated actions
// IF decision.status == 'approved': continue to Refund Execution Agent
// ── AGENT HANDOFF 2: Refund Execution Agent ───────────────────────────────────
// Step D: Send approval email via Gmail
POST https://gmail.googleapis.com/gmail/v1/users/me/messages/send
PAYLOAD {
to: decision.customer_email
subject: 'Your return request has been approved'
body: template.merge({
customer_first_name,
order_id: decision.order.id,
refund_amount_usd: decision.refund_amount_usd,
estimated_processing_days: 3
})
}
RESPONSE { gmail.message_id }
// Step E: Issue refund in Shopify
POST /admin/api/2024-01/orders/{order.id}/refunds.json
PAYLOAD {
refund.note: decision.reason_code,
refund.refund_line_items: [
{ line_item_id, quantity, restock: true }
],
refund.transactions: [{ amount: decision.refund_amount_usd, kind: 'refund' }]
}
RESPONSE { shopify.refund.id, shopify.refund.status }
// Step F: Create credit note in Xero
// Lookup: GET https://api.xero.com/api.xro/2.0/Contacts?where=EmailAddress=="{customer_email}"
// -> xero.contact.ContactID
// If not found: flag 'XERO_CONTACT_NOT_FOUND', skip Xero, continue
POST https://api.xero.com/api.xro/2.0/CreditNotes
PAYLOAD {
Type: 'ACCREC',
Contact: { ContactID: xero.contact.ContactID },
Date: today_ISO8601,
LineItems: [
{ Description: 'Refund - ' + decision.order.id,
Quantity: 1,
UnitAmount: decision.refund_amount_usd,
AccountCode: '200' }
],
Status: 'AUTHORISED'
}
RESPONSE { xero.credit_note.id, xero.credit_note.status }
// Step G: Conditional Slack alert
// IF decision.refund_amount_usd >= rule.high_value_threshold_usd:
POST https://slack.com/api/chat.postMessage
PAYLOAD {
channel: slack.channel_id,
text: 'High-value refund processed',
blocks: [
{ type: 'section', text:
'Order: ' + decision.order.id +
' | Amount: $' + decision.refund_amount_usd +
' | Customer: ' + decision.customer_email
}
]
}
RESPONSE { slack.message_ts }
// Step H: Resolve and tag Zendesk ticket
PUT /api/v2/tickets/{zendesk_ticket_id}.json
PAYLOAD {
ticket.status: 'solved',
ticket.tags: append ['refund_processed', 'auto_resolved'],
ticket.comment: {
body: 'Refund processed automatically. ' +
'Shopify refund ID: ' + shopify.refund.id + '. ' +
'Xero credit note ID: ' + xero.credit_note.id + '. ' +
'Executed at: ' + ISO8601_timestamp,
public: false
}
}
// ── EXECUTION LOG WRITTEN ─────────────────────────────────────────────────────
execution_log {
ticket_id, completed_at, actions[], errors[]
}04Recommended build stack
More documents for this process
Every document generated for Refund & Returns Processing.