{
  "info": {
    "name": "Merdi Express Public API",
    "description": "B2B public API for tracking, orders, service tiers, and webhook subscriptions. Stripe-style Bearer auth + Idempotency-Key on mutating endpoints + per-key rate limiting (100 req/sec, 10000 req/day default). \n\nBASE URL: every endpoint is served from `https://merdiexpress.com/api/v1/`. The `/api/v1` prefix is embedded in every path key (so SDK generators that ignore the `servers` field still produce correct URLs). For convenience, requests to `https://merdiexpress.com/v1/*` are 308-redirected to `https://merdiexpress.com/api/v1/*` — but please update any base URL config to the canonical `/api/v1/` form.\n\nSee https://merdiexpress.com/developers for docs + key issuance.",
    "version": "1.1.0",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://merdiexpress.com",
      "type": "string"
    },
    {
      "key": "api_key",
      "value": "mk_test_REPLACE_ME",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "reference",
      "item": [
        {
          "name": "GET List supported destination countries",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/countries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "countries"
              ]
            },
            "description": "List supported destination countries"
          }
        }
      ]
    },
    {
      "name": "rates",
      "item": [
        {
          "name": "POST Get rate quotes",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/rates",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "rates"
              ]
            },
            "description": "Quotes are entitlement-scoped: options come from the Service Codes your account can book — resolved from BOTH entitlement tracks (Product Code entitlements and assigned rate cards) — and each option carries the pricing-model v2 metadata (product_code, service_options, is_standard_service). Zoned destinations (currently AU) price by postal-code zone: supply destination_postal_code, or zoned options come back available=false with unavailable_reason. Opt-in surcharges (e.g. SIGNATURE, INSURANCE) can be passed in selected_surcharges; per-option eligibility is returned in opt_in_availability and surcharges that could not be applied are listed in surcharge_conflicts. EU destinations include an ioss_notice block showing the VAT-prepay picture without IOSS — IOSS itself is applied at shipment create (ioss_number / account default), so using_ioss is always false here. Engine rejections return 400 INVALID_FIELD whose message is the engine's actual reason; data.field classifies it — rate_card (entitlement / pricing setup; also the fallback), declared_value_hkd (destination declared-value cap), parcels (size over carrier limit) — and data.errors lists every reason.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"cod\": false,\n  \"declared_value_hkd\": 320,\n  \"destination_country\": \"destination_country\",\n  \"destination_postal_code\": \"2000\",\n  \"goods_category\": \"general\",\n  \"weight_kg\": 0.5\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "service_tiers",
      "item": [
        {
          "name": "GET List service tiers the account can use",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/service_tiers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "service_tiers"
              ]
            },
            "description": "Entitlement-scoped (2026-06-10): returns only the Service Codes the calling account can book — resolved from BOTH entitlement tracks (Product Code entitlements and assigned rate cards) — and that have a carrier assignment (fulfillable), the same rule the quote engine applies. No entitlements yet → 200 with an empty list (ask your account manager to assign a Product Code). Each tier carries the pricing-model v2 fields: product_code (the Merdi product container / entitlement unit, e.g. HKASTD — null on legacy tiers, and NOT the same thing as carrier.product_code, which is the upstream carrier's own reference), is_standard_service, and service_options (the cargo / air / last-mile dimensions)."
          }
        }
      ]
    },
    {
      "name": "shipments",
      "item": [
        {
          "name": "GET List the authenticated account's shipments",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/shipments",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments"
              ]
            },
            "description": "Cursor-based pagination. Filters: status, updated_after. Slim list-view shape — call /v1/shipments/{id} for full detail with events."
          }
        },
        {
          "name": "POST Create a shipment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/shipments",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments"
              ]
            },
            "description": "Re-quotes the selected service tier server-side, deducts the wallet in live mode (test mode skips), dispatches to the carrier, and returns the full shipment record with pricing snapshot + dispatch result. Requires Idempotency-Key. service_tier_id takes a Service Code (e.g. HKASTDGGSTCM) — list yours via GET /v1/service_tiers. Optional selected_surcharges opts into SIGNATURE / INSURANCE where eligible (check opt_in_availability on POST /v1/rates; on the US priority line signature and insurance are mutually exclusive). Optional ioss_number (IM + 10 digits) overrides the account-level IOSS default for this shipment; EU destinations only.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"goods_category\": \"general\",\n  \"ioss_number\": \"IM1234567890\",\n  \"items\": [\n    {\n      \"description\": \"Sample item\",\n      \"quantity\": 1,\n      \"unit_price_hkd\": 100\n    }\n  ],\n  \"parcels\": [\n    {\n      \"height_cm\": 6,\n      \"length_cm\": 22,\n      \"weight_kg\": 0.5,\n      \"width_cm\": 18\n    }\n  ],\n  \"payment_type\": \"prepaid\",\n  \"recipient\": {\n    \"address\": \"1 Hacker Way\",\n    \"city\": \"San Francisco\",\n    \"contact_name\": \"John Doe\",\n    \"country_code\": \"US\",\n    \"district\": \"CA\",\n    \"phone\": \"+14155551234\",\n    \"postal_code\": \"94025\"\n  },\n  \"reference_no\": \"po_2026_05_28_0017\",\n  \"sender\": {\n    \"address\": \"1 Hacker Way\",\n    \"city\": \"San Francisco\",\n    \"contact_name\": \"John Doe\",\n    \"country_code\": \"US\",\n    \"district\": \"CA\",\n    \"phone\": \"+14155551234\",\n    \"postal_code\": \"94025\"\n  },\n  \"service_tier_id\": \"HKASTDGGSTCM\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET Get detail for a single shipment",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/:shipment_id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                ":shipment_id"
              ],
              "variable": [
                {
                  "key": "shipment_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "shipment_id accepts the tracking number. Filters by clientId + livemode — your shipment only. Distinct from GET /v1/tracking/{tn} which is intentionally broader (tracking-widget use case)."
          }
        },
        {
          "name": "POST Cancel a pre-pickup shipment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/shipments/:shipment_id/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "shipments",
                ":shipment_id",
                "cancel"
              ],
              "variable": [
                {
                  "key": "shipment_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Only `label_created` (and `draft` if any exist) are cancellable; anything past first carrier scan returns 400 INVALID_FIELD(status). Idempotent — cancel-then-cancel returns the existing snapshot with `refund_hkd=0`. Live mode refunds the wallet by the original total; test mode does not (test create skipped the deduction)."
          }
        }
      ]
    },
    {
      "name": "tracking",
      "item": [
        {
          "name": "GET Get tracking information for a shipment",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/tracking/:tracking_number",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "tracking",
                ":tracking_number"
              ],
              "variable": [
                {
                  "key": "tracking_number",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Returns the public tracking projection (status, destination country/district, event timeline) for the given tracking number. Authenticated but NOT clientId-scoped — any valid Bearer key can look up any tracking number (rate-limit applies). Designed for B2B clients to power their own tracking pages."
          }
        }
      ]
    },
    {
      "name": "wallet",
      "item": [
        {
          "name": "GET Get wallet balance",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/wallet/balance",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "wallet",
                "balance"
              ]
            },
            "description": "Read the authenticated account's prepaid wallet balance. Poll to monitor funds proactively: because shipment creation is gated on `available`, POST /api/v1/shipments can start rejecting on insufficient funds the instant available dips below the next quote. `held` = open holds against in-flight shipments; `available` = `balance` - `held`. Read-only; no Idempotency-Key."
          }
        }
      ]
    },
    {
      "name": "webhooks",
      "item": [
        {
          "name": "GET List the account's webhook endpoints",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            },
            "description": "List the account's webhook endpoints"
          }
        },
        {
          "name": "POST Register a webhook endpoint",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            },
            "description": "Returns the plaintext signing secret EXACTLY ONCE in the response.secret field. Capture it; no recovery path — rotation = delete + recreate. Requires Idempotency-Key header.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"description\": \"Sample item\",\n  \"events\": [\n    \"shipment.delivered\"\n  ],\n  \"url\": \"https://example.com/hooks/merdi\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "DELETE Revoke a webhook endpoint",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/:webhook_id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":webhook_id"
              ],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Revoke a webhook endpoint"
          }
        },
        {
          "name": "POST Re-send a previously-failed webhook delivery",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/:webhook_id/deliveries/:delivery_id/redeliver",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":webhook_id",
                "deliveries",
                ":delivery_id",
                "redeliver"
              ],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "REPLACE_ME"
                },
                {
                  "key": "delivery_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Re-fires the ORIGINAL event_id + event_type + payload to the endpoint and returns the NEW delivery record. The stable event_id lets a well-behaved receiver dedupe this redeliver against the original attempt. Manual debug action (sibling to test-fire); Idempotency-Key not required."
          }
        },
        {
          "name": "POST Rotate a webhook signing secret",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/:webhook_id/rotate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":webhook_id",
                "rotate"
              ],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Generates a fresh whsec_* signing secret and replaces the encrypted blob on the endpoint doc. The new plaintext secret is returned EXACTLY ONCE — capture it. Endpoint id, url, events, and delivery history are preserved. Recommended pattern: customer accepts both old + new secrets for a brief overlap window while in-flight deliveries drain."
          }
        },
        {
          "name": "GET Get a signed sample event (no delivery)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/:webhook_id/sample",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":webhook_id",
                "sample"
              ],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Returns a fully-signed sample webhook event WITHOUT delivering it anywhere. The killer feature for integrators on private staging / localhost / CI that can't receive inbound webhooks: feed the returned `raw_body` + `X-Merdi-Signature` straight into your receiver's verification code and confirm — before production — that your HMAC check accepts a real Merdi signature. IMPORTANT: verify against `raw_body` exactly as returned, NOT a re-serialized object (JSON key order / whitespace changes the HMAC input)."
          }
        },
        {
          "name": "POST Fire a synthetic test event to a webhook endpoint",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}",
                "type": "text"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/webhooks/:webhook_id/test",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":webhook_id",
                "test"
              ],
              "variable": [
                {
                  "key": "webhook_id",
                  "value": "REPLACE_ME"
                }
              ]
            },
            "description": "Sends a fully-signed shipment.delivered event with payload.is_test=true. Returns the WebhookDelivery record (status code, body excerpt, latency) so you can confirm your receiver handled it."
          }
        }
      ]
    }
  ]
}
