# Envara Connect — Full Developer Documentation

> Single-file bundle. You can paste this entire document into an AI coding agent
> (Claude Code, Cursor, Lovable, Aider, Codex, etc.) and ask it to integrate
> Envara Connect into your application.
>
> Base URL: `https://bridge.envara.ae`
> Browse online: https://bridge.envara.ae/docs

---

## How to use this with an AI coding agent

1. In your dashboard at https://bridge.envara.ae/sites create a **site** (pick
   *Generic / Custom backend* if you're not on WooCommerce or Shopify). You'll be
   shown an **API key** (`X-Envara-Site-Key`) and a **webhook secret** once. Store
   both in your secret manager.
2. Paste this file into your agent's context with a prompt like:
   *"Using these Envara Connect docs, add a server-side helper that signs and
   POSTs an `order.created` event whenever our checkout succeeds. Store the
   site key and webhook secret as env vars `ENVARA_SITE_KEY` and
   `ENVARA_WEBHOOK_SECRET`."*
3. Approve a WhatsApp template in the dashboard (Templates page), then create a
   **binding** that maps your event to that template.

That's the entire integration loop. The rest of this document is reference.

---

## Page: Overview

Envara Connect turns any incoming event from your platform into a WhatsApp
template message, sent through Meta's WhatsApp Cloud API. One signed POST
request — we handle templates, recipient resolution, retries, delivery logs,
and the Meta integration.

### What you can do

- Send order confirmations, OTPs, appointment reminders, abandoned-cart nudges — anything.
- Plug in from any stack: WooCommerce, Shopify, Node, PHP, Python, Go, Ruby, mobile, Zapier, n8n.
- Design and submit WhatsApp templates from the dashboard (or let Eva, our AI, build them).
- Map any field in your payload to any template variable, with fallbacks and helpers.
- Get delivery status logged for every message.

### How it works

1. Create a **site** in your dashboard. You get an `API key` and a `webhook secret`.
2. From your app, POST any event to `/api/public/ingest/{source}/{event}` with an HMAC signature.
3. In the dashboard, create a **binding**: pick the event, pick a WhatsApp template, map variables.
4. Every matching event automatically fires the template.

### Base URL

All requests go to: `https://bridge.envara.ae`

---

## Page: Quickstart

Send your first WhatsApp message in 5 minutes. You'll need a WhatsApp Business
number connected to your Envara account (done once, in Settings).

### 1. Create a site

In the dashboard, go to **Sites → Add site**. Pick **Generic / Custom backend**
as the platform. You'll be shown an `API key` and `webhook secret` exactly once
— copy both into your app's secret manager.

### 2. Sign and send an event

Pick any event name (lowercase, dotted: `resource.action`). Build the JSON
body. Compute an HMAC-SHA256 signature of the raw body using your webhook
secret. Include the headers below.

```bash
SITE_KEY="ec_live_xxxxxxxx..."
SECRET="ecs_xxxxxxxx..."
BODY='{"event":"order.created","order":{"id":"1","number":"1042","total":"199.00","currency":"AED","customer":{"first_name":"Ada","phone_e164":"+9715XXXXXXXX"}}}'
TS=$(date +%s)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST https://bridge.envara.ae/api/public/ingest/generic/order.created   -H "Content-Type: application/json"   -H "X-Envara-Site-Key: $SITE_KEY"   -H "X-Envara-Signature: sha256=$SIG"   -H "X-Envara-Timestamp: $TS"   -d "$BODY"
```

### 3. Check that we received it

Open **Sites → your site**. The event should appear in the live event feed within seconds.

### 4. Wire a WhatsApp template

Open **Templates**. Create or pick an approved WhatsApp template (or ask Eva
to draft one for you). Then in **Notifications**, create a binding: pick your
event (`order.created`), pick the template, map each `{{1}}` slot to a path in
your payload (e.g. `order.customer.first_name`). Save.

### 5. Trigger again — get a WhatsApp message

Re-send the same request. This time the binding fires and the customer's phone
receives the templated message.

### Test without sending

Set `X-Envara-Mode: test` on any request to run validation + binding resolution
and return a preview of what would be sent, without calling Meta or charging
you. Great for development.

---

## Page: API keys

API keys are issued **per site** from the dashboard. There is no global
account-level key.

### Getting a key

1. Sign in at https://bridge.envara.ae/login
2. Go to **Sites → Add site**
3. Pick a name and URL, choose your source type (`woocommerce`,
   `shopify`, or `generic`)
