# External Partner Integration Guide

This document describes how external partners integrate with Wipex to send or receive warehouse logistics data.

## Overview

Wipex acts as the integration middleware between your system and the Warasto warehouse:

- **Inbound:** You send structured JSON messages to Wipex via webhook. Wipex validates, transforms, and delivers them to the warehouse management system.
- **Outbound:** When events occur in the warehouse (shipments, inventory changes), Wipex delivers them to your registered webhook endpoint.

## Environments

| Environment | Base URL | Purpose |
|-------------|----------|--------|
| **Development** | `https://dev.wipex.warasto.fi` | Integration testing, sandbox data. Use this for all development and testing. |
| **Production** | `https://wipex.warasto.fi` | Live data, real warehouse operations. |

You will receive separate API keys for each environment. Always develop and test against the development environment first.

## Quick Reference

| Direction | Document Types | Your Role |
|-----------|---------------|-----------|
| **Inbound (you → Wipex)** | SalesOrder, PurchaseOrder, ASN, ProductMaster | You POST to our endpoint |
| **Outbound — push (Wipex → you)** | ShippingAdvice, InventoryBalance, InventoryAdjustment | You receive on your endpoint |
| **Outbound — poll (you → Wipex)** | ShippingAdvice, InventoryBalance, InventoryAdjustment | You fetch from our API *(future)* |

---

## Sending Data to Wipex (Inbound)

### Endpoint

```
POST https://wipex.warasto.fi/webhook/{tenantCode}/{docType}
```

| Parameter | Description | Example |
|-----------|-------------|---------|
| `tenantCode` | Your assigned tenant identifier | `mycompany` |
| `docType` | Document type being sent (see table below) | `SalesOrder` |

### Supported Inbound Document Types

| `docType` | Direction | Description |
|-----------|-----------|-------------|
| `SalesOrder` | IN | Customer order for warehouse fulfillment — the warehouse picks, packs, and ships the goods to the end customer. |
| `PurchaseOrder` | IN | Purchase order notifying the warehouse about expected incoming goods from a supplier. |
| `ASN` | IN | Advance Shipping Notice with package-level detail (SSCC codes, batch/expiry per item). Enables efficient goods receiving at the warehouse. *(future)* |
| `ProductMaster` | IN | Product catalogue data — identifiers, descriptions, dimensions, packaging hierarchy, and tracking requirements. Products must exist before they can be referenced in orders. |

### Required Headers

| Header | Required | Description |
|--------|----------|-------------|
| `X-Api-Key` | Yes | Your assigned API key |
| `Content-Type` | Yes | `application/json` |
| `webhook-id` | Recommended | Your unique message ID for idempotency |

**API key scoping:** Each message route (tenant + docType combination) can have its own API key. For example, if your organization has two systems — one sending SalesOrders and another sending ProductMaster updates — each system can be given a separate API key. This allows independent key rotation and access control per integration point. Discuss your setup with TANOMA during onboarding.

### Response

All valid requests return `202 Accepted`:

```json
{"status": "accepted", "requestId": "req-a1b2c3d4e5f6g7h8"}
```

**Important:** `202 Accepted` means the message was **received and queued** — NOT that it was successfully processed. Processing happens asynchronously. If your payload has validation errors (missing required fields, unknown product codes, etc.), the message will be marked as `rejected` in the audit trail. You will not receive an immediate HTTP error for business validation failures.

Save the `requestId` from the response — it can be used for troubleshooting with TANOMA operations.

### Error Responses

| Code | Meaning | Action |
|------|---------|--------|
| `400 Bad Request` | Not valid JSON, empty body, or structural error | Fix the payload and retry |
| `401 Unauthorized` | Unknown tenant code | Check the URL path |
| `403 Forbidden` | Invalid or missing API key | Check your `X-Api-Key` header |
| `405 Method Not Allowed` | Only `POST` is accepted | Change HTTP method to POST |
| `413 Payload Too Large` | Body exceeds size limit | Split into smaller messages |
| `415 Unsupported Media Type` | Missing or wrong Content-Type | Set `Content-Type: application/json` |

