> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vambe.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound Webhooks

> Push canonical events (orders, checkouts, fulfillment, products) to Vambe. Vambe ingests them into the same pipeline as native integrations.

## Overview

Your platform **pushes** events to Vambe as they happen. Each event is a canonical JSON
payload — you map your own data onto Vambe's shape, so there is no per-provider code on
Vambe's side. All inbound endpoints are:

```
POST https://api.vambe.me/api/public/apps/{appId}/events/{event}
```

| Event          | Path                     | Purpose                           |
| -------------- | ------------------------ | --------------------------------- |
| Order          | `/events/order`          | A paid/created order.             |
| Checkout       | `/events/checkout`       | An abandoned checkout to recover. |
| Fulfillment    | `/events/fulfillment`    | A shipment created/updated.       |
| Product upsert | `/events/product`        | Create or update a product.       |
| Product delete | `/events/product/delete` | Remove a product.                 |

## Authenticating your webhooks

Every request must be signed with your app's **signing secret** (the `signing_secret` from
[Create Your App](/docs/app-platform/create-your-app)).

Compute an HMAC-SHA256 (hex) over the string `{timestamp}.{rawBody}` and send:

| Header              | Value                                                          |
| ------------------- | -------------------------------------------------------------- |
| `x-vambe-timestamp` | Current time in epoch milliseconds.                            |
| `x-vambe-signature` | `HMAC_SHA256(signing_secret, "{timestamp}.{rawBody}")` as hex. |

<Warning>
  Sign and send the **exact same body bytes**. Vambe verifies the signature against the raw
  request body — re-serializing differently will fail verification. Requests older than
  **5 minutes** are rejected.
</Warning>

```javascript theme={null}
import crypto from 'crypto';

function postEvent(appId, event, payload, signingSecret) {
  const timestamp = Date.now().toString();
  const body = JSON.stringify(payload); // sign and send these exact bytes
  const signature = crypto
    .createHmac('sha256', signingSecret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  return fetch(`https://api.vambe.me/api/public/apps/${appId}/events/${event}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-vambe-timestamp': timestamp,
      'x-vambe-signature': signature,
    },
    body,
  });
}
```

A `2xx` response means the event was accepted. **Retry on any non-2xx** — Vambe is idempotent
on the entity's external id.

## The envelope

Every payload is an envelope with two optional routing fields plus the entity:

```json theme={null}
{
  "external_store_id": "store_12345",
  "event_id": "evt_abc",
  "order": { "...": "..." }
}
```

<ParamField path="external_store_id" type="string">
  Your identifier for the merchant store. Used to resolve the installation (see below).
</ParamField>

<ParamField path="event_id" type="string">
  Your event id. Advisory today; reserved for explicit de-duplication.
</ParamField>

### Resolving the installation

A single app can be installed by many merchants, so Vambe must map each webhook to the right
installation:

* **Multi-store:** send `external_store_id` matching the `external_id` Vambe captured from your
  [`account_info_url`](/docs/app-platform/oauth-connection). Vambe resolves by `(app, external_id)`.
* **Single installation:** if the app has exactly one installation, it resolves automatically
  even without `external_store_id`.

If no installation can be resolved, Vambe responds `404`.

## Order

`POST /api/public/apps/{appId}/events/order`

```json theme={null}
{
  "external_store_id": "store_12345",
  "order": {
    "external_id": "1001",
    "external_name": "#1001",
    "currency": "CLP",
    "total_price": 29990,
    "status_url": "https://myecommerce.com/orders/1001/status",
    "details_url": "https://admin.myecommerce.com/orders/1001",
    "customer": {
      "name": "Jane Doe",
      "phone": "+56912345678",
      "email": "jane@example.com"
    },
    "line_items": [
      { "external_product_id": "sku-001", "quantity": 2 }
    ],
    "metadata": { "note": "gift wrap" }
  }
}
```

<Note>
  The customer **phone** is the most important field: it is how Vambe links the order to the
  WhatsApp contact. Send it in E.164 format when possible. Products are matched by
  `external_product_id` against the catalog you synced.
</Note>

## Checkout (abandoned)

`POST /api/public/apps/{appId}/events/checkout`

```json theme={null}
{
  "external_store_id": "store_12345",
  "checkout": {
    "external_id": "chk_555",
    "external_name": "Checkout 555",
    "currency": "CLP",
    "total_price": 19990,
    "recover_url": "https://myecommerce.com/checkout/chk_555",
    "details_url": "https://admin.myecommerce.com/checkouts/chk_555",
    "customer": {
      "name": "Jane Doe",
      "phone": "+56912345678",
      "email": "jane@example.com"
    },
    "line_items": [
      { "external_product_id": "sku-001", "quantity": 1 }
    ]
  }
}
```

<Note>
  `customer.phone` and `recover_url` are required for recovery: Vambe resolves (or creates) the
  contact by phone and records the abandoned checkout with its recovery link.
</Note>

## Fulfillment

`POST /api/public/apps/{appId}/events/fulfillment`

```json theme={null}
{
  "external_store_id": "store_12345",
  "fulfillment": {
    "order_external_id": "1001",
    "fulfillment_external_id": "ful_900",
    "tracking_number": "TRACK123",
    "type": "delivery",
    "delivery_address": {
      "recipient_name": "Jane Doe",
      "phone": "+56912345678",
      "address_line_1": "Av. Siempre Viva 742",
      "address_line_2": null,
      "city": "Santiago",
      "region": "RM",
      "country": "CL",
      "postal_code": "8320000"
    }
  }
}
```

| Field                     | Notes                                                |
| ------------------------- | ---------------------------------------------------- |
| `order_external_id`       | Links the shipment to the order.                     |
| `fulfillment_external_id` | Unique id of this fulfillment (Vambe upserts on it). |
| `type`                    | `delivery` or `pickup` (defaults to `delivery`).     |

## Products

Products are **incremental** — each event affects only the product you send. Vambe never
deletes the rest of your catalog from a webhook.

### Upsert (create or update)

`POST /api/public/apps/{appId}/events/product`

```json theme={null}
{
  "external_store_id": "store_12345",
  "product": {
    "external_id": "sku-001",
    "name": "Premium Coffee Beans",
    "description": "Arabica from Colombia, medium roast.",
    "price": 12990,
    "currency": "CLP",
    "sku": "COF-001",
    "file_url": "https://cdn.myecommerce.com/coffee.jpg",
    "image_urls": ["https://cdn.myecommerce.com/coffee.jpg"],
    "metadata": [{ "key": "roast", "value": "medium" }]
  }
}
```

Vambe upserts by `(external_id, store)`: a new `external_id` is created, an existing one is
updated. Product embeddings (for AI search and recommendations) run automatically.

### Delete

`POST /api/public/apps/{appId}/events/product/delete`

```json theme={null}
{
  "external_store_id": "store_12345",
  "external_id": "sku-001"
}
```

This soft-deletes the product for that store.

<Note>
  Catalog over webhook is the **push** model. If you'd rather have Vambe **pull** your full
  catalog instead, that is handled by the `products_list` outbound capability — talk to the
  Vambe team about which model fits your platform.
</Note>
