# Orange Wine — API Part 13: Pre-arrival Operations and Webhooks

Version 1.0 — 28 Sep 2026 · 8 endpoints · Base URL `/api/v1` · Read with Part 00 (conventions, error catalog, permissions).

## 1. Before you start

- Base URL `/api/v1` (webhooks: `/webhooks/...`). JSON, `snake_case` keys, prefixed string IDs, ISO 8601 UTC timestamps.
- Success: `{ data, request_id, correlation_id }`. Error: `{ error: { code, message, details, retryable } }`. Gated feature: `data.enabled = false`, `status = "pending_configuration"`.
- Auth via HttpOnly cookies (`access_token`, `refresh_token`); every non-GET browser request sends `X-XSRF-TOKEN`.
- Money is always `{ amount_minor, currency }`. Percentages are basis points.
- Commands marked Idempotency = Required need an `Idempotency-Key` header (UUID v4).
- Status never changes through PATCH — use the named command endpoints.
- Full conventions, error catalog, permission catalog and open items: Part 00.

### Error codes used in this part

| Code | HTTP | Meaning |
|---|---|---|
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |
| `INVENTORY_ALLOCATION_CONFLICT` | 409 | Allocation or release could not be completed consistently. |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

### Permissions used in this part

| Permission | Grants | Endpoints |
|---|---|---|
| `pre_arrivals.manage` | Pre-arrival records (PROPOSED) | 6 |

### Endpoints requiring Idempotency-Key

| Method | Path | Title |
|---|---|---|
| POST | `/dashboard/pre-arrivals` | Create pre-arrival record |
| POST | `/dashboard/pre-arrivals/{preArrivalId}/delay` | Record supplier delay |
| POST | `/dashboard/pre-arrivals/{preArrivalId}/release` | Release (stock arrived) |

### PROPOSED and gated endpoints in this part

| Status | Method | Path |
|---|---|---|
| PROPOSED | POST | `/dashboard/pre-arrivals` |
| CONFIGURATION GATE | POST | `/webhooks/payments/{provider}` |
| CONFIGURATION GATE | POST | `/webhooks/shipping/{provider}` |

## 2. Pre-arrival Operations

Future supply is never on_hand/available (D-23). Customer orders create commitments; release receives stock and allocates commitments in one transaction.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/dashboard/pre-arrivals` | Pre-arrival records | CONFIRMED |
| 2.2 | POST | `/dashboard/pre-arrivals` | Create pre-arrival record | PROPOSED |
| 2.3 | GET | `/dashboard/pre-arrivals/{preArrivalId}` | Record detail | CONFIRMED |
| 2.4 | PATCH | `/dashboard/pre-arrivals/{preArrivalId}` | Update non-state fields | CONFIRMED |
| 2.5 | POST | `/dashboard/pre-arrivals/{preArrivalId}/delay` | Record supplier delay | CONFIRMED |
| 2.6 | POST | `/dashboard/pre-arrivals/{preArrivalId}/release` | Release (stock arrived) | CONFIRMED |

### 2.1 `GET /dashboard/pre-arrivals`

**Pre-arrival records**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/pre-arrivals` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum · optional |  |
| location_id / product_id | string · optional |  |
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/pre-arrivals?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "pa_1001",
        "product_id": "prod_500",
        "location_id": "loc_albany",
        "supplier_reference": "SUP-88",
        "expected_arrival_from": "2026-10-15",
        "expected_arrival_to": "2026-10-22",
        "status": "committed",
        "committed_quantity": 24,
        "ordered_quantity": 12,
        "allocated_quantity": 0
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `committed` \| `delayed` \| `released` \| `cancelled`. |
| committed_quantity | integer | Supplier commitment. |
| ordered_quantity | integer | Customer commitments. |
| allocated_quantity | integer | After release. |

---

### 2.2 `POST /dashboard/pre-arrivals`