---

## Inbound Message Types

### 1. SalesOrder

Send a sales order to the warehouse for fulfillment (picking, packing, shipping).

```http
POST /webhook/mycompany/SalesOrder HTTP/1.1
Host: wipex.warasto.fi
Content-Type: application/json
X-Api-Key: your-api-key
```

```json
{
  "order": {
    "orderNumber": "ORD-2026-1042",
    "orderType": "ecommerce",
    "orderDate": "2026-06-01",
    "requestedDeliveryDate": "2026-06-03",
    "currency": "EUR",
    "gatheringInstruction": "Gift wrap requested"
  },
  "parties": [
    {
      "role": "buyer",
      "name": "Customer Name",
      "address": {
        "street": "Billing Street 1",
        "city": "Helsinki",
        "postalCode": "00100",
        "countryCode": "FI"
      },
      "phone": "+358401234567",
      "email": "customer@example.com"
    },
    {
      "role": "shipTo",
      "name": "Delivery Recipient",
      "address": {
        "street": "Delivery Street 5",
        "city": "Tampere",
        "postalCode": "33100",
        "countryCode": "FI"
      },
      "phone": "+358407654321"
    }
  ],
  "references": [
    { "type": "customerPO", "value": "PO-2026-001" }
  ],
  "lines": [
    {
      "lineNumber": 1,
      "item": {
        "identifiers": {
          "buyerItemNo": "SKU-001",
          "gtin": "6430012345678"
        },
        "description": "Product Name 500ml"
      },
      "orderQuantity": { "value": 2, "uom": "EA" },
      "price": {
        "unitPrice": 19.90,
        "currency": "EUR"
      }
    },
    {
      "lineNumber": 2,
      "item": {
        "identifiers": {
          "buyerItemNo": "SKU-002"
        },
        "description": "Another Product 1L"
      },
      "orderQuantity": { "value": 5, "uom": "EA" },
      "price": {
        "unitPrice": 29.90,
        "currency": "EUR"
      }
    }
  ]
}
```

**Required fields:**

| Field | Required | Notes |
|-------|----------|-------|
| `order.orderNumber` | **Yes** | Must be unique per tenant |
| `order.orderDate` | **Yes** | ISO date (YYYY-MM-DD) |
| `parties` with role `shipTo` | **Yes** | Delivery address is mandatory |
| `lines[]` | **Yes** | At least one line item |
| `lines[].item.identifiers.buyerItemNo` | **Yes** | Your product SKU — must match a product registered in the warehouse |
| `lines[].orderQuantity.value` | **Yes** | Must be > 0 |

**Optional fields:**

| Field | Default | Notes |
|-------|---------|-------|
| `order.requestedDeliveryDate` | Today | When you want the order shipped |
| `order.currency` | `EUR` | ISO currency code |
| `order.gatheringInstruction` | — | Free-text notes for warehouse staff |
| `order.orderType` | — | `ecommerce`, `b2b` |
| `parties` with role `buyer` | — | Billing/payer party (if different from shipTo) |
| `references[]` | — | Additional reference numbers (PO number, etc.) |
| `lines[].item.gtin` | — | EAN/GTIN barcode |
| `lines[].price` | — | Unit pricing |
| `lines[].item.sourceReferences` | — | Your system's internal IDs (see below) |

**Limitations:**
- Product SKU (`buyerItemNo`) must already exist in the warehouse system. Unknown SKUs cause the message to be rejected.
- Maximum ~1000 lines per order (practical limit).
- Orders cannot be modified after submission — send a new order or contact TANOMA operations for cancellation.

---

### 2. PurchaseOrder

Notify the warehouse about an expected incoming shipment from a supplier (receiving/goods-in).

```http
POST /webhook/mycompany/PurchaseOrder HTTP/1.1
Host: wipex.warasto.fi
Content-Type: application/json
X-Api-Key: your-api-key
```

