# API — magento-orderwebhook

> **Last verified:** 2026-02-26 (Europe/London)
> **Owner:** Engineering

All externally consumed API behaviour must be defined in this document. Undocumented behaviour is undefined and unsupported.

---

## Outbound Webhook API (to NGauge)

This module does not expose an HTTP API. It **consumes** an external REST endpoint on the NGauge platform.

### Endpoint

| Attribute | Value |
|-----------|-------|
| URL | Configurable via admin: `order_webhook/general/api_url` |
| Default base URL | `https://ahshub.portalpeople.app` (`src/etc/config.xml`) |
| Method | `POST` |
| Content-Type | `application/json` |
| Authentication | `Authorization: Bearer {api_key}` |
| API key storage | Magento encrypted config (`Magento\Config\Model\Config\Backend\Encrypted`) |
| Timeout | Configurable: `order_webhook/general/timeout` (default: 30 seconds) |
| Retry | `order_webhook/general/retry_attempts` retries with exponential backoff `sleep(pow(2, $attempt))` |

---

### Webhook Payload

Payload is constructed by `OrderDataBuilder`. Field names are sent exactly as listed.

#### Top-Level Fields

| Field | Type | Notes |
|-------|------|-------|
| `event_type` | string | One of: `order_placed`, `order_updated`, `manual_resend`, `test_webhook` |
| `timestamp` | string | ISO 8601 datetime |
| `order_source` | string | Store base URL |
| `increment_id` | string | Magento order increment ID |
| `status` | string | Magento order status |
| `state` | string | Magento order state |
| `grand_total` | float | Order grand total |
| `subtotal` | float | Order subtotal |
| `tax_amount` | float | Tax amount |
| `shipping_amount` | float | Shipping amount |
| `discount_amount` | float | Discount amount |
| `currency_code` | string | Order currency code |

#### Customer Object

| Field | Type | Notes |
|-------|------|-------|
| `customer.email` | string | Customer email |
| `customer.firstname` | string | First name |
| `customer.lastname` | string | Last name |
| `customer.dob` | string\|null | Date of birth — **special-category personal data** |
| `customer.gender` | int\|null | Gender (Magento integer codes) — **personal data** |
| `customer.is_guest` | bool | Guest order flag |

#### Addresses

| Field | Type | Notes |
|-------|------|-------|
| `billing_address` | object | Street, city, region, postcode, country, telephone |
| `shipping_address` | object | Same structure as billing |

#### Payment Object

| Field | Type | Notes |
|-------|------|-------|
| `payment.method` | string | Payment method code |
| `payment.cc_last4` | string\|null | Last 4 digits of card |
| `payment.cc_type` | string\|null | Card type |
| `payment.cc_exp_month` | string\|null | Card expiry month |
| `payment.cc_exp_year` | string\|null | Card expiry year |

#### Items Array

Each entry in `items[]`:

| Field | Type | Notes |
|-------|------|-------|
| `sku` | string | Product SKU |
| `name` | string | Product name |
| `qty_ordered` | float | Quantity ordered |
| `price` | float | Unit price |
| `row_total` | float | Line total |
| `product_type` | string | Product type code |

#### Shipping Fields

| Field | Type | Notes |
|-------|------|-------|
| `shipping_method` | string | Magento shipping method code |
| `shipping_date` | string\|null | From magento-deliverydates module; null if not installed |
| `shipping_message` | string\|null | From magento-deliverydates module; null if not installed |

---

### NGauge Response

On success (HTTP 2xx), the response body is expected to contain:

| Field | Stored In | Notes |
|-------|-----------|-------|
| `ngauge_order_no` | `sales_order.ngauge_order_no` | Order number from NGauge |
| `ngauge_order_id` | `sales_order.ngauge_order_id` | Order ID from NGauge |
| `ngauge_order_link` | `sales_order.ngauge_order_link` | Deep-link URL |

`UNKNOWN — Requires Clarification`: Exact NGauge response schema not documented in source. Field mapping inferred from `WebhookSender::setNgaugeResponseFields()`.

On failure: the sender logs the error, retries up to `retry_attempts` times (exponential backoff), then exits. The order is **not** blocked.

---

### comment_only Mode

When `order_webhook/ngauge/comment_only = 1`:
- The NGauge response is added to the order as a history comment.
- `ngauge_order_no`, `ngauge_order_id`, `ngauge_order_link` are **not** written to DB.
- The admin "Send to NGauge" button continues to show (since `ngauge_order_id` is never set).

---

## Admin HTTP Routes (Inbound)

### Resend Webhook

| Attribute | Value |
|-----------|-------|
| Route | `/admin/order/resendwebhook/resend` |
| ACL | `PortalPeople_OrderWebhook::config` |
| Parameter | `order_id` (GET param) |
| Event type sent | `manual_resend` |
| Response | HTTP redirect to order view page |

---

## CLI Command

| Attribute | Value |
|-----------|-------|
| Command | `order:webhook:test [ORDER_ID]` |
| Registration | `/modules/magento-orderwebhook/src/etc/di.xml` → `TestWebhookCommand` |
| Behaviour | Shows masked config (api_key partially masked). Sends `test_webhook` event for the specified order. |
| Return | `Command::SUCCESS` (0) or `Command::FAILURE` (1) |

---

## Event Types

| Event Type | Trigger | Handler |
|------------|---------|---------|
| `order_placed` | `sales_order_place_after` (frontend orders only) | `OrderPlaceAfter` observer |
| `order_updated` | `sales_order_save_after` when status becomes `processing` | `OrderSaveAfter` observer |
| `manual_resend` | Admin resend UI | `ResendWebhook` controller |
| `test_webhook` | CLI command | `TestWebhookCommand` |

---

## Admin-Created Order Behaviour

`OrderPlaceAfter` skips admin-created orders (`empty($order->getRemoteIp())`). Admin orders are instead handled by `OrderSaveAfter` when the status changes to `processing`.

---

## Confirmed Facts

- The webhook endpoint URL and API key are configurable via Magento admin.
- API key is stored encrypted in `core_config_data`; decrypted at send time via `EncryptorInterface`.
- Retry uses exponential backoff: `sleep(pow(2, $attempt))` per attempt.
- A single `orderRepository->save()` persists both NGauge fields and the order history comment.
- Customer DOB and gender are included in the webhook payload.
- `shipping_date` and `shipping_message` are null if magento-deliverydates is absent.

---

## Unknowns

1. `UNKNOWN — Requires Clarification`: Full NGauge API response schema — field names confirmed only from code logic, not from NGauge API documentation.
2. `UNKNOWN — Requires Clarification`: Does NGauge expect any additional headers beyond `Authorization` and `Content-Type`?
3. `UNKNOWN — Requires Clarification`: Is there a NGauge-side idempotency key? Can duplicate `order_placed` events be safely re-sent?

---

## Evidence

- `/modules/magento-orderwebhook/src/Model/WebhookSender.php`
- `/modules/magento-orderwebhook/src/Model/OrderDataBuilder.php`
- `/modules/magento-orderwebhook/src/Observer/OrderPlaceAfter.php`
- `/modules/magento-orderwebhook/src/Observer/OrderSaveAfter.php`
- `/modules/magento-orderwebhook/src/Controller/Adminhtml/Order/ResendWebhook.php`
- `/modules/magento-orderwebhook/src/Console/Command/TestWebhookCommand.php`
- `/modules/magento-orderwebhook/src/etc/config.xml`
- `/modules/magento-orderwebhook/src/etc/di.xml`
- `/modules/magento-orderwebhook/src/etc/adminhtml/system.xml`
