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
Order Processing & Fulfilment
[YourCompany.com] · Fulfilment Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Order Processing and Fulfilment automation. It covers the current-state process map with bottleneck flags, the full specification for each of the three agents, the end-to-end data flow across all tool integrations, and the recommended build stack. The FullSpec team owns every build, integration, and test task described here. The process owner's responsibilities are limited to supplying API credentials, confirming courier service-level mapping, and approving wholesale invoice logic before the Finance Agent goes live.
01Process snapshot
02Agent specifications
Monitors Shopify for new paid orders via webhook, retrieves the full order payload including all line items and customer details, then queries Linnworks in real time to verify stock availability for every SKU. If all items are in stock the agent passes a verified order record downstream to the Dispatch Agent. If any item is unavailable the agent sends a structured Slack alert to the fulfilment channel containing the order ID, the affected SKUs, quantities requested, and a direct Shopify order URL so the team can act immediately. This agent is the entry point for every order in the automated flow and must be atomic in its stock check to prevent race conditions under concurrent load. Estimated build time: 10 hours. Complexity: Moderate.
// Input
shopify.order.id // string, e.g. '5678901234'
shopify.order.line_items[] // array: { sku, quantity, variant_id, title }
shopify.order.shipping_address // { name, address1, city, province, zip, country }
shopify.order.shipping_lines[0].title // customer-selected service level
shopify.order.tags // string, e.g. 'wholesale, B2B'
shopify.order.total_price // string
shopify.order.email // customer email address
// Output (all in stock)
verified_order.order_id // passed to Dispatch Agent
verified_order.line_items[] // { sku, quantity, linnworks_stock_id }
verified_order.shipping_address
verified_order.service_level // mapped courier service label
verified_order.tags
verified_order.customer_email
verified_order.status // 'ready_for_dispatch'
// Output (stock failure)
slack.alert.channel // '#fulfilment-alerts'
slack.alert.order_id
slack.alert.oos_skus[] // { sku, title, requested_qty, available_qty }
slack.alert.shopify_order_url
verified_order.status // 'on_hold_oos'- Shopify plan tier must support real-time order webhooks (Basic and above). If the store is on a legacy plan that does not surface the order/paid event reliably, build a five-minute polling fallback using the REST Orders API filtered by financial_status=paid and created_at_min set to the last successful poll timestamp.
- Linnworks stock check must call GET /api/Stock/GetStockItemsFull with the exact SKU values from the Shopify line_items array. Confirm that Shopify SKUs match Linnworks SKU references one-to-one before build; if they differ, a mapping table must be created and stored in the automation platform's credential/data store.
- The stock check for multiple line items must be treated as a single atomic operation. If two orders for the same low-stock SKU arrive within the webhook processing window, a database-level lock or a short-lived mutex flag in the automation platform's key-value store must prevent both from passing validation simultaneously.
- Slack alert must post to the exact channel name '#fulfilment-alerts' (confirm with the process owner before build). The Slack bot token requires the chat:write OAuth scope and must be installed to the workspace with permission to post in that channel.
- Dedupe: if a Shopify webhook is delivered more than once for the same order ID (Shopify guarantees at-least-once delivery), the agent must check a processed-order log before re-executing. Log the order ID on first successful processing and short-circuit on duplicate receipt.
- The out-of-stock email to the customer (original step 3) is replaced by the Slack alert to the fulfilment team only. A separate customer-facing notification for out-of-stock orders is out of scope for this build unless explicitly added to scope by the process owner.
Receives the verified order record from the Order Intake Agent and executes the full dispatch sequence without manual input. It creates the shipment in ShipStation using the mapped courier and service level, retrieves the generated tracking number, writes the fulfilment record and tracking URL back to Shopify, sends the customer a personalised confirmation email via Gmail, and decrements the dispatched quantities in Linnworks immediately after dispatch confirmation. This agent replaces the highest-volume manual steps in the process and must handle the courier service-level mapping table as a configurable input rather than hard-coded logic. Estimated build time: 18 hours. Complexity: Complex.
// Input
verified_order.order_id
verified_order.line_items[] // { sku, quantity, linnworks_stock_id }
verified_order.shipping_address // { name, address1, city, province, zip, country_code }
verified_order.service_level // e.g. 'Standard', 'Express', 'Overnight'
verified_order.customer_email
verified_order.tags
// ShipStation intermediate
shipstation.shipment_id // returned on POST /shipments/createlabel
shipstation.tracking_number // string
shipstation.carrier_code // e.g. 'ups', 'fedex', 'usps'
shipstation.label_url // PDF label URL for warehouse printer
// Output
shopify.fulfillment.tracking_number // written via POST /fulfillments.json
shopify.fulfillment.tracking_url // courier tracking URL
shopify.fulfillment.status // 'success'
gmail.message.to // verified_order.customer_email
gmail.message.subject // 'Your order is on its way'
gmail.message.body // includes carrier_code, tracking_number, tracking_url
linnworks.stock_deduction[] // { linnworks_stock_id, quantity_deducted }- ShipStation courier service-level mapping must be fully documented before build starts. The mapping table must link each Shopify shipping_lines[0].title value (e.g. 'Standard Shipping', 'Express Delivery') to the corresponding ShipStation carrier_code and service_code pair. This table must be confirmed by the process owner and stored as a configurable data structure in the automation platform, not hard-coded.
- ShipStation API authentication uses HTTP Basic Auth with the API key and API secret encoded as base64. The store must be on a ShipStation plan that includes API access (Bronze and above). The POST /shipments/createlabel endpoint must be used, not the deprecated /orders endpoint, to receive a tracking number synchronously.
- Shopify fulfilment write-back uses POST /admin/api/2024-01/orders/{order_id}/fulfillments.json. The request body must include the tracking_number, tracking_company, and tracking_url fields. Confirm that the Shopify store does not have third-party fulfilment services configured that would intercept or reject this API call.
- Gmail sending uses OAuth 2.0 with the gmail.send scope. The sending account must be a Google Workspace account belonging to [YourCompany.com]. A draft email template must be approved by the process owner before build. The template must include the courier name, tracking number, and a hyperlinked tracking URL. Do not use a personal Gmail account.
- Linnworks stock deduction calls POST /api/Orders/SetOrderItemsQuantity or the equivalent stock adjustment endpoint. Deduction must occur only after Shopify fulfilment write-back returns HTTP 201. If the Shopify write-back fails, the deduction must not proceed and the error must be logged with the order ID.
- Fallback behaviour: if ShipStation returns an error on shipment creation (e.g. unrecognised service code, address validation failure), the agent must post a Slack alert to '#fulfilment-alerts' with the order ID and the exact ShipStation error message, then halt without deducting stock or marking Shopify fulfilled.
- Rate limits: ShipStation enforces 40 API calls per minute per account. At peak volume (55 orders per week, roughly 8 to 10 per day in a single processing window), the agent is well within this limit. Build in a retry with exponential backoff (1 s, 2 s, 4 s) for any 429 response.
Runs in parallel with the Dispatch Agent for any order carrying a wholesale or B2B tag in Shopify. It reads the order line items and total from the Shopify order payload, creates a draft invoice in Xero with all line items, amounts, tax codes, and a reference to the Shopify order ID, and leaves the invoice in draft state for the Operations Manager to review and approve before sending. This agent does not send invoices autonomously. The draft-and-approve model is a deliberate design constraint confirmed in the process template. Estimated build time: 12 hours. Complexity: Moderate.
// Input
shopify.order.id // used as Xero invoice reference
shopify.order.tags // must contain 'wholesale' or 'b2b'
shopify.order.line_items[] // { title, sku, quantity, price }
shopify.order.total_price // string, e.g. '540.00'
shopify.order.total_tax // string
shopify.order.customer.email // buyer contact for invoice
shopify.order.billing_address // used to match or create Xero contact
// Output
xero.invoice.type // 'ACCREC'
xero.invoice.status // 'DRAFT'
xero.invoice.contact.name // matched from billing_address.company or customer name
xero.invoice.line_items[] // { description, quantity, unit_amount, account_code }
xero.invoice.reference // shopify.order.id as string
xero.invoice.invoice_id // Xero-generated UUID returned on POST
// On approval
// Operations Manager reviews draft in Xero and sets status to AUTHORISED
// No automation action required post-draft; approval is a manual step- Xero API authentication uses OAuth 2.0 with PKCE. The Xero app must be registered in the Xero Developer Portal under [YourCompany.com]'s Xero organisation. Required scopes are accounting.transactions and accounting.contacts. The access token expires after 30 minutes; the automation platform must use the refresh token to obtain a new access token before each API call.
- The Xero contact lookup must attempt to match the buyer by company name (billing_address.company) before creating a new contact. If no match is found, a new Xero contact is created using the billing name and email. Confirm the Xero account codes to use for product line items and tax with the Operations Manager before build. Do not use a placeholder account code.
- Wholesale tag detection must be case-insensitive. The tag string from Shopify is comma-separated; the agent must split on commas, trim whitespace, and check for 'wholesale' or 'b2b' as exact substring matches.
- The Finance Agent must run regardless of whether the Dispatch Agent succeeds. Tag detection happens at the Order Intake Agent output stage so that a wholesale order with an out-of-stock item still triggers a draft invoice for the relevant lines if confirmed by the process owner. Confirm this edge-case behaviour before build.
- Xero rate limit is 60 API calls per minute per connection. At 220 orders per month with an estimated 15 to 20 percent wholesale proportion (33 to 44 orders), peak load is minimal. Standard retry logic with a 2-second delay on 429 is sufficient.
- The invoice must not be submitted or sent by the automation. Status must remain 'DRAFT' at all times. Confirm with the Operations Manager (Priya Mehta) that the Xero notification settings will alert her when a new draft invoice appears, or build a supplementary Slack notification to '#finance-alerts' as a prompt.
03End-to-end data flow
// ============================================================
// ORDER PROCESSING & FULFILMENT — END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Shopify emits order/paid webhook on customer checkout completion
SHOPIFY_WEBHOOK POST /webhooks/orders/paid
-> payload.id // shopify_order_id
-> payload.email // customer_email
-> payload.tags // raw tag string, e.g. 'wholesale, B2B'
-> payload.shipping_address // { name, address1, city, province, zip, country_code }
-> payload.shipping_lines[0].title // service_level_label
-> payload.line_items[] // [{ sku, quantity, variant_id, title, price }]
-> payload.total_price // order_total
-> payload.total_tax // order_tax
-> payload.billing_address // { company, name, email }
// ============================================================
// AGENT 1: ORDER INTAKE AGENT
// ============================================================
// Dedupe check
DATASTORE.lookup(shopify_order_id)
-> IF found: short-circuit, log duplicate, halt
-> IF not found: DATASTORE.write(shopify_order_id, timestamp_utc)
// Stock verification — one call per line item
FOR each line_item IN payload.line_items[]:
LINNWORKS GET /api/Stock/GetStockItemsFull
-> request.SKU = line_item.sku
-> response.Available // available_qty
-> response.StockItemId // linnworks_stock_id
-> stock_check[sku] = { available_qty, linnworks_stock_id, requested_qty: line_item.quantity }
// Branch decision: all SKUs in stock?
IF any stock_check[sku].available_qty < stock_check[sku].requested_qty:
// Out-of-stock branch
SLACK POST /api/chat.postMessage
-> channel = '#fulfilment-alerts'
-> text.order_id = shopify_order_id
-> text.oos_skus = [{ sku, title, requested_qty, available_qty }]
-> text.shopify_order_url = 'https://[YourCompany.com].myshopify.com/admin/orders/{shopify_order_id}'
verified_order.status = 'on_hold_oos'
HALT dispatch pipeline
ELSE:
// All in stock — build verified_order record
verified_order = {
order_id: shopify_order_id,
customer_email: customer_email,
shipping_address: payload.shipping_address,
service_level: SERVICE_LEVEL_MAP[service_level_label], // mapped to { carrier_code, service_code }
line_items: [{ sku, quantity, linnworks_stock_id }],
tags: parse_tags(payload.tags),
order_total: payload.total_price,
order_tax: payload.total_tax,
billing_address: payload.billing_address,
status: 'ready_for_dispatch'
}
// Emit verified_order to Dispatch Agent AND (if wholesale) to Finance Agent in parallel
// ============================================================
// AGENT 2: DISPATCH AGENT (receives verified_order)
// ============================================================
// Step 1: Create shipment in ShipStation
SHIPSTATION POST /shipments/createlabel
-> request.carrierCode = verified_order.service_level.carrier_code
-> request.serviceCode = verified_order.service_level.service_code
-> request.shipTo.name = verified_order.shipping_address.name
-> request.shipTo.street1 = verified_order.shipping_address.address1
-> request.shipTo.city = verified_order.shipping_address.city
-> request.shipTo.state = verified_order.shipping_address.province
-> request.shipTo.postalCode= verified_order.shipping_address.zip
-> request.shipTo.country = verified_order.shipping_address.country_code
-> response.shipmentId // shipstation_shipment_id
-> response.trackingNumber // tracking_number
-> response.labelData // base64 PDF label
-> response.carrierCode // carrier_code (confirmed)
IF error: POST Slack alert to '#fulfilment-alerts', log error, HALT
// Step 2: Write fulfilment and tracking back to Shopify
SHOPIFY POST /admin/api/2024-01/orders/{shopify_order_id}/fulfillments.json
-> request.fulfillment.tracking_number = tracking_number
-> request.fulfillment.tracking_company = carrier_code
-> request.fulfillment.tracking_url = tracking_url (constructed from carrier_code + tracking_number)
-> request.fulfillment.notify_customer = false // Gmail handles notification
-> response.fulfillment.id // shopify_fulfillment_id
-> response.fulfillment.status // must equal 'success'
IF error: log, do NOT proceed to Gmail or Linnworks
// Step 3: Send tracking confirmation via Gmail
GMAIL POST /gmail/v1/users/me/messages/send
-> request.to = verified_order.customer_email
-> request.subject = 'Your order is on its way'
-> request.body = TEMPLATE(carrier_code, tracking_number, tracking_url)
-> response.id // gmail_message_id (logged)
// Step 4: Deduct stock in Linnworks
FOR each item IN verified_order.line_items[]:
LINNWORKS POST /api/Orders/SetOrderItemsQuantity (or stock adjustment endpoint)
-> request.StockItemId = item.linnworks_stock_id
-> request.Quantity = item.quantity
-> request.Note = 'Auto-deducted: Shopify order ' + shopify_order_id
IF error: log with order_id, alert Slack '#fulfilment-alerts'
// ============================================================
// AGENT 3: FINANCE AGENT (parallel branch, wholesale only)
// ============================================================
// Tag check (case-insensitive)
IF 'wholesale' OR 'b2b' IN verified_order.tags:
// Xero contact lookup or create
XERO GET /api.xro/2.0/Contacts?where=Name='{billing_address.company}'
-> IF match: xero_contact_id = response.Contacts[0].ContactID
-> IF no match:
XERO POST /api.xro/2.0/Contacts
-> request.Name = billing_address.company OR billing_address.name
-> request.EmailAddress = customer_email
-> response.ContactID // xero_contact_id
// Create draft invoice
XERO POST /api.xro/2.0/Invoices
-> request.Type = 'ACCREC'
-> request.Status = 'DRAFT'
-> request.Contact.ContactID = xero_contact_id
-> request.Reference = shopify_order_id
-> request.LineItems[] = [{ Description: sku+title, Quantity, UnitAmount: price, AccountCode }]
-> request.TaxType = confirmed with Operations Manager pre-build
-> response.InvoiceID // xero_invoice_id (logged)
-> response.Status // must equal 'DRAFT'
// Optional: Slack prompt to Operations Manager
SLACK POST /api/chat.postMessage
-> channel = '#finance-alerts'
-> text = 'Draft Xero invoice created for Shopify order ' + shopify_order_id
// ============================================================
// ERROR LOGGING (all agents)
// ============================================================
ON any unhandled exception:
ERROR_LOG.insert({
timestamp_utc,
agent_name,
shopify_order_id,
step,
error_code,
error_message,
payload_snapshot
})
SLACK POST '#fulfilment-alerts' with error summary04Recommended build stack
More documents for this process
Every document generated for Order Processing & Fulfilment.