Skip to content

Powered by Grav CMS + Helios Theme

Tracking

This page covers how a parcel's progress reaches your store, what each status means, and what the customer is told.

Before you begin

  • At least one label bought. The tracking sweep is booked the first time a label is bought; a store that has never bought one has no sweep at all.
  • The worker running. Tracking is entirely on the queue.
  • kahunacart.orders.view to read tracking, and kahunacart.shipping.manage to set the webhook up.

Two ways of finding out, and your store needs both

The webhook is prompt. The aggregator posts to an address belonging to your store within seconds of a scan, and the customer sees the parcel move while it is moving.

The poll is reliable. Every tracking.poll_hours — six by default — every parcel still in the air is asked about directly.

The poll matters more than it looks. A URL pasted wrong, a site that changed domain, a firewall, a secret rotated on one side: every one of those looks identical from inside the store, and what it looks like is "the parcel never moved". Switch every webhook off and the poll still takes a parcel all the way to delivered.

Both end up in the same place and neither writes anything the other does not. The only difference is how quickly.

The seven statuses

Couriers have their own vocabularies and the aggregator has a shorter one again. Everything is recorded as one of seven.

Status What it means
unknown The courier has the number and nothing else yet, or said something nobody recognised.
pre_transit A label exists. The parcel has not been handed over.
in_transit Moving.
out_for_delivery On a van, today.
delivered Done.
returned Coming back to you.
failure The courier has stopped trying.

Shippo has six of these. It has no separate word for out for delivery and reports that as in transit with a substatus; the substatus is kept beside the event so nothing is lost.

Note

A status nobody recognises is kept, never dropped. It is stored as unknown with the courier's own words beside it. A parcel whose history has a hole in it is a parcel nobody can answer a question about.

What each status does

Status What happens
delivered Stamps the label, writes a line into the order's history, and sends the customer a delivered email.
returned, failure Raises a Parcel came back notification for the store owner, once per parcel.
everything else Records the event and does nothing else.

Each of those hangs off the writing of a tracking event rather than off the status, so a parcel that reports delivered three times — a webhook, a redelivered webhook and a poll that overlapped them — produces exactly one email.

The delivered email

A separate message from the shipped email KahunaCart already sends. It carries the courier's name, the tracking number, a link to the courier's own page and the date it arrived, and no label, no postage cost and no box. It goes out in the language the order was placed in, and it can be switched off per store on the notifications screen like any other message.

Note

It cannot be re-sent from an order's Send email… menu. That list is fixed inside KahunaCart and an add-on cannot join it, so a delivered email that needs sending again goes from the mail log. See Emails.

Set up the webhook

The steps below are for Shippo. ShipEngine uses the additional header and registration instructions below.

Four steps, all of them by hand for now.

  1. Make yourself a long random string. openssl rand -hex 24 is one way to get one.

  2. Open KahunaCart → Shipping carriers → Settings → Shippo, put it in Webhook secret, and click Save.

  3. Build your store's tracking address out of it:

    TXT
    https://your-store.example/api/v1/kahunacart/shipping/public/webhook/shippo/<your secret>
    
  4. Register that address in Shippo, on its Settings → Webhooks page, for the track_updated event.

Parcels now move within seconds of a scan rather than within six hours.

The address has to be https and under 200 characters, both of which are Shippo's own rules. Register one address, not two: a second one pointing at the same store means every scan arrives twice.

To rotate the secret, change it in step 2 and register the new address. The old one stops working the moment you save, so do both in one sitting.

Note

There is no Generate button and no Register button. The driver implements the whole self-setup contract and the plugin can build the address, but no route exposes either, so the manual steps above are what a merchant has. The same four steps are on the plugin's Instructions tab.

The secret is the credential

Shippo does not sign its webhooks. There is no shared secret, no HMAC header, and no way to ask for one without an account-manager conversation. So your store's protection is the unguessable part of the address itself, compared in constant time, plus two optional extras.

Setting What it does
shippo.webhook_ips Empty accepts any caller, which is the default. auto accepts only Shippo's nine published addresses. Anything else is a comma-separated list.
Rotating the secret Changes the address. The old one stops working immediately.

Warning

Leave shippo.webhook_ips empty unless you have a reason not to. A site behind a proxy or a tunnel that rewrites the caller's address will lock itself out of its own tracking, and that looks exactly like the webhook never arriving.

The endpoint writes no tracking. It checks the size, checks the secret, writes a log row, queues a job and answers 200. The job then re-reads the parcel's record from the aggregator with your store's own key. So a forged ping costs one API call and writes nothing that did not come from the courier, which is why an unsigned webhook is acceptable here and would not be if the payload were trusted.

The delivery log

Every inbound delivery is written down — when it arrived, where from, and what happened to it — and kept for thirty days. It is on the Shipping carriers screen.

It exists for the single most common question anybody asks about a webhook: is it not arriving, or is it arriving and being refused? Those are two completely different problems and without the log they look the same.