4. On success you'll see two values **once**:
   - **API key** — used as the `X-Envara-Site-Key` header
   - **Webhook secret** — used to sign request bodies (HMAC-SHA256)

Copy both immediately into your secret manager. The dashboard cannot show them again.

### Lost the key or secret?

- Delete and recreate the site to issue a fresh API key.
- Rotate the webhook secret from the Site detail page (this invalidates the old secret immediately).

### Using the credentials

```bash
export ENVARA_SITE_KEY="ec_live_..."
export ENVARA_WEBHOOK_SECRET="ecs_..."
```

Never ship these to a browser. Sign and send from your server, edge function, or backend worker.

---

## Page: Authentication

Every request to the ingest API must include three headers: a site API key, an
HMAC signature, and a timestamp. We validate all three before processing the body.

### Headers

| Header | Required | Description |
| --- | --- | --- |
| `X-Envara-Site-Key` | Yes | Your site's API key (starts with `ec_live_`). Identifies which site is sending. |
| `X-Envara-Signature` | Yes | `sha256=<hex>` — HMAC-SHA256 of the raw body, using your webhook secret as key. |
| `X-Envara-Timestamp` | Recommended | Unix seconds. Requests outside ±5 minutes are rejected. |
| `X-Envara-Idempotency-Key` | Optional | Your own unique ID. If the same key arrives twice within 5 minutes for the same site+event, the second is treated as a duplicate. |
| `X-Envara-Mode` | Optional | Set to `test` to dry-run: validate, resolve bindings, return preview, never send to Meta. |

### Computing the signature

The signature is an HMAC-SHA256 of the **raw request body bytes**, lowercase
hex, prefixed with `sha256=`. Sign the body *before* any framework reformats it.

**Node**
```javascript
import { createHmac } from "crypto";
const body = JSON.stringify(payload);
const signature = "sha256=" + createHmac("sha256", WEBHOOK_SECRET)
  .update(body)
  .digest("hex");
```

**PHP**
```php
<?php
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$signature = 'sha256=' . hash_hmac('sha256', $body, $webhookSecret);
```

**Python**
```python
import hmac, hashlib, json
body = json.dumps(payload, separators=(",", ":"))
signature = "sha256=" + hmac.new(
    webhook_secret.encode(), body.encode(), hashlib.sha256
).hexdigest()
```

### Why timestamps

Including `X-Envara-Timestamp` (current unix seconds) prevents replay attacks.
We reject any request more than 5 minutes off from server time. Always use
server time, not client time.

### Idempotency

If you might retry a request after a network blip, pass
`X-Envara-Idempotency-Key` with a unique value per logical event
(e.g. `order:1042:created`). Duplicates are silently deduplicated and return
the same result as the original.

### Common errors

- `401 Missing site key` — header not sent.
- `401 Invalid site key` — key doesn't match any site.
- `401 Invalid signature` — recompute over the exact body bytes you sent.
- `401 Stale timestamp` — clock drift > 5 min. Sync your server's NTP.
- `403 Site is disabled` — re-enable the site in the dashboard.

---

## Page: Event envelope

An event is any JSON object you POST to `/api/public/ingest/{source}/{event}`.
The shape is flexible — we walk the payload and learn every field — but
following the conventions below means existing templates and bindings work out
of the box.

### Naming

Use lowercase dotted names: `resource.action`. Examples: `order.created`,
`order.shipped`, `user.signup`, `appointment.reminder`, `payment.received`,
`cart.abandoned`. `{source}` identifies the platform (`woocommerce`,
`shopify`, `generic`, or anything you choose).

### Canonical envelope

```json
{
  "event": "order.created",
  "test": false,
  "site": { "name": "Acme", "url": "https://acme.com" },
  "order": {
    "id": "12345",
    "number": "1042",
    "status": "processing",
    "currency": "AED",
    "total": "199.00",
    "items_count": 2,
    "items": [
      { "name": "T-shirt", "qty": 2, "price": "99.50" }
    ],
    "customer": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "ada@example.com",
      "phone_e164": "+9715XXXXXXXX"
    },
    "order_url_customer": "https://acme.com/order/1042"
  },
  "data": {
    "any_custom_field": "any_value"
  }
}
```

### Standard fields

**Customer (universal)**
- `customer.first_name`, `customer.last_name`, `customer.full_name`
- `customer.email`
- `customer.phone_e164` — **required** if you want to target the customer's WhatsApp.

