Order import API

JV Medical · Developer guide · Version 1.0

Download integration kit · OpenAPI specification · Sample order JSON · Next.js server example

# Sendaparcel order import API — JV Medical
Version 1.0 · 3 November 2026
Base URL: https://sendaparcel.com.au

This is a push-to-Imports integration. It does not expose automatic quote, payment or courier-booking operations. JV Medical sends paid orders in its Processing / ready-to-dispatch state. A person reviews the imported delivery address, suggested parcel sizes, pickup location and service in Sendaparcel before paying and booking. Product prices and order totals are not freight prices and are not used for charging freight.

## Credentials and environments
An administrator opens Admin → Customers → Open customer → Order API connection and creates a TEST API key for the correct customer. Keys are shown once and should be shared through a password manager or other secure channel. Never embed keys in Next.js client components, NEXT_PUBLIC_* variables, browser storage, URLs or application logs. Use a server-only SENDAPARCEL_API_KEY environment variable. Use Authorization: Bearer <key> on every request.

Test and live keys use the same base URL. The key determines the environment. Test keys start sap_test_; live keys start sap_live_. There are no shared demo credentials. API · TEST imports appear in that customer's Imports page but are blocked from live booking. They are separate from live imports with the same reference. Do not use a live key for a dummy-order booking test. Revoking a key takes effect on subsequent requests; a suspended customer account cannot use its keys.

## Operations
| Method | URL | Purpose |
|---|---|---|
| POST | /api/v1/imports | Create or replace an imported order by reference |
| PUT | /api/v1/imports/{reference} | Create or replace that reference using the full order payload |
| GET | /api/v1/imports/{reference} | Read import, shipment, tracking, pickup and label state |
| DELETE | /api/v1/imports/{reference} | Withdraw an unbooked import; never cancels a courier booking |
| GET | /api/v1/labels/{labelId} | Download a private PDF using the same Bearer key |

POST/PUT require application/json. Maximum body size is 64 KB. Rate limit: 120 requests/minute per key. After HTTP 429, wait at least 60 seconds. Poll each active reference about once a minute; reduce frequency after delivery or cancellation. There are no outgoing tracking webhooks in version 1.0. GET returns the latest recorded Sendaparcel status; it does not force an immediate courier tracking refresh.

## Card payment before booking

Importing an order creates an unbooked order for review in Sendaparcel. The customer must review the parcel details, choose a rate and pay the freight by card. A courier booking is created only after Stripe confirms successful payment. Payment for products in JV Medical’s store does not pay for Sendaparcel freight. This API cannot create live courier bookings or bypass card checkout.

## Payload
See jv-medical-order.json for the supplied example. reference is required: 1–80 letters, numbers, dots, underscores or hyphens. Keep it stable across retries; it is unique within the customer account and environment, including after API-key rotation. orderedAt is optional ISO 8601 with timezone. recipient.name, recipient.phone, deliveryAddress.line1/suburb/state/postcode/country and 1–100 items are required. country must be AU; postcode must be four digits (send as a string to preserve leading zeroes). recipient.businessName/email and deliveryInstructions are optional. deliveryInstructions maximum 250 characters. Item names max 200 characters; SKU max 100; quantity integer 1–10000. Item prices and order totals may be sent but are not stored or interpreted by the shipping integration.

parcels is an optional array of up to 20 suggested parcel types, at most 99 pieces total. Each entry has type carton or satchel; lengthCm/widthCm/heightCm are positive whole centimetres; weightKg is 0.01–25 kg. Length + width + height ≤180 cm. Satchels ≤5 kg. Optional quantity defaults to 1, integer 1–99. Empty parcels means the shipping team must enter measurements in Sendaparcel. Suggested measurements are always reviewed before booking. Business recipients default to a business address; otherwise residential defaults true. Optional residential boolean overrides this classification.

Pickup is chosen by the shipping team in Sendaparcel, not supplied by this API. Add the Wangara location to the customer's Address book: JV Medical, Unit 4, 83 Motivation Drive, Wangara WA 6065; phone (08) 6199 7931. The Brabham WA 6055 pickup needs a complete street address and contact before use. This guide does not automatically create either pickup address.

## Retry, update and withdrawal behaviour
Submitting the same reference with the same full payload is idempotent: the response returns the same importId with duplicate:true. Different references create different imports. POST and PUT are full replacements, not JSON patches. If the URL contains a reference, the body reference must match, if supplied.