```json
{
  "order": {
    "orderNumber": "PO-2026-050",
    "orderType": "purchase",
    "orderDate": "2026-06-01",
    "requestedDeliveryDate": "2026-06-10",
    "currency": "EUR",
    "incoterms": "DAP"
  },
  "parties": [
    {
      "role": "seller",
      "ids": { "GLN": "7312345678901" },
      "name": "Supplier AB",
      "address": {
        "street": "Leveransvägen 10",
        "city": "Stockholm",
        "postalCode": "11120",
        "countryCode": "SE"
      }
    },
    {
      "role": "buyer",
      "name": "My Company Oy",
      "address": {
        "street": "Tehtaankatu 5",
        "city": "Helsinki",
        "postalCode": "00140",
        "countryCode": "FI"
      }
    }
  ],
  "references": [
    { "type": "supplierOrderNumber", "value": "SUP-ORD-7890" }
  ],
  "lines": [
    {
      "lineNumber": 1,
      "item": {
        "identifiers": {
          "buyerItemNo": "SKU-001",
          "supplierItemNo": "SUP-PROD-100",
          "gtin": "6430012345678"
        },
        "description": "Product Name 500ml"
      },
      "orderQuantity": { "value": 500, "uom": "EA" },
      "price": {
        "unitPrice": 8.50,
        "currency": "EUR"
      }
    }
  ]
}
```

**Required fields:**

| Field | Required | Notes |
|-------|----------|-------|
| `order.orderNumber` | **Yes** | Your purchase order number |
| `order.orderDate` | **Yes** | ISO date |
| `parties` with role `seller` | **Yes** | Supplier identity |
| `lines[]` | **Yes** | At least one line |
| `lines[].item.identifiers.buyerItemNo` | **Yes** | Your product SKU |
| `lines[].orderQuantity.value` | **Yes** | Expected quantity |

**Optional fields:**

| Field | Notes |
|-------|-------|
| `order.requestedDeliveryDate` | Expected arrival date |
| `order.incoterms` | Delivery terms (DAP, FCA, etc.) |
| `lines[].item.identifiers.supplierItemNo` | Supplier's article number |
| `lines[].item.identifiers.gtin` | EAN barcode |
| `references[]` | Supplier order numbers, contract references |

**Limitations:**
- This registers an expected receiving — the actual goods receipt is handled by warehouse operations.
- Updating a PO: send a new PurchaseOrder with the same `orderNumber`. Wipex will process the latest version.

---

### 3. ASN (Advance Shipping Notice)

Notify the warehouse about an inbound shipment with package-level detail. Enables the warehouse to prepare for receiving with SSCC scanning.

```http
POST /webhook/mycompany/ASN HTTP/1.1
Host: wipex.warasto.fi
Content-Type: application/json
X-Api-Key: your-api-key
```

```json
{
  "shipment": {
    "shipmentNumber": "SHP-2026-001",
    "orderNumber": "PO-2026-050",
    "shipmentDate": "2026-06-05",
    "expectedArrivalDate": "2026-06-08",
    "despatchAdviceNumber": "DESADV-001"
  },
  "transport": {
    "carrier": "Posti",
    "carrierCode": "POSTI",
    "trackingNumber": "JJFI64300012345678",
    "transportMode": "road",
    "incoterms": "DAP"
  },
  "parties": [
    {
      "role": "supplier",
      "ids": { "GLN": "7312345678901" },
      "name": "Supplier AB",
      "address": {
        "street": "Leveransvägen 10",
        "city": "Stockholm",
        "postalCode": "11120",
        "countryCode": "SE"
      }
    }
  ],
  "references": [
    { "type": "purchaseOrderNumber", "value": "PO-2026-050" }
  ],
  "packages": [
    {
      "packageNumber": 1,
      "sscc": "006430012345678001",
      "packageType": "pallet",
      "grossWeight": 120.5,
      "grossWeightUnit": "KG",
      "dimensions": {
        "length": 120,
        "width": 80,
        "height": 150,
        "unit": "CM"
      },
      "items": [
        {
          "lineNumber": 1,
          "item": {
            "identifiers": {
              "buyerItemNo": "SKU-001",
              "supplierItemNo": "SUP-PROD-100",
              "gtin": "6430012345678"
            },
            "description": "Product Name 500ml"
          },
          "quantity": { "value": 48, "uom": "EA" },
          "batchNumber": "BATCH-2026-A",
          "expiryDate": "2027-12-01"
        }
      ]
    },
    {
      "packageNumber": 2,
      "sscc": "006430012345678002",
      "packageType": "box",
      "grossWeight": 8.2,
      "grossWeightUnit": "KG",
      "items": [
        {
          "lineNumber": 2,
          "item": {
            "identifiers": {
              "buyerItemNo": "SKU-003",
              "gtin": "6430012345680"
            },
            "description": "Accessory Item"
          },
          "quantity": { "value": 100, "uom": "EA" }
        }
      ]
    }
  ],
  "totals": {
    "packageCount": 2,
    "totalGrossWeight": 128.7,
    "totalGrossWeightUnit": "KG"
  }
}
```

