Returns and Failed Deliveries

Failed Deliveries and Returns

Sometimes a parcel cannot be delivered, the receiver refuses it, nobody is available, the address turns out to be wrong, or the order is cancelled while the courier is already en route. When that happens the parcel is already in the courier's hands and has to go somewhere.

Gophr handles this by creating a return delivery: a second, linked delivery that takes the parcel back to the original pickup address. This page explains when that happens, what the return delivery looks like, and how your integration is told about it.


The key concept

A return is not a status on the original delivery. It is a new delivery in its own right, linked to the one that failed.

This means:

  • The original delivery reaches a terminal status (CANCELED) and never moves again.
  • A new delivery is created with leg_type set to RETURN. Its pickup address is the failed delivery's dropoff address, and its dropoff address is the original pickup address.
  • The return delivery has its own delivery_id, its own status lifecycle, its own ETAs, and its own price.

When a return is created

A return delivery is created when a delivery is cancelled after the parcel has been picked up (that is, at or after EN_ROUTE) for a reason that means the goods still exist and need to go back.

The reasons that trigger a return are:

ReasonMeaning
REFUSEDThe receiver refused the parcel
PERSON_NOT_AVAILABLENobody was available to take the parcel
ADDRESS_NOT_FOUNDThe dropoff address could not be found
CHALLENGE_25The receiver failed an age verification check
DAMAGEDThe item was damaged
ORDER_CANCELLEDThe order was cancelled after collection

Returns are not created when:

  • The delivery is cancelled before the parcel is picked up — there is nothing to return.
  • The delivery is cancelled for a reason outside the list above.
  • Returns are disabled on your account.

Returns are automatic. You never need to book the return leg yourself, and you should not create your own replacement delivery in response to a failed one — you would be charged for a collection that Gophr has already scheduled.


What the return delivery looks like

PropertyValue on the return delivery
leg_typeRETURN
delivery_idNew — different from the original delivery
job_idNew — the return is its own job
external_idCopied from the original delivery
Pickup addressThe failed delivery's dropoff address
Dropoff addressThe original delivery's pickup address
CourierThe same courier who is already holding the parcel
Starting statusEN_ROUTE — the parcel is already collected
PriceCalculated and charged as a separate delivery

Two consequences worth planning for:

  1. external_id is not unique across deliveries. The original and its return share it. Always key your records on delivery_id, and use leg_type to tell the two apart.
  2. The return starts mid-lifecycle. You will not receive ACCEPTED_BY_COURIER or AT_PICKUP for it. The first status webhook you receive for a return delivery is EN_ROUTE, followed by the normal AT_DELIVERYTAKING_SIGNATUREDELIVERED progression as the parcel is handed back.

A return delivery can itself be cancelled, in exactly the same way as any other delivery.


How you find out about a return

There are three channels. Which are available to you depends on your account configuration — talk to your account manager if you need one enabled.

1. Status webhooks for the return delivery (default)

The return delivery sends Status Update Webhooks just like any other delivery. Its leg_type is RETURN:

{
  "delivery_id": "0dd41eb9-564d-4b5d-8acc-05b4cd8e0ae2",
  "external_id": "gophr-64a53d967f54d",
  "pickup_eta": "2023-07-03T10:57:16+01:00",
  "delivery_eta": "2023-07-03T11:09:28+01:00",
  "courier_location_lat": "51.50700000",
  "courier_location_lng": "-0.10500000",
  "status": "EN_ROUTE",
  "leg_type": "RETURN",
  "price_net": {
    "amount": 11.91,
    "currency": "GBP"
  },
  "price_gross": {
    "amount": 14.29,
    "currency": "GBP"
  },
  "meta_data": [
  ],
  "courier_name": "dCourier",
  "courier_notes": null,
  "courier_mobile_number": "+447777777777",
  "cancellation_comment": null,
  "proof_of_pickup": [],
  "proof_of_delivery": [],
  "events": [
    {
      "timestamp": 1688547447,
      "status": "EN_ROUTE"
    }
  ]
}

This is the simplest thing to integrate against: a return is just another delivery whose leg_type tells you what it is.

2. The linked_delivery block on the original delivery's webhook

The status webhook for the original delivery carries the return delivery nested under linked_delivery. This lets you see the failure and the return in a single message.

The field is null when no return exists.


linked_delivery reference

Present on the status webhook of the original delivery. null when the delivery has no return.