**Create pre-arrival record**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/pre-arrivals` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) |
| Status | PROPOSED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/pre-arrivals HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "product_id": "prod_500",
  "location_id": "loc_albany",
  "supplier_reference": "SUP-88",
  "committed_quantity": 24,
  "expected_arrival_from": "2026-10-15",
  "expected_arrival_to": "2026-10-22"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| product_id | string · required | Product must have `pre_arrival = true`. |
| location_id | string · required |  |
| committed_quantity | integer · required |  |
| expected_arrival_from / to | YYYY-MM-DD · optional |  |
| supplier_reference | string · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "pa_1001",
    "product_id": "prod_500",
    "location_id": "loc_albany",
    "supplier_reference": "SUP-88",
    "expected_arrival_from": "2026-10-15",
    "expected_arrival_to": "2026-10-22",
    "status": "committed",
    "committed_quantity": 24,
    "ordered_quantity": 12,
    "allocated_quantity": 0
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `committed` \| `delayed` \| `released` \| `cancelled`. |
| committed_quantity | integer | Supplier commitment. |
| ordered_quantity | integer | Customer commitments. |
| allocated_quantity | integer | After release. |

---

### 2.3 `GET /dashboard/pre-arrivals/{preArrivalId}`

**Record detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/pre-arrivals/{preArrivalId}` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| preArrivalId | string |  |

**Request example**