**Required fields:**

| Field | Required | Notes |
|-------|----------|-------|
| `shipment.shipmentNumber` | **Yes** | Unique shipment identifier |
| `shipment.orderNumber` OR `references[].purchaseOrderNumber` | **Yes** | Must link to a known purchase order |
| `packages[]` | **Yes** | At least one package |
| `packages[].items[]` | **Yes** | At least one item per package |
| `packages[].items[].item.identifiers.buyerItemNo` | **Yes** | Your product SKU |
| `packages[].items[].quantity.value` | **Yes** | Must be > 0 |

**Optional fields:**

| Field | Notes |
|-------|-------|
| `packages[].sscc` | 18-digit SSCC code — enables barcode-based receiving |
| `packages[].packageType` | `pallet`, `box`, `bag`, `drum`, etc. |
| `packages[].grossWeight` | Weight of the package |
| `packages[].dimensions` | Physical dimensions of the package |
| `packages[].items[].batchNumber` | Required if the product uses batch tracking |
| `packages[].items[].expiryDate` | Required if the product uses expiry tracking (YYYY-MM-DD) |
| `transport` | Carrier and tracking details |
| `shipment.expectedArrivalDate` | When the shipment should arrive |

**Limitations:**
- SSCC must be a valid 18-digit code if provided.
- Batch and expiry tracking requirements depend on how the product is configured in the warehouse. If a product requires batch tracking, you must include `batchNumber` — otherwise the ASN will be rejected.
- One ASN = one physical shipment. If goods arrive in multiple shipments, send multiple ASNs.

---

### 4. ProductMaster

Create or update product data in the warehouse (identifiers, descriptions, dimensions, packaging, tracking requirements).

```http
POST /webhook/mycompany/ProductMaster HTTP/1.1
Host: wipex.warasto.fi
Content-Type: application/json
X-Api-Key: your-api-key
```

```json
{
  "action": "upsert",
  "products": [
    {
      "identifiers": {
        "buyerItemNo": "SKU-001",
        "supplierItemNo": "SUP-100",
        "gtin": "6430012345678",
        "gtinCase": "16430012345675"
      },
      "description": {
        "name": "Product Name 500ml",
        "shortName": "Prod 500ml",
        "description": "Full product description for warehouse labeling",
        "brand": "BrandName",
        "productGroup": "Pharmaceuticals",
        "productSubGroup": "OTC Medicines"
      },
      "dimensions": {
        "weight": 0.52,
        "weightUnit": "KG",
        "grossWeight": 0.55,
        "grossWeightUnit": "KG",
        "length": 5.0,
        "width": 5.0,
        "height": 15.0,
        "dimensionUnit": "CM",
        "volume": 0.000375,
        "volumeUnit": "M3"
      },
      "packaging": {
        "baseUnit": "EA",
        "caseSize": 12,
        "palletSize": 48,
        "palletLayers": 4,
        "casesPerLayer": 12
      },
      "tracking": {
        "batchTracking": true,
        "serialTracking": false,
        "expiryTracking": true,
        "expiryWarningDays": 90,
        "temperatureControlled": false
      },
      "storage": {
        "storageClass": "normal",
        "hazmat": false,
        "stackable": true,
        "fragile": false
      },
      "customs": {
        "countryOfOrigin": "SE",
        "commodityCode": "3004.90.00"
      },
      "status": {
        "active": true
      }
    }
  ]
}
```