**Order**
- `order.id`, `order.number`, `order.status`
- `order.total`, `order.currency`, `order.items_count`
- `order.items[]` — array with `name`, `qty`, `price`
- `order.order_url_customer` — link the customer should follow
- `order.tracking_number`, `order.tracking_carrier`

**Cart**
- `cart.id`, `cart.total`, `cart.currency`
- `cart.recovery_url`

**Custom data (`data.*`)** — Anything you put in `data` is available in
template variables via dotted paths (`data.code`, `data.starts_at`, etc.).

### Phone format

Phone numbers must be E.164: a leading `+`, country code, and digits.
Example: `+919876543210`. Numbers in other formats are silently dropped.

### Test events

Set `"test": true` in the body, or send to event name `test`, to route the
message to your site's configured test recipient phone instead of the customer.

---

## Page: Endpoints

All public endpoints live under `https://bridge.envara.ae/api/public`.

### POST /api/public/ingest/{source}/{event}

The single endpoint you'll use 99% of the time. Accepts any signed event for
any source/event combination.

**Path parameters**
- `{source}` — platform identifier (`woocommerce`, `shopify`, `generic`, or your own)
- `{event}` — dotted event name (`order.created`, `user.signup`, etc.)

**Headers**
```
Content-Type: application/json
X-Envara-Site-Key: ec_live_xxxxxxxx...
X-Envara-Signature: sha256=<hex hmac>
X-Envara-Timestamp: 1731234567
X-Envara-Idempotency-Key: order:1042:created   # optional
X-Envara-Mode: test                            # optional: dry-run
```

**Response — 200 OK**
```json
{
  "ok": true,
  "event": "order.created",
  "event_id": "8a3f...c1",
  "order_id": "uuid-or-null",
  "whatsapp": "sent",
  "bindings_run": 1,
  "recipients_sent": 1,
  "recipients_failed": 0
}
```

`whatsapp` is one of: `sent`, `partial`, `failed`, `no_bindings`,
`skipped_no_phone`, `skipped_no_config`.

**Dry-run response (`X-Envara-Mode: test`)**
```json
{
  "ok": true,
  "event": "order.created",
  "event_id": "8a3f...c1",
  "whatsapp": "skipped_no_config",
  "bindings_run": 1,
  "dry_run": true,
  "previews": [
    {
      "binding_id": "uuid",
      "template": "order_confirmation_v1",
      "language": "en",
      "recipients": ["+9715XXXXXXXX"],
      "slots": {
        "header": ["Acme"],
        "body": ["Ada", "1042", "AED 199.00"],
        "buttonUrl": []
      },
      "resolved": true
    }
  ]
}
```

### GET /api/public/health

Returns `200` with a small JSON body. Use it for uptime probes — no auth required.

### GET /api/public/docs/bundle

Returns this same Markdown file as an attachment for one-shot AI agent ingestion.

---

## Page: Code recipes

Drop these into your codebase. Replace placeholders with your own site key and
webhook secret (kept in your secret manager — never client-side).

### Universal helper

**Node.js**
```javascript
import { createHmac } from "crypto";

const SITE_KEY = process.env.ENVARA_SITE_KEY;
const SECRET = process.env.ENVARA_WEBHOOK_SECRET;

export async function sendEnvaraEvent(source, event, payload) {
  const body = JSON.stringify(payload);
  const ts = Math.floor(Date.now() / 1000);
  const sig = createHmac("sha256", SECRET).update(body).digest("hex");

  const res = await fetch(`https://bridge.envara.ae/api/public/ingest/${source}/${event}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Envara-Site-Key": SITE_KEY,
      "X-Envara-Signature": `sha256=${sig}`,
      "X-Envara-Timestamp": String(ts),
    },
    body,
  });
  return res.json();
}
```

**PHP**
```php
<?php
function sendEnvaraEvent(string $source, string $event, array $payload): array {
  $body = json_encode($payload, JSON_UNESCAPED_SLASHES);
  $sig  = hash_hmac('sha256', $body, getenv('ENVARA_WEBHOOK_SECRET'));
  $ts   = (string) time();

  $ch = curl_init("https://bridge.envara.ae/api/public/ingest/$source/$event");
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
      'Content-Type: application/json',
      'X-Envara-Site-Key: ' . getenv('ENVARA_SITE_KEY'),
      'X-Envara-Signature: sha256=' . $sig,
      'X-Envara-Timestamp: ' . $ts,
    ],
  ]);
  $resp = curl_exec($ch);
  curl_close($ch);
  return json_decode($resp, true);
}
```

**Python**
```python
import os, hmac, hashlib, json, time, requests