```http
GET /api/v1/dashboard/pre-arrivals/{preArrivalId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pa_1001",
    "product_id": "prod_500",
    "location_id": "loc_albany",
    "supplier_reference": "SUP-88",
    "expected_arrival_from": "2026-10-15",
    "expected_arrival_to": "2026-10-22",
    "status": "committed",
    "committed_quantity": 24,
    "ordered_quantity": 12,
    "allocated_quantity": 0,
    "commitments": [
      {
        "order_id": "ord_6001",
        "quantity": 6,
        "status": "committed",
        "placed_at": "2026-09-17T16:00:00Z"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `committed` \| `delayed` \| `released` \| `cancelled`. |
| committed_quantity | integer | Supplier commitment. |
| ordered_quantity | integer | Customer commitments. |
| allocated_quantity | integer | After release. |
| commitments[].status | enum | `committed` \| `allocated` \| `cancelled`. |

---

### 2.4 `PATCH /dashboard/pre-arrivals/{preArrivalId}`

**Update non-state fields**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/pre-arrivals/{preArrivalId}` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| preArrivalId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/pre-arrivals/{preArrivalId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "supplier_reference": "SUP-88B",
  "committed_quantity": 30,
  "notes": "Supplier confirmed extra case"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| supplier_reference / committed_quantity / notes | optional | Status and dates are rejected here — use /delay. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pa_1001",
    "product_id": "prod_500",
    "location_id": "loc_albany",
    "supplier_reference": "SUP-88",
    "expected_arrival_from": "2026-10-15",
    "expected_arrival_to": "2026-10-22",
    "status": "committed",
    "committed_quantity": 30,
    "ordered_quantity": 12,
    "allocated_quantity": 0
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Tried to change dates/status |

---

### 2.5 `POST /dashboard/pre-arrivals/{preArrivalId}/delay`

**Record supplier delay**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/pre-arrivals/{preArrivalId}/delay` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| preArrivalId | string |  |

**Request example**

```http
POST /api/v1/dashboard/pre-arrivals/{preArrivalId}/delay HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "new_expected_arrival_from": "2026-11-01",
  "new_expected_arrival_to": "2026-11-15",
  "reason": "Supplier shipment delayed."
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| new_expected_arrival_from / to | YYYY-MM-DD · required | A date range (D-23); `to` ≥ `from`. |
| reason | string · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pa_1001",
    "status": "delayed",
    "expected_arrival_from": "2026-11-01",
    "expected_arrival_to": "2026-11-15",
    "customer_notifications_queued": 12
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Released or cancelled |

---

### 2.6 `POST /dashboard/pre-arrivals/{preArrivalId}/release`

**Release (stock arrived)**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/pre-arrivals/{preArrivalId}/release` |
| Auth | Staff `pre_arrivals.manage` (PROPOSED permission) + `inventory.receive` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| preArrivalId | string |  |

**Request example**

```http
POST /api/v1/dashboard/pre-arrivals/{preArrivalId}/release HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "received_quantity": 24,
  "damaged_quantity": 1
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| received_quantity | integer · required | Good units → on_hand. |
| damaged_quantity | integer · optional | → damaged. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pa_1001",
    "status": "released",
    "inventory_movements": [
      {
        "movement_id": "mov_2001",
        "type": "pre_arrival_receive",
        "quantity": 23
      },
      {
        "movement_id": "mov_2002",
        "type": "damage",
        "quantity": 1
      }
    ],
    "allocations_created": 12,
    "fulfillments_queued": 12,
    "shortfall": 0,
    "inventory_exception_id": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| shortfall | integer | > 0 opens an inventory exception; uncovered commitments stay `committed` (fill order FIFO — recommended default). |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Already released |
| `INVENTORY_ALLOCATION_CONFLICT` | 409 | Allocation or release could not be completed consistently. |

---

## 3. Webhooks (inbound)

Signature verified over the raw body before parsing. Provider event IDs are unique; duplicates return 200 with no effect. These are the only non-envelope responses.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 3.1 | POST | `/webhooks/payments/{provider}` | Payment provider events | CONFIGURATION GATE |
| 3.2 | POST | `/webhooks/shipping/{provider}` | Shipping provider events | CONFIGURATION GATE |

### 3.1 `POST /webhooks/payments/{provider}`

**Payment provider events**

| Property | Value |
|---|---|
| Endpoint | `POST /webhooks/payments/{provider}` |
| Auth | Provider signature |
| Status | CONFIGURATION GATE |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| provider | string | Configured provider code. |

**Request example**

```http
POST /webhooks/payments/{provider} HTTP/1.1
Content-Type: application/json
<Provider-Signature-Header>: <signature>

{
  "id": "evt_123",
  "type": "payment.captured",
  "data": {
    "payment_reference": "pay_9001",
    "amount_minor": 29880
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (body) | provider-defined | Normalized internally to `payment.authorized` \| `payment.captured` \| `payment.failed` \| `refund.succeeded` \| `refund.failed` \| `dispute.opened`. |

**Response — new event (200)**

```http
HTTP/1.1 200 OK

{
  "received": true,
  "duplicate": false
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| duplicate | boolean | `true` for replays. |

**Response — duplicate (200)**

```http
HTTP/1.1 200 OK

{
  "received": true,
  "duplicate": true
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Invalid signature |

---

### 3.2 `POST /webhooks/shipping/{provider}`

**Shipping provider events**

| Property | Value |
|---|---|
| Endpoint | `POST /webhooks/shipping/{provider}` |
| Auth | Provider signature |
| Status | CONFIGURATION GATE |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| provider | string | e.g. `shippo`. |

**Request example**

```http
POST /webhooks/shipping/{provider} HTTP/1.1
Content-Type: application/json
<Provider-Signature-Header>: <signature>

{
  "type": "tracking.updated",
  "shipment_reference": "shp_1001",
  "tracking_number": "1Z999",
  "status": "delivered",
  "occurred_at": "2026-09-22T18:00:00Z"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum (normalized) | `in_transit` \| `out_for_delivery` \| `delivered` \| `exception` \| `returned_to_sender`. |
| occurred_at | ISO 8601 | Out-of-order events resolved by this timestamp. |

**Response — accepted (200)**

```http
HTTP/1.1 200 OK

{
  "received": true,
  "duplicate": false
}
```

**Notes**

- `delivered` closes the Rapid Ship SLA record. `returned_to_sender` starts the failed-delivery workflow (system return → receive → inspect → refund → order cancelled).

---
