A return is structurally just another delivery job — same shape, same lifecycle — except the pickup is at the customer's address and the dropoff is back at the merchant. The Logistics module doesn't ship a separate "returns" controller; you create a return as a job with the right stops and a reference to the original outbound job for traceability.
Creating a return job
await fetch('/logistics/delivery/jobs', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: {
firstName: 'Alex',
lastName: 'Reyes',
email: '[email protected]',
phone: '+1 555 0100',
},
stops: [
{
type: 'pickup',
location: { address: '456 Side Ave, NY' }, // customer
contact: { name: 'Alex Reyes', phone: '+1 555 0100' },
},
{
type: 'dropoff',
location: { address: '123 Main St, NY' }, // merchant
contact: { name: 'Returns Desk', phone: '+1 555 0001' },
},
],
requirements: { vehicleType: 'car', requiresPhoto: true },
scheduling: { type: 'asap' },
notes: `Return for outbound job ${originalJobId} — RMA #${rmaNumber}`,
customerNotes: 'Please pick up from front desk',
metadata: {
isReturn: true,
relatedJobId: originalJobId,
returnReason: 'wrong-size',
rmaId: rmaId,
},
}),
});
The metadata block is a free-form passthrough on the job record. Setting isReturn: true lets you filter return jobs from regular deliveries in dashboards (GET /logistics/delivery/jobs?metadata.isReturn=true via the data layer).
Tying to Storefront returns
The Storefront's /storefront/returns/* endpoints handle the merchant-side RMA workflow — issuing the return authorisation, refunding, restocking. The Logistics return job is the physical movement piece; create it once the RMA is approved and link them via metadata.rmaId.
// 1. Customer requests return through Storefront
const rma = await fetch('/storefront/returns/create', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, items, reason: 'wrong-size' }),
}).then(r => r.json());
// 2. Once the RMA is approved, schedule a Logistics pickup
const job = await fetch('/logistics/delivery/jobs', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
/* ...stops, customer, requirements... */
metadata: { isReturn: true, rmaId: rma.id },
}),
}).then(r => r.json());
// 3. Update the RMA with the pickup job id so the customer can track it
Label generation
For self-fleet returns there's no carrier label to print. The "label" is the QR/job-id sticker the driver shows at pickup so the customer hands them the right package. If you do need a parcel-carrier return label (for orgs running hybrid fleet + parcel), that's the Storefront's shipping module:
POST /storefront/shipping/return-labelissues a UPS / FedEx / USPS return label tied to the order- The customer drops the package at a carrier office; the carrier handles the rest
- The Logistics module is not involved in that path
Re-quoting and pricing
A return job is priced just like a forward job — base + distance + surcharges. If the merchant is absorbing the return cost, set the customer-paid total to zero and book the actual cost as an internal expense:
{
pricing: {
base: 0,
distance: 0,
total: 0,
customerPaid: 0,
internalCost: 14.50, // tracked for reporting
coveredBy: 'merchant',
},
}
This shape is convention rather than a schema constraint — the pricing block is intentionally flexible. Just be consistent across return jobs so your reports query the same fields.
Status lifecycle
Return jobs follow the same state machine as forward jobs (see Shipments). dropoff_completed is the trigger to mark the RMA as "received" on the Storefront side — wire it through the Automation module:
Trigger: delivery_job.dropoff_completed
Condition: data.metadata.isReturn === true
Action: PUT /storefront/returns/:rmaId/receive
There's no native bulk-return job — if you need to pick up returns from many customers in one route (a "milk run"), create one job per customer or extend a single job's stops array with multiple dropoff/pickup pairs.