SITE_KEY = os.environ["ENVARA_SITE_KEY"]
SECRET   = os.environ["ENVARA_WEBHOOK_SECRET"]

def send_envara_event(source: str, event: str, payload: dict) -> dict:
    body = json.dumps(payload, separators=(",", ":"))
    sig  = hmac.new(SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
    r = requests.post(
        f"https://bridge.envara.ae/api/public/ingest/{source}/{event}",
        data=body,
        headers={
            "Content-Type": "application/json",
            "X-Envara-Site-Key": SITE_KEY,
            "X-Envara-Signature": f"sha256={sig}",
            "X-Envara-Timestamp": str(int(time.time())),
        },
        timeout=15,
    )
    return r.json()
```

**Go**
```go
package envara

import (
  "bytes"
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "encoding/json"
  "fmt"
  "net/http"
  "os"
  "strconv"
  "time"
)

func Send(source, event string, payload any) (*http.Response, error) {
  body, _ := json.Marshal(payload)
  mac := hmac.New(sha256.New, []byte(os.Getenv("ENVARA_WEBHOOK_SECRET")))
  mac.Write(body)
  sig := hex.EncodeToString(mac.Sum(nil))

  url := fmt.Sprintf("https://bridge.envara.ae/api/public/ingest/%s/%s", source, event)
  req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  req.Header.Set("Content-Type", "application/json")
  req.Header.Set("X-Envara-Site-Key", os.Getenv("ENVARA_SITE_KEY"))
  req.Header.Set("X-Envara-Signature", "sha256="+sig)
  req.Header.Set("X-Envara-Timestamp", strconv.FormatInt(time.Now().Unix(), 10))
  return http.DefaultClient.Do(req)
}
```

**Ruby**
```ruby
require 'net/http'
require 'openssl'
require 'json'

def send_envara_event(source, event, payload)
  body = payload.to_json
  sig  = OpenSSL::HMAC.hexdigest('sha256', ENV['ENVARA_WEBHOOK_SECRET'], body)
  uri  = URI("https://bridge.envara.ae/api/public/ingest/#{source}/#{event}")

  req = Net::HTTP::Post.new(uri, {
    'Content-Type'         => 'application/json',
    'X-Envara-Site-Key'    => ENV['ENVARA_SITE_KEY'],
    'X-Envara-Signature'   => "sha256=#{sig}",
    'X-Envara-Timestamp'   => Time.now.to_i.to_s,
  })
  req.body = body
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
end
```

### Example: order created
```javascript
await sendEnvaraEvent("generic", "order.created", {
  event: "order.created",
  order: {
    id: "12345",
    number: "1042",
    total: "199.00",
    currency: "AED",
    items_count: 2,
    customer: { first_name: "Ada", phone_e164: "+9715XXXXXXXX" },
    order_url_customer: "https://acme.com/order/1042",
  },
});
```

### Example: OTP / verification
```javascript
await sendEnvaraEvent("generic", "user.verification", {
  event: "user.verification",
  customer: { first_name: "Ada", phone_e164: "+9715XXXXXXXX" },
  data: { code: "938472", expires_in: "5 minutes" },
});
```

### Example: appointment reminder
```python
send_envara_event("generic", "appointment.reminder", {
  "event": "appointment.reminder",
  "customer": { "first_name": "Ada", "phone_e164": "+9715XXXXXXXX" },
  "data": { "starts_at": "Tomorrow, 3:00 PM", "location": "Clinic, Dubai Marina" },
})
```

### Example: abandoned cart
```php
sendEnvaraEvent('generic', 'cart.abandoned', [
  'event'    => 'cart.abandoned',
  'cart'     => [
    'id'           => 'cart_abc',
    'total'        => '149.00',
    'currency'     => 'AED',
    'items_count'  => 3,
    'recovery_url' => 'https://acme.com/cart/restore/abc',
  ],
  'customer' => [ 'first_name' => 'Ada', 'phone_e164' => '+9715XXXXXXXX' ],
]);
```

---

## Page: Bindings & templates

A **binding** tells Envara: *"When event X arrives for site Y, send WhatsApp
template T to recipient R, filling each slot with these values."*

### Recipient strategies

- `customer` — uses `customer.phone_e164` from the payload.
- `site_recipients` — sends to all active phones in the site's recipient list (good for internal alerts).
- `static_phone` — a fixed E.164 number you type in.
- `field_path` — pulls the phone from any dotted path in the payload (e.g. `data.assignee_phone`).
- `test` — uses the site's test recipient.

### Variable map

WhatsApp templates have positional slots like `{{1}}`, `{{2}}` in header /
body / button-url. A variable map tells Envara what to put in each slot.

```json
{
  "header": {
    "1": { "source": "field", "path": "site.name" }
  },
  "body": {
    "1": { "source": "field", "path": "order.customer.first_name", "fallback": "Customer" },
    "2": { "source": "field", "path": "order.number" },
    "3": { "source": "expression", "expr": "total.with_currency" }
  },
  "button_url": {
    "1": { "source": "field", "path": "order.order_url_customer" }
  }
}
```

### VarSpec shapes

- `{ source: "field", path: "...", fallback?: "..." }` — pulls a dotted path from the payload.
- `{ source: "static", value: "..." }` — fixed string.
- `{ source: "expression", expr: "...", fallback?: "..." }` — built-in helper.

### Expressions

- `customer.full_name` — first + last name.
- `items.summary_short` — `name x qty, name x qty…`
- `items.summary_lines` — line-separated with prices.
- `total.with_currency` — `AED 199.00`
- `date.today`, `date.now`

### Template approval

WhatsApp templates must be approved by Meta before they can be sent. Drafts
live in the dashboard Templates page. Click **Submit to Meta** to request
approval. Status (approved / rejected / paused) is polled hourly.

---

## Page: Errors

Errors are returned as JSON with an `error` string and the appropriate HTTP
status. Bodies are stable — safe to match on.

### Authentication errors (401 / 403)

| Status | Body | Meaning |
| --- | --- | --- |
| 401 | `{"error":"Missing site key"}` | You didn't send `X-Envara-Site-Key`. |
| 401 | `{"error":"Invalid site key"}` | Key doesn't match any site. |
| 401 | `{"error":"Invalid signature"}` | HMAC mismatch. You likely signed a different body than you sent. |
| 401 | `{"error":"Stale timestamp"}` | Clock drift > 5 minutes. Sync NTP on your server. |
| 403 | `{"error":"Site is disabled"}` | Re-enable the site from the dashboard. |

### Server errors (5xx)

| Status | Body | Meaning |
| --- | --- | --- |
| 500 | `{"error":"<message>"}` | Unexpected — retry with exponential backoff. |

### Successful responses with no send

An event can be accepted (200 OK) without any WhatsApp message going out.
Check the `whatsapp` field:

- `no_bindings` — no binding configured for this site+event. Create one in the dashboard.
- `skipped_no_phone` — no recipient phone could be resolved.
- `skipped_no_config` — WhatsApp Cloud credentials missing in Settings.

### Per-recipient failures

If some recipients succeed and others fail, the response returns
`"whatsapp": "partial"` with `recipients_sent` and `recipients_failed` counts.
Per-message error details (Meta error code, reason) are visible in the
Notification Log.

### Retries & idempotency

If your request fails with a 5xx or network error, retry — but include
`X-Envara-Idempotency-Key` with a stable per-event value to make sure
duplicates are deduplicated server-side.

---

## Page: Outbound webhooks (preview)

**Coming soon.** Today, message delivery status is available in your dashboard
Notification Log. We're shipping outbound webhooks so your backend can be
notified the moment a WhatsApp send succeeds, fails, or gets a delivery / read
receipt from Meta.

Planned payload:
```json
{
  "type": "message.delivered",
  "event_id": "8a3f...c1",
  "site_id": "uuid",
  "binding_id": "uuid",
  "template": "order_confirmation_v1",
  "to_phone": "+9715XXXXXXXX",
  "wa_message_id": "wamid.HBgM...",
  "status": "delivered",
  "occurred_at": "2026-05-18T12:34:56Z"
}
```

Types: `message.sent`, `message.delivered`, `message.read`, `message.failed`.

Outbound webhooks will be signed with the same HMAC scheme as inbound, using a
per-site outbound secret you can rotate from the Site detail page.

---

## Page: Changelog

**2026-05-18**
- Generic source type. The ingest endpoint now officially supports any platform.
- Idempotency keys via `X-Envara-Idempotency-Key`.
- Dry-run mode via `X-Envara-Mode: test`.
- Webhook secret reveal on site creation.
- Public developer docs and single-file Markdown bundle.

**Earlier**
- WooCommerce plugin v2 (generic ingest endpoint).
- Per-event bindings with variable maps and expressions.
- Field discovery from incoming payloads.
- Eva AI assistant for templates, bindings, and diagnostics.

---

*Need help? Email support@envara.ae — include your `event_id` for fastest triage.*
