Developers
MWS Connect Partner Integration Guide
MWS Connect is the standard way for a pharmacy system to book MWS deliveries and receive live delivery status. Your system makes three HTTPS calls to MWS; MWS sends status updates back to one URL you host.
How it works
An order is booked the moment your system sends it; you do not need to call MWS again unless it is cancelled.
- Register the recipient
Your system sends the patient's name, phone and address (
POST /v1/partner/client). - Send the order
Once it is packed and ready for pickup (
POST /v1/partner/order). MWS checks it straight away and replies 200 (accepted) or 400 (with the exact problem). - MWS books the delivery
For pickup at your pharmacy, within the delivery window you gave.
- MWS sends status updates
To your webhook URL as the order moves: picked up, out for delivery, delivered, or not delivered with a reason.
- Cancel if needed
Before pickup (
POST /v1/partner/order/cancel). A CANCELLED update follows.
All calls are HTTPS with JSON bodies. Your references (recipient ID and order ID) are the keys throughout; you never need to store an MWS ID.
Environments and access
Each partner gets a sandbox and a production environment, with separate keys. Build and test in sandbox; MWS switches you to production only after the test plan passes.
| Item | Sandbox | Production |
|---|---|---|
| Base URL | https://connect-api-sandbox.mwscares.com/v1 | https://connect-api.mwscares.com/v1 |
| API key | Your own sandbox key | Your own production key |
| Webhook signing secret | Your own sandbox secret | Your own production secret |
| Your webhook URL | A test URL you give MWS | Your live URL |
| Deliveries | Test only, never dispatched | Real drivers |
- Keys and secrets are sent by one-time secure link, never by email. Store them as secrets, not in code.
- Authentication: send your key in the
x-mws-api-keyheader on every call. A missing or wrong key returns 401. - IP allowlisting (optional): status updates come from one fixed address,
184.193.175.142. Allowlist it if your firewall or hosting (for example, a bot checkpoint) blocks unknown callers. - Times are 24-hour Eastern (America/Toronto) for delivery windows, and UTC ISO 8601 for timestamps.
Paths in this guide follow the base URL, for example https://connect-api.mwscares.com/v1/partner/order. New optional fields may be added within v1; ignore fields you do not recognise. Anything that would break your integration comes as a new version, with notice.
Register a recipient
/v1/partner/clientCreates or updates the person receiving the delivery. Send it before their first order, and again whenever their details change; the same external_reference_id updates the existing record.
| Field | Rules |
|---|---|
external_reference_id | Your patient or client ID. Up to 100 characters. |
first_name | Up to 100 characters. |
last_name | Up to 100 characters. If you hold a single name, send a placeholder such as -. |
phone | Valid 10-digit North American number; +1 and formatting are fine. The driver calls this number. |
address | Full street address incl. unit, city, postal code. Up to 300 characters. |
email | Valid email address, up to 254 characters. |
updated_at | When the record last changed in your system, UTC ISO 8601. |
{
"external_reference_id": "PT-10482",
"first_name": "Jane",
"last_name": "Doe",
"phone": "416-555-0142",
"email": "jane@example.com",
"address": "100 Main St, Unit 4, Mississauga, ON L5B 1A1",
"updated_at": "2026-10-18T14:05:00Z"
}Reply 200: {"status": "received", "external_reference_id": "PT-10482"}.
Create a delivery order
/v1/partner/orderBooks one delivery. Send it when the order is packed and ready for pickup; the recipient must already be registered.
| Field | Rules |
|---|---|
order_external_reference | Your order ID, unique per delivery. Up to 100 characters. Status updates and cancels use it. |
client_external_reference | The recipient's external_reference_id. |
destination_address | Where to deliver, incl. unit, city, postal code. Up to 300 characters. |
delivery_date | YYYY-MM-DD. Today up to 8 days ahead; not in the past. |
time_window_start | HH:MM, 24-hour Eastern. |
time_window_end | HH:MM, later than the start. |
delivery_options | List of option codes from the table below. Omit or send [] for none. |
cod_amount | Cash the driver collects at the door, in CAD, as "12.50". Omit or "0.00" when prepaid. Cash only. |
location_id | Which of your stores the driver picks up from. Needed only if MWS has set up more than one pickup location for you; MWS gives you the IDs. Otherwise pickup is your registered address. |
item_notes | Instructions for the driver, up to 1,000 characters. No medication names or clinical details. |
delivery_type | DELIVERY if sent (the only type today). |
status | READY_FOR_HAULAGE if sent. |
updated_at | UTC ISO 8601. |
Delivery options Coming soon
| Code | Meaning for the driver |
|---|---|
SIGNATURE_REQUIRED | Recipient must sign |
NARCOTIC | Contains a narcotic or controlled drug; MWS controlled-drug handling applies |
REFRIGERATED | Keep cold; deliver from the fridge bag |
URGENT | Priority order |
ASAP | Deliver as soon as possible after pickup |
CALL_ON_ARRIVAL | Phone the recipient on arrival |
RING_BELL | Ring the bell |
LEAVE_AT_DOOR | May be left at the door if no answer |
DO_NOT_LEAVE_AT_DOOR | Never leave unattended |
LEAVE_IN_MAILBOX | May be left in the mailbox |
MULTIPLE_BOXES | More than one box; check the count at pickup |
COLLECT_FORM | Bring back a form signed by the recipient |
COLLECT_PRESCRIPTION | Collect a paper prescription from the recipient |
Unknown codes, and contradictory pairs such as LEAVE_AT_DOOR with DO_NOT_LEAVE_AT_DOOR or with SIGNATURE_REQUIRED, are rejected with 400.
{
"order_external_reference": "RX-2026-55871",
"client_external_reference": "PT-10482",
"destination_address": "100 Main St, Unit 4, Mississauga, ON L5B 1A1",
"delivery_date": "2026-10-19",
"time_window_start": "14:00",
"time_window_end": "18:00",
"delivery_options": ["SIGNATURE_REQUIRED", "REFRIGERATED"],
"cod_amount": "12.50",
"item_notes": "Buzz 104."
}Reply 200: {"status": "received", "order_external_reference": "RX-2026-55871"}. The order is booked within seconds.
- Resending is safe. The same
order_external_referencesent twice books one delivery; the repeat changes nothing. - To change an order (address, date, window, options, amount), cancel it and send it again under a new
order_external_reference. - Cash orders: if the recipient cannot pay, the driver does not leave the order; you receive
NOT_DELIVEREDwith the reason.
Cancel an order
/v1/partner/order/cancelCancels a delivery that has not been picked up yet. The answer is immediate, and a CANCELLED status update follows.
{
"order_external_reference": "RX-2026-55871",
"reason": "Patient picked up in store"
}reason is optional (up to 500 characters) and kept for MWS records.
| Reply | Body status / meaning | What to do |
|---|---|---|
| 200 | cancellation_requested: cancelled | Nothing; a CANCELLED update follows |
| 200 | already_cancelled | Nothing; safe to resend |
| 404 | Order reference not found | Check the reference |
| 409 | Already picked up by a driver | Call MWS dispatch to stop the delivery |
| 503 | Order still being booked, or briefly unavailable | Retry after the Retry-After seconds (30) |
Status updates
MWS posts each status change to your webhook URL as JSON. The first update arrives at pickup; there is no update when the order is booked.
| Status | Meaning |
|---|---|
PICKED_UP | Driver has collected the order from your pharmacy |
AT_WAREHOUSE | Held at the MWS depot (for example, overnight before a next-day route) |
OUT_FOR_DELIVERY | On the driver's route to the recipient |
DELIVERED | Handed over |
NOT_DELIVERED | Attempt failed; failure_reason says why. A later attempt may follow |
CANCELLED | Cancelled by you or by MWS |
{
"event_id": "6f1c2a9e-8b3d-5f47-9a10-2c4e7d8b1f35",
"order_external_reference": "RX-2026-55871",
"status": "NOT_DELIVERED",
"occurred_at": "2026-10-19T19:42:10.000Z",
"tracking_url": "https://...",
"failure_reason": "Customer didn't answer"
}- Verify every call. The
x-mws-signatureheader is the HMAC-SHA256 of the exact raw request body, keyed with your webhook secret, in lowercase hex. Compute it over the raw bytes before parsing, and compare in constant time. Reject a mismatch with 401. - Reply 2xx within 10 seconds. Save the update and process it afterwards if needed.
- Retries: any other reply, or a timeout, is retried after 30 s, 1 min, 2 min and so on, at most 2 h apart, for about 24 hours. MWS is alerted if delivery still fails.
- De-duplicate on
event_id. A retried update keeps the sameevent_id; ignore one you have already stored. - Order by
occurred_at. Retries can arrive late, so never let an older update overwrite a newer one. failure_reasonis filled only onNOT_DELIVERED.tracking_urlmay be empty.
import hmac, hashlib
def is_from_mws(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")Errors and retries
A 400 lists every problem at once, so one fix-and-resend is usually enough.
{
"error": "Invalid request: phone must be a valid 10-digit North American number; delivery_date is in the past",
"problems": [
"phone must be a valid 10-digit North American number",
"delivery_date is in the past"
]
}| Reply | Meaning | What your system should do |
|---|---|---|
| 200 | Accepted | Done |
| 400 | Invalid or malformed request | Fix the data and resend; do not retry unchanged |
| 401 | Missing or wrong API key | Check the key and environment |
| 404 | Wrong path, or unknown order (cancel) | Check the URL or reference |
| 409 | Cancel too late: already picked up | Call MWS dispatch |
| 4295xxor timeout | Temporary | Retry with growing gaps (for example 30 s, 1 min, 2 min); resending is safe |
Staff should see 400 problems in your system, so a bad phone number or address can be corrected at the counter.
Testing and go-live
The sandbox test plan below is the same one completed by our live partner; it usually takes one or two working sessions together.
Sandbox test plan
- Register a recipient and create orders with and without delivery options, a cash amount and (if you have several stores) a location: all return 200 and show correctly on the booking
- Send deliberately bad requests (invalid phone, past date, missing field, wrong key): each returns the expected 400 or 401 with the right problem
- Resend an accepted order unchanged: 200, no second delivery
- MWS moves a test order through PICKED_UP, OUT_FOR_DELIVERY and DELIVERED: your endpoint returns 2xx and verifies every signature
- MWS sends NOT_DELIVERED with a reason: it shows correctly in your system
- A repeated update (same
event_id) is ignored - Cancel a fresh order: 200
cancellation_requested, then a CANCELLED update arrives - Cancel it again: 200
already_cancelled
Go-live checklist
- MWS confirms the sandbox results
- You send your production webhook URL; MWS sends your production key and secret by one-time link
- Allowlist
184.193.175.142if your hosting filters callers - Agree the cutover date, and stop booking the same orders by phone, email or portal from that date to avoid double deliveries
- First live order followed end to end by both teams
Contact
Integration questions and sandbox access: contact@mwscares.com. Same-day delivery issues: MWS dispatch.
Request sandbox access
contact@mwscares.com
Changelog
| Date | Version | Change |
|---|---|---|
| October 18, 2026 | v1 | First public release. |