# Shipping services

Use the logistics company’s own shipping services: list services → load one service’s config → estimate → create the order → pay → read it back → track → cancel a test order.

This playbook documents the following operations only. Complete them in sequence. Authentication uses a **customer** account.

Replace `YOUR_HOST` and `ACCESS_TOKEN` with values from your environment.

## 1. Login (customer)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/customer/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your_password"}'
```

Place the returned `access_token` in the request header:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL uses the same header on `POST /api/graphql`.

**Verification:** login returns `access_token`. Subsequent requests without this token return `401`.

## 2. List services

**REST:** `GET /api/v1/customer/shipping-orders/services` — [REST handbook](/api/documentation#/paths/v1-customer-shipping-orders-services/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `customerShippingOrderServices` ([GraphQL handbook](/api/graphql/documentation#/customer/customerShippingOrderServices))

Each row has `service_code`, `name`, `offer_pickup`, `allow_warehouse_delivery`. An empty list means no service is assigned to this customer.

**Verification:** you captured one `service_code` (example below: `intl_express`).

## 3. Load that service’s config

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/config` — [REST handbook](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/config \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `customerShippingOrderServiceConfig` ([GraphQL handbook](/api/graphql/documentation#/customer/customerShippingOrderServiceConfig))

**Verification:** you have at least one warehouse `id` if this service allows warehouse drop-off. `403` means this customer is not allowed that service.

## 4. Estimate

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/estimate-price`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/estimate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_postcode": "M5V2H1",
    "delivery_country": "CA",
    "packages": [{
      "weight": 2,
      "length": 30,
      "width": 20,
      "height": 15,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

`origin_type`: `warehouse` (drop off at a warehouse) or `pickup` (the company collects). Match what step 3 said the service allows.

**Verification:** `result` is true and you have a price (or a “needs quote” flag for manual pricing). Do not create yet if the destination/package is refused.

## 5. Create the order

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/orders`

Send `Idempotency-Key`.

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-ship-001" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "DEV-SHIP-001",
    "delivery_name": "Jane Recipient",
    "delivery_telephone": "5555555555",
    "delivery_email": "jane@example.com",
    "delivery_address_1": "123 King St W",
    "delivery_city": "Toronto",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "M5V2H1",
    "package": [{
      "weight": 2,
      "length": 30,
      "width": 20,
      "height": 15,
      "weight_unit": 2,
      "dimension_unit": 2,
      "quantity": 1
    }]
  }'
```

**Verification:** the response has an order `id`. Store `id`, `tracking_number` / `reference_number` when present.

## 6. Pay

**REST:** `POST /api/v1/customer/shipping-orders/{id}/pay`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/9001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Optional: `GET /api/v1/customer/shipping-orders/{id}/payment-info` first. A `402` means the wallet cannot cover the amount — top up, then retry.

**Verification:** the order is payable no longer, or `remaining_balance` is `0`.

## 7. Retrieve the order and track

**REST:** `GET /api/v1/customer/shipping-orders/{id}` — [REST handbook](/api/documentation#/paths/v1-customer-shipping-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `customerShippingOrderShow` ([GraphQL handbook](/api/graphql/documentation#/customer/customerShippingOrderShow))

When a `tracking_number` is on the order, public tracking (no token):

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012
```

[REST handbook](/api/documentation#/operations/getPublicTracking) · [GraphQL handbook](/api/graphql/documentation#/tracking/trackingPublic)

**Verification:** detail is this customer’s order. Public tracking finds it once a number exists.

## 8. Configure event notifications

| Setting | Event | When |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persist `id` and tracking numbers |
| `tracking_event_webhook_url` | `tracking.event` | Timeline |
| `order_status_change_webhook_url` | `order.status_change` | Status in your system |

**REST:** `PUT /api/v1/webhook-settings` — [REST handbook](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL:** `webhookSettingsUpdate` ([GraphQL handbook](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Verify **v2** over the raw body: `HMAC_SHA256(timestamp + "." + raw_body, secret)` against `X-Webhook-Signature-V2`. Deduplicate on `X-Webhook-Event-Id`. Answer **2xx in under 3 seconds**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**Verification:** one test create produces `order.created` with that `id`.

## 9. Cancel a test order

**REST:** `POST /api/v1/customer/shipping-orders/{id}/cancel`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/9001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Verification:** a second cancel is safe, or the API says the order is already cancelled. `422` means this status cannot be cancelled.

## Test checklist

Use a test `reference` such as `DEV-SHIP-001`:

- [ ] Service list is not empty; you captured one `service_code`.
- [ ] Config returns warehouses / packaging for that service.
- [ ] Estimate returns a price (or a clear needs-quote flag).
- [ ] Create returns an `id`; the same `Idempotency-Key` does not create a second order.
- [ ] Pay succeeds, **or** you confirmed the wallet must be topped up (`402`).
- [ ] Detail shows this customer’s order.
- [ ] Public tracking finds the shipment once a tracking number exists.
- [ ] Cancel succeeds, **or** you confirmed this status cannot be cancelled.