**Required fields:**

| Field | Required | Notes |
|-------|----------|-------|
| `action` | **Yes** | `upsert` (create or update) or `deactivate` |
| `products[]` | **Yes** | At least one product |
| `products[].identifiers.buyerItemNo` | **Yes** | Your product SKU — the primary key for warehouse lookup |
| `products[].description.name` | **Yes** | Product name |

**Optional fields:**

| Field | Default | Notes |
|-------|---------|-------|
| `identifiers.gtin` | — | EAN-13 or EAN-14 barcode |
| `identifiers.supplierItemNo` | — | Supplier's article number |
| `identifiers.gtinCase` | — | Case-level barcode |
| `description.brand` | — | Brand name |
| `dimensions.weight` | 0 | Net weight per unit |
| `dimensions.grossWeight` | 0 | Gross weight per unit (including packaging) |
| `packaging.baseUnit` | `EA` | Unit of measure (EA, KG, L, etc.) |
| `packaging.caseSize` | — | How many base units per case/box |
| `packaging.palletSize` | — | How many base units per pallet |
| `tracking.batchTracking` | `false` | Whether the product must be tracked by batch number |
| `tracking.expiryTracking` | `false` | Whether the product has expiry dates |
| `tracking.expiryWarningDays` | — | Required if expiryTracking is true |
| `storage.hazmat` | `false` | Dangerous goods flag |
| `customs.countryOfOrigin` | — | ISO country code |
| `customs.commodityCode` | — | HS/CN customs tariff code |
| `status.active` | `true` | Set to `false` to deactivate the product |

**Limitations:**
- `buyerItemNo` is the primary lookup key. If you send a product with an existing `buyerItemNo`, it will be updated (upserted).
- Products must be created before they can be referenced in SalesOrders or PurchaseOrders. Unknown SKUs in orders will be rejected.
- You can send multiple products in one message (batch up to ~500 products per request).
- Dimension units must be consistent across all products (`KG` for weight, `CM` for dimensions, `M3` for volume).

---

### Source References (Optional)

If your system has internal identifiers that you need preserved for callbacks or reconciliation, include them in `sourceReferences` on each line item:

```json
{
  "lines": [
    {
      "lineNumber": 1,
      "item": {
        "identifiers": { "buyerItemNo": "SKU-001" },
        "description": "Product",
        "sourceReferences": [
          { "type": "erpOrderLineId", "value": "12345" },
          { "type": "shopifyVariantId", "value": "44012345678" }
        ]
      },
      "orderQuantity": { "value": 1, "uom": "EA" }
    }
  ]
}
```

These are stored for audit/traceability but do NOT affect warehouse processing.

---

## Receiving Data from Wipex (Outbound)

### How Outbound Delivery Works

1. You provide TANOMA with your webhook endpoint URL and an HMAC secret during onboarding
2. When events occur in the warehouse, Wipex sends an HTTP POST to your endpoint
3. Each request includes an HMAC-SHA256 signature for verification

**HTTP request from Wipex:**
```http
POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: base64-encoded-hmac-sha256
X-Webhook-Id: unique-delivery-id
```

### Verifying the Signature

Always verify the HMAC signature before processing:

```python
import hmac, hashlib, base64

def verify(request_body: bytes, signature_header: str, secret: str) -> bool:
    expected = base64.b64encode(
        hmac.new(secret.encode(), request_body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature_header)
```