FieldTypeDescription
job_idstringJob identifier of the return
delivery_idstringDelivery identifier of the return — use this as your key
external_idstringCopied from the original delivery
statusstringCurrent status of the return delivery
leg_typestringRETURN
earliest_pickup_timeISO 8601Earliest the return can be collected from the failed dropoff address
pickup_deadlineISO 8601Latest the return should be collected
earliest_dropoff_timeISO 8601Earliest the return can be handed back
dropoff_deadlineISO 8601Latest the return should be handed back
pickup_etaISO 8601Live estimate for collection
delivery_etaISO 8601Live estimate for handing the parcel back
courier_namestringCourier carrying the return
courier_mobile_numberstringCourier contact number
courier_notesstringNotes left by the courier
courier_location_latnumberCourier's last known latitude
courier_location_lngnumberCourier's last known longitude
price_netobject{ "amount": 8.50, "currency": "GBP" }
price_grossobject{ "amount": 10.20, "currency": "GBP" }
cancellation_commentstringSet only if the return itself is cancelled
proof_of_pickuparrayProof captured at the failed delivery attempt, which evidences the parcel going back onto the vehicle
proof_of_deliveryarrayProof captured when the parcel is handed back
eventsarrayStatus history
meta_dataobjectReserved for future use

Time fields are ISO 8601 with offset, for example 2026-07-30T15:42:00+01:00. Any of them may be null if the corresponding time has not been set.

For the return delivery's own status history, prefer the status webhooks it sends directly.

Example

{
  "job_id": "b81d20f4c7",
  "delivery_id": "3e77a5091d",
  "external_id": "ORDER-10432",
  "status": "CANCELED",
  "leg_type": null,
  "courier_name": "Alex",
  "price_net": { "amount": 12.00, "currency": "GBP" },
  "price_gross": { "amount": 14.40, "currency": "GBP" },
  "proof_of_delivery": [],
  "linked_delivery": {
    "job_id": "0f4c1b2e9a",
    "delivery_id": "7d3a91cc42",
    "external_id": "ORDER-10432",
    "status": "EN_ROUTE",
    "leg_type": "RETURN",
    "earliest_pickup_time": "2026-07-30T15:42:00+01:00",
    "pickup_deadline": "2026-07-30T16:12:00+01:00",
    "earliest_dropoff_time": "2026-07-30T15:42:00+01:00",
    "dropoff_deadline": "2026-07-30T18:00:00+01:00",
    "pickup_eta": "2026-07-30T15:45:00+01:00",
    "delivery_eta": "2026-07-30T16:20:00+01:00",
    "courier_name": "Alex",
    "courier_mobile_number": "+447700900123",
    "courier_location_lat": 51.5142,
    "courier_location_lng": -0.0931,
    "price_net": { "amount": 8.50, "currency": "GBP" },
    "price_gross": { "amount": 10.20, "currency": "GBP" },
    "cancellation_comment": null,
    "proof_of_pickup": [],
    "proof_of_delivery": [],
    "events": [],
    "meta_data": {}
  }
}

A worked example

An order is collected from a warehouse and the receiver refuses it at the door.

#What happensWhat you receive
1Courier collects the parcelAT_PICKUP, then EN_ROUTE for delivery 3e77a5091d
2Courier arrives, receiver refuses the parcelAT_DELIVERY for 3e77a5091d
3Delivery is cancelled with reason REFUSED; Gophr creates the returnCANCELED for 3e77a5091d, carrying linked_delivery for the new delivery 7d3a91cc42
4Courier sets off back to the warehouseEN_ROUTE for 7d3a91cc42, leg_type: "RETURN"
5Courier arrives at the warehouseAT_DELIVERY for 7d3a91cc42
6Parcel is handed back and signed forDELIVERED for 7d3a91cc42 — the goods are back with the sender

The order is only fully resolved at step 6. Between steps 3 and 6 the parcel is in transit back to you.


Integration checklist

  • Key your delivery records on delivery_id, never on external_id alone - the original and its return share an external_id.
  • Read leg_type on every status webhook. RETURN means these updates describe goods coming back to you, not going out.
  • Do not treat CANCELED as the end of the story. Check for a return before writing the order off, refunding, or restocking.
  • Expect a return delivery's first status to be EN_ROUTE, not ACCEPTED_BY_COURIER.
  • Only mark stock as returned when the return delivery reaches DELIVERED.
  • Never book your own collection in response to a failed delivery - the return is created automatically.
  • Handle linked_delivery: null, which is the normal case for the vast majority of deliveries.

Account configuration

Returns and the way they are reported are configured per account:

  • Returns - whether a failed delivery produces a return leg at all.
  • Return delivery webhooks - whether the return delivery sends its own status webhooks. Enabled by default.
  • linked_delivery in webhooks - whether the original delivery's webhook embeds the return. Available on request.
  • return_job in the job resource - whether GET /jobs/{job_id} includes the return. Available on request.

Contact your account manager to change any of these.


See also



Did this page help you?