Returns & RMA Processing: Cross-System Integration Playbook
How do you implement returns and RMA processing across ecommerce, CRM, ERP, and WMS?
TL;DR
- Bottom line: Returns/RMA integration spans 4-6 systems (ecommerce, returns portal, CRM, ERP, WMS, payments) and requires strict sequential orchestration — the refund must never fire before the warehouse confirms receipt and inspection.
- Key limit: NetSuite Return Authorization is non-posting — it does NOT touch GL or inventory until you create the Item Receipt + Credit Memo follow-on documents. SAP requires a returns delivery (document type RE) before credit memo creation.
- Watch out for: Processing refunds before physical receipt confirmation is the #1 integration failure — it creates unrecoverable financial leakage averaging 2-5% of return value.
- Best for: Any business processing >50 returns/day across online and/or physical channels that needs automated end-to-end flow from return request to refund and inventory restock.
- Authentication: Each system uses its own auth — OAuth 2.0 (Salesforce, Shopify), Token-Based Auth or OAuth 2.0 (NetSuite), x-csrf-token + OAuth (SAP), Azure AD (Dynamics 365). iPaaS middleware manages credential lifecycle.
System Profile
This playbook covers the end-to-end returns/RMA flow across the six system categories involved in a typical returns operation. The "source of truth" shifts at each stage: the ecommerce platform owns the customer-facing return request, the CRM owns the case/service interaction, the WMS owns the physical receiving and inspection, and the ERP owns the financial settlement (credit memo, refund, inventory valuation). Integration middleware (iPaaS or custom) orchestrates the handoffs.
Not covered: vendor/supplier returns (use Procure-to-Pay), warranty claim processing with repair depot workflows, or recalls with serial-number-level tracking.
| System | Role | API Surface | Direction |
|---|---|---|---|
| Ecommerce (Shopify / Magento / BigCommerce) | Return initiation, customer-facing status, refund execution | REST / GraphQL / Webhooks | Outbound (triggers) + Inbound (refund) |
| Returns Portal (Loop / ReturnGO / AfterShip) | Self-service return request, label generation, reason capture | REST / Webhooks | Outbound (return data) |
| CRM (Salesforce Service Cloud) | Case management, agent-assisted returns, RMA approval | REST v62.0 / Platform Events | Bidirectional |
| ERP — NetSuite | Return Authorization, Item Receipt, Credit Memo, Customer Refund, GL posting | SuiteTalk SOAP / REST / SuiteQL | Inbound (master) |
| ERP — SAP S/4HANA | Returns Order (RE), Returns Delivery, Credit Memo, FI posting | OData v4 / BAPI / IDoc | Inbound (master) |
| ERP — Dynamics 365 | Return Order, disposition codes, Credit Note, inventory adjustment | OData v4 / Business Events | Inbound (master) |
| WMS | Receiving, inspection, disposition, restock / quarantine / scrap | REST / EDI 180 | Bidirectional |
| Payment Gateway (Stripe / Adyen / PayPal) | Refund execution to original payment method | REST | Inbound (refund call) |
API Surfaces & Capabilities
| System | Return Object | Create API | Status Updates | Webhook/Event Support | Bulk? |
|---|---|---|---|---|---|
| Shopify | Refund / Return | POST /admin/api/2025-01/orders/{id}/refunds.json | GET /admin/api/.../refunds.json | return_created, refund_created | No |
| Loop Returns | Return | POST /api/v1/warehouse/return | Webhooks on status change | return.created, return.received, return.resolved | No |
| Salesforce OM | ReturnOrder + ReturnOrderLineItem | POST /services/data/v62.0/sobjects/ReturnOrder | PATCH .../ReturnOrder/{id} | Platform Events | No |
| NetSuite | Return Authorization (RA) | POST /record/v1/returnAuthorization | PATCH .../returnAuthorization/{id} | SuiteScript User Events | SuiteTalk async |
| SAP S/4HANA | Returns Order (doc type RE) | POST /API_SALES_ORDER_SRV/A_SalesOrder | PATCH .../A_SalesOrder('{id}') | Business Events / IDoc | BAPI batch |
| Dynamics 365 | SalesReturnOrder | POST /data/SalesReturnOrderHeaders | PATCH .../SalesReturnOrderHeaders('{id}') | Business Events | Data Entity batch |
Rate Limits & Quotas
Per-System Limits Relevant to Returns
| System | Limit Type | Value | Impact on Returns | Notes |
|---|---|---|---|---|
| Shopify | API calls | 40 req/s (REST), 50 cost points/s (GraphQL) | Burst-create refunds may throttle | Use GraphQL for bulk queries |
| Salesforce | API calls | 100,000/24h (Enterprise) | Shared with all integrations | Monitor with /limits endpoint |
| Salesforce | Governor limits | 100 SOQL / 150 DML per transaction | ReturnOrder triggers cascade | Bulkify all triggers |
| NetSuite | Concurrent requests | 5 default, 10+ SuiteCloud Plus | Post-holiday spikes cause queuing | Implement request queuing |
| NetSuite | Governance units | 10,000 (scheduled), 1,000 (user event) | Credit Memo creation ~50-100 units | Chain scripts if needed |
| SAP S/4HANA | x-csrf-token | Required per session | Every write needs fresh token | Cache token, refresh on 403 |
| Dynamics 365 | Throttling | 6,000 req/5 min per user | Batch returns can hit limit | Use $batch OData requests |
Volume Recommendations
| Return Volume | Recommended Pattern | Key Risk |
|---|---|---|
| < 100/day | Real-time event-driven | Over-engineering |
| 100-1,000/day | Event-driven + batch financial settlement | Post-holiday spikes to 5-10x |
| > 1,000/day | Full async with message queue + batch ERP posting | Queue depth monitoring, DLQ handling |
Authentication
| System | Auth Method | Return-Specific Notes |
|---|---|---|
| Shopify | OAuth 2.0 / API key | Refund scope requires write_orders |
| Loop Returns | API Key (X-Authorization header) | Warehouse API uses separate key |
| Salesforce | OAuth 2.0 JWT bearer | Requires Order Management Operator permission set |
| NetSuite | TBA or OAuth 2.0 | Returns role permission required |
| SAP S/4HANA | OAuth 2.0 + x-csrf-token | Communication arrangement for API_SALES_ORDER_SRV |
| Dynamics 365 | Azure AD OAuth 2.0 | user_impersonation scope required |
Authentication Gotchas
- NetSuite TBA tokens do not expire but are role-bound — losing "Returns" permission causes silent failures with generic "permission violation" errors. [src1]
- SAP x-csrf-token expires with the HTTP session — pooled connection managers that reuse sessions across threads cause 403 errors on second write. Fetch new token per logical transaction. [src4]
- Salesforce Order Management license is separate from Service Cloud — without it, ReturnOrder sObject does not exist and API returns INVALID_TYPE. [src3]
Constraints
- Returns must always link to the original sales order/invoice in the ERP — orphaned returns cause GL reconciliation failures and audit findings.
- Refund execution must be gated on warehouse receipt confirmation — never process refund on return shipment tracking alone.
- NetSuite Return Authorization is non-posting — GL and inventory only affected when Item Receipt and Credit Memo are created. [src1]
- SAP returns require the full document chain: Returns Order (RE) → Returns Delivery → Credit Memo Request → Credit Memo. [src4]
- Multi-currency returns must use the exchange rate from the original transaction, not current rate.
- Partial returns require line-level tracking — integration must map individual line items, not just order-level totals.
- WMS disposition codes must map 1:1 to ERP inventory movement types — unmapped dispositions block receiving.
Integration Pattern Decision Tree
START — Customer initiates a return
|
+-- Where was the return initiated?
| +-- Ecommerce self-service portal (Shopify/Loop)
| | +-- Webhook fires return.created --> iPaaS
| +-- Agent-assisted (Salesforce Service Cloud)
| | +-- Agent creates ReturnOrder via Service Console --> Platform Event
| +-- In-store POS
| +-- POS creates return + immediate refund --> OMS sync
|
+-- Does the item need to ship back?
| +-- YES (ship-back return)
| | +-- Generate return label --> Create WMS inbound ASN
| | +-- Create ERP Return Authorization (Pending Receipt)
| | +-- WAIT for warehouse receipt confirmation
| | | +-- WMS confirms receipt --> inspection
| | | +-- Disposition: Resaleable --> restock
| | | +-- Disposition: Damaged --> quarantine/scrap
| | | +-- Disposition: Wrong item --> manual review
| | +-- After receipt + inspection:
| | +-- Create Credit Memo + Customer Refund in ERP
| | +-- Execute payment gateway refund
| | +-- Update ecommerce + CRM status
| |
| +-- NO (returnless refund / keep-the-item)
| +-- Skip WMS --> Credit Memo directly --> Payment refund
|
+-- Exchange instead of refund?
+-- Process return (same receiving flow)
+-- Create new sales order for replacement
+-- Handle price difference
Quick Reference: End-to-End Process Flow
| Step | Source System | Action | Target System | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1. Return Request | Ecommerce / Loop | Customer submits return | iPaaS | Return request, line items, reason | Retry 3x, then DLQ |
| 2. RMA Creation | iPaaS | Create Return Authorization | ERP | RA linked to original SO/invoice | Validate SO exists first |
| 3. Case Creation | iPaaS | Create/update service case | CRM | Case with RMA reference | Optional for self-service |
| 4. Label Generation | Returns Portal | Generate prepaid return label | Carrier API | Tracking number, label PDF | Fallback to manual label |
| 5. ASN Creation | iPaaS | Create inbound shipment notice | WMS | Expected items, RMA number | WMS must accept before ship |
| 6. Warehouse Receipt | WMS | Scan + receive returned items | iPaaS | Receipt confirmation, qty received | Partial receipt triggers partial flow |
| 7. Inspection | WMS | Inspect condition, assign disposition | iPaaS | Disposition code | Unknown disposition → manual queue |
| 8. Inventory Update | iPaaS | Post inventory movement | ERP | Item Receipt / Goods Receipt | Idempotent: check if already posted |
| 9. Credit Memo | iPaaS | Create credit memo | ERP | CM linked to RA + original invoice | Use original FX rate |
| 10. Customer Refund | iPaaS | Create customer refund record | ERP | Refund linked to Credit Memo | Match original payment method |
| 11. Payment Refund | iPaaS | Execute refund to payment method | Payment Gateway | Refund amount, original txn ID | Check status before retry |
| 12. Status Update | iPaaS | Update order/return status | Ecommerce + CRM | "Refunded" status | Idempotent: check current status |
Step-by-Step Integration Guide
1. Capture return request from ecommerce platform
Configure webhook subscriptions on your ecommerce platform to receive return events in real-time. [src6]
// Shopify webhook payload for return creation
// Subscribe: POST /admin/api/2025-01/webhooks.json
// Topic: "returns/request"
{
"return": {
"id": 123456789,
"order_id": 987654321,
"status": "requested",
"return_line_items": [{
"fulfillment_line_item_id": 222,
"quantity": 1,
"return_reason": "DEFECTIVE",
"customer_note": "Screen cracked on arrival"
}]
}
}
Verify: Shopify Admin > Settings > Notifications > Webhooks — HTTP 200 from endpoint within 5s.
2. Create Return Authorization in ERP
Transform the return request into the ERP's native return object. [src1]
// NetSuite: Create RA via REST API
// POST https://{accountId}.suitetalk.api.netsuite.com/services/rest/record/v1/returnAuthorization
{
"entity": { "id": "12345" },
"createdFrom": { "id": "67890" }, // Original Sales Order ID
"memo": "RMA-2026-00456 via Loop Returns",
"item": { "items": [{
"item": { "id": "1001" },
"quantity": 1,
"rate": 49.99,
"orderLine": 1
}]}
}
// Response: 204 No Content, Location header has new RA ID
Verify: GET /record/v1/returnAuthorization/{id} → status "Pending Receipt".
3. Create inbound ASN in WMS
Notify the warehouse of the expected return with RMA number and expected items. [src2]
// POST https://wms.example.com/api/v1/inbound-shipments
{
"shipment_type": "RETURN",
"rma_number": "RMA-2026-00456",
"expected_date": "2026-03-14",
"carrier_tracking": "1Z999AA10123456784",
"lines": [{
"sku": "WIDGET-BLU-LG",
"expected_qty": 1,
"inspection_required": true,
"disposition_options": ["RESTOCK", "QUARANTINE", "SCRAP"]
}]
}
Verify: WMS returns shipment ID and status "AWAITING_RECEIPT".
4. Process warehouse receipt and inspection
WMS confirms receipt and inspection disposition, driving downstream financial processing. [src2]
// WMS receipt + inspection callback
// POST https://ipaas.example.com/webhooks/wms/receipt
{
"event": "return.inspected",
"rma_number": "RMA-2026-00456",
"received_date": "2026-03-12",
"lines": [{
"sku": "WIDGET-BLU-LG",
"received_qty": 1,
"disposition": "RESTOCK",
"condition_notes": "Original packaging, no damage"
}]
}
Verify: iPaaS logs show receipt event processed. ERP RA status → "Pending Refund/Credit".
5. Create Item Receipt and Credit Memo in ERP
Two-step process: Item Receipt (inventory) then Credit Memo (GL). [src1, src5]
// Step 1: Item Receipt from RA
// POST /services/rest/record/v1/itemReceipt
{ "createdFrom": { "id": "RA_ID" }, "item": { "items": [{
"item": { "id": "1001" }, "quantity": 1, "location": { "id": "5" }, "itemReceive": true
}]}}
// Step 2: Credit Memo
// POST /services/rest/record/v1/creditMemo
{ "entity": { "id": "12345" }, "createdFrom": { "id": "RA_ID" }, "item": { "items": [{
"item": { "id": "1001" }, "quantity": 1, "rate": 49.99
}]}}
// Step 3: Customer Refund
// POST /services/rest/record/v1/customerRefund
{ "entity": { "id": "12345" }, "apply": { "items": [{
"doc": { "id": "CREDIT_MEMO_ID" }, "amount": 49.99, "apply": true
}]}}
Verify: RA status = "Closed". Credit Memo posted. GL: AR credited, Sales Returns debited.
6. Execute payment gateway refund
After ERP financial documents post, refund to original payment method. [src5]
// Stripe refund to original charge
const refund = await stripe.refunds.create({
charge: 'ch_original_charge_id',
amount: 4999, // cents
reason: 'requested_by_customer',
metadata: { rma_number: 'RMA-2026-00456', erp_credit_memo: 'CM-12345' }
});
// Verify: refund.status === 'succeeded'
Verify: Stripe refund status = "succeeded". If "pending", poll every 60s up to 5 times.
7. Update ecommerce and CRM status
Close the loop by updating all customer-facing systems. [src2]
// Salesforce: Close the service case
// PATCH /services/data/v62.0/sobjects/Case/{caseId}
{ "Status": "Closed", "Resolution_Type__c": "Refund Processed",
"RMA_Number__c": "RMA-2026-00456", "Refund_Amount__c": 49.99 }
Verify: Shopify order shows "Refunded". Salesforce case = "Closed".
Data Mapping
Field Mapping Reference
| Source Field (Ecommerce) | NetSuite | SAP S/4HANA | D365 | Gotcha |
|---|---|---|---|---|
| order_id | createdFrom (SO ref) | ReferenceSDDocument (VBELN) | SalesId | Must resolve to internal ID |
| return_reason | memo + custom field | ReasonForRejection (ABGRU) | ReturnReasonCodeId | Reason codes differ per system |
| line_item.sku | item.id (internal ID) | Material (MATNR) | ItemNumber | NetSuite needs SuiteQL lookup |
| line_item.quantity | quantity | OrderQuantity | ReturnInventoryQuantity | SAP uses sales unit of measure |
| refund_amount | rate (per unit) | NetAmount (NETWR) | SalesPrice | Must match original currency/rate |
| customer_email | entity (Customer ref) | SoldToParty (KUNNR) | CustAccount | Cross-system ID mapping required |
| tracking_number | custom field | HandlingUnitExternalID | TrackingNumber | Not native on NetSuite RA |
| disposition_code | custom field | MovementType (BWART) | InventDispositionCodeId | SAP: 541/542/551 |
Data Type Gotchas
- NetSuite uses internal numeric IDs — SuiteQL lookup required to resolve order numbers to IDs. [src1]
- SAP amounts stored in document currency — exchange rate must come from original billing document (VBRK-KURRF). [src4]
- Shopify multi-currency stores: API accepts presentment currency — passing shop currency silently creates wrong refund amount. [src6]
- D365 disposition codes are configurable per legal entity — validate existence before posting. [src7]
- Return reason codes have no industry standard — maintain custom translation table between ecommerce and ERP codes. [src4]
Error Handling & Failure Points
Common Error Codes
| System | Code | Meaning | Resolution |
|---|---|---|---|
| NetSuite | INVALID_KEY_OR_REF | Referenced record not found | SuiteQL lookup to validate SO exists |
| NetSuite | RCRD_HAS_BEEN_CHANGED | Optimistic lock conflict | GET fresh record, then PATCH |
| SAP | BUSI_EXCEPTION | Business rule violation | Check partner function / pricing |
| SAP | 403 Forbidden | CSRF token expired | Fetch new x-csrf-token and retry |
| Salesforce | INVALID_TYPE | ReturnOrder sObject not found | Verify Order Management license |
| Salesforce | ENTITY_IS_LOCKED | Record in approval process | Wait for approval or admin bypass |
| Shopify | 422 Unprocessable | Refund exceeds capturable amount | Validate refund ≤ original minus prior refunds |
| Stripe | charge_already_refunded | Duplicate refund attempt | Check refund list before creating |
Failure Points in Production
- Orphaned Return Authorization: RA created in ERP but webhook delivery failed — customer sees "submitted" but ERP has no record. Fix:
Webhook delivery confirmation with retry + daily reconciliation job. [src2] - Premature refund execution: Payment refund fires before WMS receipt — customer keeps item AND gets refund. Fix:
Gate refund on WMS inspection completion event only. [src2] - Partial receipt mismatch: 3 items expected, 2 received, credit memo created for 3. Fix:
Use WMS received quantities, not RA expected quantities, for credit memo. [src5] - Duplicate credit memos: iPaaS retry after timeout creates CM twice. Fix:
Idempotency check — query for existing CM linked to RA before creation. [src1] - Exchange rate mismatch: Credit memo uses current rate instead of original transaction rate. Fix:
Explicitly pass exchange rate from original invoice. [src4] - Unmapped WMS disposition code: New disposition code blocks Item Receipt creation. Fix:
Maintain mapping table with QUARANTINE as default fallback. [src7]
Anti-Patterns
Wrong: Processing refund on carrier delivery scan
// BAD — Refund triggers when carrier marks delivered to warehouse
// Item hasn't been inspected. Could be empty box, wrong item, or damaged.
on('tracking.delivered_to_warehouse', async (event) => {
await erp.createCreditMemo(event.rma_id); // Too early!
await stripe.refunds.create({ charge: event.charge_id }); // Money gone!
});
Correct: Gate refund on WMS inspection completion
// GOOD — Refund fires only after warehouse confirms receipt AND inspection
on('wms.inspection_completed', async (event) => {
if (event.disposition === 'REJECTED') {
await notifyCustomer(event.rma_id, 'Return rejected');
return; // No refund
}
const itemReceipt = await erp.createItemReceipt(event.rma_id, event.received_lines);
const creditMemo = await erp.createCreditMemo(event.rma_id, event.received_lines);
await stripe.refunds.create({
charge: event.charge_id, amount: creditMemo.total_cents
});
});
Wrong: Using order-level totals for partial return refunds
// BAD — Refund full order for partial return
const refundAmount = originalOrder.total; // Wrong for partial returns!
await stripe.refunds.create({ charge: chargeId, amount: refundAmount * 100 });
Correct: Calculate from individual returned line items
// GOOD — Sum returned lines at original per-unit price
const refundAmount = returnedLines.reduce((sum, line) =>
sum + (line.original_unit_price * line.returned_quantity), 0);
const taxRefund = refundAmount * originalOrder.tax_rate;
await stripe.refunds.create({
charge: chargeId, amount: Math.round((refundAmount + taxRefund) * 100)
});
Wrong: Creating ERP documents without idempotency
// BAD — Timeout retry creates duplicate Credit Memo
async function processReturn(rmaId) {
const cm = await netsuite.create('creditMemo', { createdFrom: rmaId });
// If timeout but actually succeeded, retry = SECOND credit memo
}
Correct: Idempotent document creation with existence check
// GOOD — Check for existing CM before creating
async function processReturn(rmaId) {
const existing = await netsuite.suiteql(
`SELECT id FROM transaction WHERE createdfrom = ${rmaId} AND type = 'CustCred'`
);
if (existing.items.length > 0) return existing.items[0];
return await netsuite.create('creditMemo', { createdFrom: rmaId });
}
Common Pitfalls
- Ignoring returnless refund flow: Low-value items should skip WMS entirely. Fix:
Implement returnless path that skips steps 3-8, goes directly to Credit Memo. [src6] - Not handling exchanges as linked transactions: Standalone return + standalone order inflates return rates in reporting. Fix:
Use NetSuite RA Exchange option or SAP replacement delivery linked to returns order. [src1] - Hardcoding disposition-to-action mapping: Disposition logic changes frequently. Fix:
Store mapping in configurable table (ERP custom list or iPaaS lookup). [src7] - Not reconciling return inventory daily: WMS and ERP counts drift silently. Fix:
Daily reconciliation comparing WMS receipts against ERP item receipts. Alert on discrepancies > 0. [src2] - Ignoring tax on partial refunds: Proportional tax recalculation varies by jurisdiction. Fix:
Use ERP tax engine (SuiteTax, SAP Tax, Avalara/Vertex) — never calculate tax manually in integration. [src1] - Posting to closed fiscal periods: Q4 returns processed in Q2 may hit closed period. Fix:
Check period status before posting; if closed, post to current period with original reference. [src4]
Diagnostic Commands
# NetSuite: Check RA status and linked documents (SuiteQL)
curl -X POST "https://{accountId}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" \
-H "Authorization: OAuth ..." \
-d '{"q": "SELECT id, tranid, status FROM transaction WHERE type = '\''RtnAuth'\'' AND tranid = '\''RA-12345'\''"}'
# NetSuite: Find all follow-on documents for an RA
curl -X POST ".../suiteql" \
-d '{"q": "SELECT id, tranid, type, status FROM transaction WHERE createdfrom = 12345"}'
# Salesforce: Check ReturnOrder status
curl "https://{instance}.salesforce.com/services/data/v62.0/query?q=SELECT+Id,Status,TotalAmount+FROM+ReturnOrder+WHERE+Id='\''0RR...'\''" \
-H "Authorization: Bearer {token}"
# SAP: Check returns order status
curl "https://{host}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('{id}')?$select=OverallSDProcessStatus" \
-H "Authorization: Bearer {token}"
# Shopify: List refunds for an order
curl "https://{store}.myshopify.com/admin/api/2025-01/orders/{order_id}/refunds.json" \
-H "X-Shopify-Access-Token: {token}"
# Stripe: Check refund status
curl "https://api.stripe.com/v1/refunds/{refund_id}" -u "sk_live_...:"
Version History & Compatibility
| System | Current Version | Returns API Status | Key Changes |
|---|---|---|---|
| Shopify Admin API | 2025-01 | GA | Returns resource added 2023-07 (separate from Refunds) |
| NetSuite REST API | 2025.2 | GA | RA fully in REST since 2023.1; SuiteTalk SOAP not deprecated |
| SAP S/4HANA | 2408 | GA | Advanced Returns Management (ARM) in SD Fiori |
| Salesforce OM | v62.0 (Winter '26) | GA | Managed RMA workflow enhanced Winter '26 |
| Dynamics 365 F&SCM | 10.0.40 | GA | Enhanced disposition codes for WMS-only mode |
When to Use / When Not to Use
| Use This Playbook When | Don't Use When | Use Instead |
|---|---|---|
| Processing customer returns across ecommerce + ERP + WMS | Handling vendor/supplier returns | P2P Integration |
| Returns volume > 50/day requiring automation | < 10 returns/day (manual is fine) | Manual ERP entry |
| Multi-channel returns (online + in-store) | Single-channel, single-system | ERP native returns module |
| Need financial audit trail linking return to original sale | Simple exchange without financial impact | Ecommerce native exchange |
| Operating across multiple ERPs or migrating | Single ERP, no ecommerce integration | ERP vendor documentation |
Cross-System Comparison
| Capability | NetSuite | SAP S/4HANA | Dynamics 365 F&SCM | Notes |
|---|---|---|---|---|
| Return Object | Return Authorization (RA) | Returns Order (RE doc type) | Sales Return Order | All non-posting until follow-on docs |
| API Create | REST + SuiteTalk SOAP | OData (API_SALES_ORDER_SRV) | OData (SalesReturnOrderHeaders) | SAP requires OrderType=RE |
| Receiving | Item Receipt | Returns Delivery (doc 532) | Item Arrival Journal | NetSuite: initialize from RA |
| Credit | Credit Memo | CM Request + Credit Memo | Credit Note | SAP has extra step |
| Refund | Customer Refund record | AR clearing (F-32) | Customer Payment Journal | NetSuite most explicit |
| Disposition Codes | Custom field/list | Movement Types (541/542/551) | InventDispositionCode entity | SAP most granular |
| Exchange Support | Native (RA Exchange option) | Replacement delivery | Exchange order | NetSuite simplest |
| Multi-Currency | exchangeRate field on CM | KURRF from billing doc | ExchRate on journal | All require original-rate |
| Inspection | Custom workflow / SuiteScript | QM integration (QA01/QA02) | Quality management module | SAP most mature |
| Partial Returns | Line-level on RA | Line-level on returns order | Line-level on return order | All support line-level |
Important Caveats
- This playbook covers integration architecture — each ERP requires internal configuration (NetSuite: Advanced Receiving feature, SAP: returns doc type in SPRO, D365: return order parameters) before API integration works.
- Return volumes spike 3-5x during post-holiday periods. Load-test at 5x normal volume to avoid peak-period failures.
- Tax handling on returns varies by jurisdiction — always delegate to ERP tax engine or tax service (Avalara, Vertex), never hardcode in integration.
- Returnless refund policies (keep the item, items under $20-30) bypass WMS entirely — integration must support both paths.
- This playbook assumes iPaaS middleware. Point-to-point integrations create O(n²) maintenance burden; at 4+ systems, iPaaS is mandatory.