```php
$expected = base64_encode(hash_hmac('sha256', $requestBody, $secret, true));
$valid = hash_equals($expected, $signatureHeader);
```

### Your Response

| Your response | Wipex behavior |
|---------------|----------------|
| `200 OK` | Delivery marked successful |
| `4xx` or `5xx` | Delivery marked failed, recorded in audit trail |
| Timeout (30s) | Delivery marked failed |

---

### 5. ShippingAdvice (Outbound)

Sent when the warehouse ships an order. Contains tracking information and batch/serial details per line.

**You will receive:**
```json
{
  "fulfillment": {
    "order_id": "ORD-2026-1042",
    "tracking_number": "JJFI12345678901234",
    "tracking_url": "https://tracking.example.com/JJFI12345678901234",
    "line_items": [
      {
        "line_item_id": "SKU-001",
        "fulfillments": [
          {
            "quantity": 2,
            "batch_number": "BATCH-2026-A",
            "expiry_date": "2027-12-01"
          }
        ]
      },
      {
        "line_item_id": "SKU-002",
        "fulfillments": [
          {
            "quantity": 3,
            "batch_number": "BATCH-2026-B"
          },
          {
            "quantity": 2,
            "batch_number": "BATCH-2026-C"
          }
        ]
      }
    ]
  }
}
```

**Field details:**

| Field | Description |
|-------|-------------|
| `fulfillment.order_id` | The order number you originally submitted |
| `fulfillment.tracking_number` | Carrier tracking number |
| `fulfillment.tracking_url` | Tracking URL (if available) |
| `line_items[].line_item_id` | Your product SKU |
| `line_items[].fulfillments[]` | Array of batch fulfillments for this line |
| `fulfillments[].quantity` | Quantity shipped from this batch |
| `fulfillments[].batch_number` | Batch/lot number |
| `fulfillments[].expiry_date` | Batch expiry date (if tracked) |

**Notes:**
- One order may generate multiple ShippingAdvice messages (partial shipments).
- A single line item may be fulfilled from multiple batches — each batch appears as a separate entry in `fulfillments[]`.
- `tracking_url` may be null if the carrier doesn't provide one.

---

### 6. InventoryBalance (Outbound)

Sent on a schedule (e.g., daily). Contains the current stock level for all your products in the warehouse.

**You will receive:**
```json
[
  {
    "sku": "SKU-001",
    "available_quantity": 150,
    "warehouse": "WH01"
  },
  {
    "sku": "SKU-002",
    "available_quantity": 0,
    "warehouse": "WH01"
  },
  {
    "sku": "SKU-003",
    "available_quantity": 42,
    "warehouse": "WH01"
  }
]
```

**Field details:**

| Field | Description |
|-------|-------------|
| `sku` | Your product SKU (`buyerItemNo`) |
| `available_quantity` | Current available stock (excludes reserved/blocked) |
| `warehouse` | Warehouse code |

**Notes:**
- This is a **full snapshot** — all your products are included, including those with 0 stock.
- Delivery frequency is configured per tenant (typically daily or multiple times per day).
- Use this to synchronize your e-commerce/ERP inventory levels.

---

### 7. InventoryAdjustment (Outbound)

Sent when inventory changes occur in the warehouse outside of normal order fulfillment (damage, correction, receipt, etc.).

**You will receive:**
```json
[
  {
    "sku": "SKU-001",
    "quantity_change": -2,
    "reason": "DAMAGE",
    "timestamp": "2026-06-01T10:30:00+03:00"
  },
  {
    "sku": "SKU-002",
    "quantity_change": 500,
    "reason": "RECEIPT",
    "timestamp": "2026-06-01T11:00:00+03:00"
  }
]
```

**Field details:**

| Field | Description |
|-------|-------------|
| `sku` | Your product SKU |
| `quantity_change` | Positive = stock increase, negative = stock decrease |
| `reason` | Reason code: `DAMAGE`, `RECEIPT`, `CORRECTION`, `RETURN`, `SHIPMENT`, etc. |
| `timestamp` | When the adjustment occurred |