An update resets the import's saved address corrections and review flag so the team sees and reviews the newest address/items/parcel suggestions. An existing unpaid draft that has no Stripe payment session or payment group is discarded; no courier booking is cancelled. The team must review and quote again. DELETE similarly withdraws the import and releases an unlocked unpaid draft. DELETE is idempotent after withdrawal. A later POST/PUT can restore a withdrawn import.

If payment has started, a payment group is attached, or courier booking has begun, update/withdraw returns HTTP 409 shipment_locked. No courier cancellation or refund is attempted. The shipping team must manage the existing shipment in Sendaparcel. A concurrent modification may return 409 conflict; read the latest order and retry deliberately. Do not change the reference merely to bypass a conflict, as that could create a second shipment.

## Typical responses
POST or PUT success (HTTP 200):
```json
{"reference":"JV-100042","importId":"api-...","mode":"test","status":"imported","duplicate":false}
```
After an update, requiresReview:true is returned. DELETE returns status:withdrawn. POST's status imported means accepted into the inbox, not booked with a courier.

GET before booking:
```json
{"reference":"JV-100042","importId":"api-...","mode":"test","status":"pending_review","updatedAt":"2026-11-03T01:15:00.000Z","shipment":null}
```
GET after a live booking (illustrative values only):
```json
{"reference":"JV-100042","importId":"api-...","mode":"live","status":"booked","updatedAt":"2026-11-04T01:00:00.000Z","shipment":{"id":"...","status":"booked","carrier":"StarTrack","trackingNumber":"EXAMPLE123","bookingReference":"EXAMPLE123","pickupBookingId":"EXAMPLE-PICKUP","labelRequired":true,"labels":[{"id":"...","url":"https://sendaparcel.com.au/api/v1/labels/...","requiresBearer":true}]}}
```
Shipment and pickup/booking/tracking references may be null until confirmed. A label can arrive after the booking response; continue polling while labels is empty. Direct Couriers same-day service has labelRequired:false and no courier PDF. Other carriers may require labels even while their labels array is empty. Shipment status values include draft, awaiting_payment, paid, booking_pending, booked, in_transit, out_for_delivery, delivered, booking_failed, delivery_exception, cancelled, refunded and manual administrative statuses. Preserve unknown future values rather than failing your order page. Terminal cancelled/refunded status is independent from an import withdrawal.

Label URLs require the API key; they are not public email links. Your Next.js server should download and proxy the PDF behind your own customer's authorisation, or attach it to an authorised shipping email. Never append the key to a URL or send it to the customer. Download URLs work only while that key remains active and authorised for the same customer and environment.

## Error handling
All API errors use {"error":{"code":"...","message":"..."}}.
401 unauthorised: missing/invalid/revoked key or inactive account.
404 not_found: reference/label does not exist within this key's customer and environment.
409 shipment_locked or conflict: stop automatic replacement and review the latest state.
413 body_too_large; 415 content_type; 422 invalid_order or reference_mismatch: fix the request.
429 rate_limited: wait 60 seconds.
500 internal_error / 503 label_unavailable: retry with backoff, preserving the same reference. If an earlier POST timed out, GET the reference before changing anything. Do not retry a courier booking; this API only imports orders.

## Developer test sequence
1. Obtain the customer's test key securely from the administrator.
2. POST the dummy sample. Expect mode:test and status:imported; locate API · TEST in Imports.
3. Repeat the same POST. Verify identical importId and duplicate:true.
4. PUT a changed delivery address/instructions. Verify only one test import remains and review is required again.
5. GET it; expect pending_review and shipment:null. Verify Sendaparcel blocks live booking for the test import.
6. DELETE it, then repeat DELETE. Verify it appears under Removed and no pickup is created.
7. Attempt to read another account's reference or a live reference using the test key; expect 404.
8. When the teams agree to go live, create a separate live key and store it server-side. Push only real paid Processing orders. Confirm pickup/contact details and parcel measurements in Sendaparcel before authorising the first real booking.
9. After that agreed live booking, poll GET until carrier/tracking and label (where required) are available. Download PDF with Bearer authentication. Check the customer order page and shipping email mapping.

No live bookings or emails were created to test this implementation.