Outcome Meaning
accepted Logged and queued.
unknown_parcel A real delivery for a parcel this store does not have. When it arrives while a label from the same provider is being bought, the provider is asked to send it again (status 503), and the retry is normally accepted a few minutes later.
bad_secret The secret in the path did not match.
bad_source shippo.webhook_ips is set and the caller is not on it.
too_large The body was over 256 KB.
ignored Read fine, and named no parcel to act on yet, such as Sendcloud's first status for a new label before it has a tracking number. Nothing is wrong.
unreadable A body that could not be read at all.

No rows at all means the webhook is not arriving. Check the address registered at Shippo. Rows saying bad_secret mean it is arriving and being refused, so the secret was rotated on one side only.

Test tracking without posting anything

Shippo has test tracking numbers that answer a whole delivery history on demand. Use the carrier token shippo with one of SHIPPO_PRE_TRANSIT, SHIPPO_TRANSIT, SHIPPO_DELIVERED, SHIPPO_RETURNED, SHIPPO_FAILURE or SHIPPO_UNKNOWN.

Important

A real tracking number cannot be tracked with a test key. Ask about a genuine USPS number in test mode and Shippo answers, in as many words, usps is not a valid test tracking carrier. Please use 'shippo'. A test label bought against a real courier gets a real number that will never move while you are in test mode. Nothing is wrong; test mode has two halves that do not meet.

When tracking goes wrong

What happened What you see What to do
A parcel never moves No delivery rows at all The webhook is not arriving. Check the address at Shippo. The poll should still be moving it
Rows say bad_secret The webhook arrives and is refused Register the current address at Shippo again
A Webhook rejected message arrives The aggregator posted to the store and was refused: a wrong or missing secret, a wrong source, a bad signature or a driver that cannot read tracking. Once per reason per day Check the secret at the aggregator against the one on the Shipping carriers screen. Needs KahunaCart 1.5.3
Rows say bad_source Every delivery refused Clear shippo.webhook_ips. A proxy rewrites the caller's address
Tracking stopped after 45 days Nothing By design (tracking.poll_until_days). A parcel still moving after six weeks is one somebody has already telephoned about
The poll cannot reach the aggregator A Carrier unreachable notification, once a day Usually the aggregator having an outage. Check its status page
A status shows odd wording The courier's own words Kept on purpose
The delivered email never went Nothing in the mail log Check the message is enabled on the notifications screen
No polling at all No shipping.poll_tracking job in the queue The sweep is booked when the first label is bought

ShipEngine webhooks

Save a long random value in Settings → ShipEngine → Webhook secret. The endpoint is https://your-store.example/api/v1/kahunacart/shipping/public/webhook/shipengine/<your secret>. ShipEngine must also send X-KahunaCart-ShipEngine-Secret: the lowercase hexadecimal HMAC-SHA256 of the literal kahunacart-shipping:shipengine:webhook, keyed by the saved secret. A delivery with no header or the wrong value is refused and logged.

Registration currently needs a developer: call ShipEngineAggregator::setupWebhook() from a bootstrapped store integration, or register the track event through ShipEngine's /v1/environment/webhooks API with the endpoint and headers: [{key: "X-KahunaCart-ShipEngine-Secret", value: "<derived value>"}]. The driver reuses this store's registration. There is no registration button in the admin yet.

When rotating the secret, update both the registered URL and the header. The driver's reset option returns the new secret and its config path; the caller must save that result. The old credential stops working after the change.

The header is a shared credential, not a signature over the body. ShipEngine offers RSA signatures, but this driver does not verify them, so the capability card says Signed webhooks: no. The tracking worker always rereads the parcel through the API before recording events. API-created labels are tracked automatically.

ShipEngine's normalized status details identify out-for-delivery and return-to-sender events. Unknown statuses remain in the history with the carrier's wording. The sandbox has no simulated tracking journey and sandbox labels produce no genuine webhook events. A sandbox test therefore cannot verify delivered email end to end. Registration reports the API's actual answer and never uses a production key as a fallback.

Sendcloud webhooks

Sendcloud's webhook is set up from the carriers screen. Press Generate secret, then Register webhook: the add-on finds your Sendcloud API integration, switches on its Webhook feedback and fills in your store's address. You can do the same by hand in Sendcloud under Settings → Integrations → Configure if you prefer, pasting the address the carriers screen shows. Registering again is harmless.

Sendcloud signs every delivery with your integration's secret key, the one on the Sendcloud tab, and the store checks every delivery against it, so the capability card says Signed webhooks: yes. There is nothing else to copy anywhere. Deliveries can arrive out of order; the store re-reads the parcel from Sendcloud each time, so the latest status always wins.

Your store must be reachable from the internet over https for Sendcloud to deliver. A site on a local .test address will register fine and receive nothing.

Sendcloud sends its first status for a new label while the purchase is still finishing, before the store knows the tracking number. That delivery is asked to come again and is accepted on the retry, a few minutes later. Labels bought in Test mode are free letters that no carrier ever scans, so they produce a few statuses, such as the cancellation, and no tracking events.