**Notes:**
- Normal order shipments also generate adjustments with reason `SHIPMENT`.
- Adjustments are event-driven (sent as they happen), not batched.

---

## Polling Outbound Data from Wipex *(Future Development)*

If your system cannot receive webhooks (e.g., firewall restrictions, batch-oriented architecture), Wipex will provide REST endpoints where you can actively poll for outbound data.

> **Status:** These endpoints are planned but not yet implemented. During onboarding, discuss with TANOMA whether push (webhook) or poll (API) delivery suits your architecture.

### Planned Endpoints

```
GET https://wipex.warasto.fi/api/{tenantCode}/outbound/{docType}
GET https://wipex.warasto.fi/api/{tenantCode}/outbound/{docType}/{messageId}
```

**Authentication:** `X-Api-Key` header (same key as inbound).

### Planned Document Types for Polling

| `docType` | Description | Expected Behavior |
|-----------|-------------|-------------------|
| `ShippingAdvice` | Fulfillment confirmations | Returns unread fulfillments since last poll. You acknowledge receipt per message. |
| `InventoryBalance` | Current stock levels | Returns the latest inventory snapshot. |
| `InventoryAdjustment` | Inventory change events | Returns unacknowledged adjustment events. You acknowledge receipt per message. |

### Planned Flow

1. You call `GET /api/mycompany/outbound/ShippingAdvice` to list pending messages
2. Response includes an array of messages with `messageId` and payload
3. You process each message
4. You call `POST /api/mycompany/outbound/ShippingAdvice/{messageId}/ack` to acknowledge
5. Acknowledged messages are not returned in subsequent polls

### Planned Response Format

```json
{
  "messages": [
    {
      "messageId": "msg-abc123",
      "docType": "ShippingAdvice",
      "createdAt": "2026-06-01T10:05:00+03:00",
      "payload": { "...same structure as webhook delivery..." }
    }
  ],
  "hasMore": false
}
```

**Note:** The payload format for each document type is identical whether delivered via webhook push or API poll. See the outbound message type sections above for payload examples.

---

## Idempotency

Wipex prevents duplicate processing using **idempotency keys**:

- If you include a `webhook-id` header, that value is used as the idempotency key
- If not, Wipex generates a key from a SHA-256 hash of the request body

If you send the same message twice (same body or same `webhook-id`), the second submission is recorded as `duplicate` and **not processed again**. This makes it safe to retry on network errors.

**Recommendation:** Always include a `webhook-id` header with a unique value per message (e.g., your order ID or a UUID).

---

## Additional Delivery Channels and Formats *(Future)*

This guide describes the standard JSON/HTTPS webhook integration. Wipex is designed to support additional delivery channels and message formats:

**Delivery channels (planned):**
- **SFTP / SCP** — file-based integration for partners who cannot use webhooks (both inbound and outbound)
- **FTP** — legacy file-based integration where required

**Message formats (planned):**
- **EDIFACT** — UN/EDIFACT messages (ORDERS, DESADV, ORDRSP, INVOIC, etc.)
- **X12** — ANSI X12 EDI transactions (850, 856, 855, etc.)

**Custom integrations:** The document types and formats described in this guide represent the standard offering. Wipex can accommodate customer-specific processing requirements — custom field mappings, business rules, validation logic, or entirely custom message formats. Discuss your specific needs with TANOMA during onboarding.

---

## Onboarding Checklist

To integrate with Wipex, you need:

1. **Tenant code** — assigned by TANOMA during onboarding
2. **API key** — provided securely by TANOMA
3. **Agreed document types** — which message types you'll send and/or receive
4. **Product catalogue** — submit a ProductMaster before sending orders (products must exist in the warehouse)
5. **Webhook endpoint** (outbound only) — your HTTPS endpoint URL + HMAC shared secret

## Support

Contact TANOMA operations for:
- API key provisioning or rotation
- Adding new document types to your integration
- Investigating failed or rejected messages (provide the `requestId`)
- Onboarding new integration flows
