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

# Receive an event

> Receive a webhook from a connected third-party app (for example Recharge) and start any workflow whose **Webhook received** trigger matches it.

The URL you paste into the sending app carries its own credential, so this endpoint takes no `Authorization` header. Create a source in **Settings → Inbound events** to get the full URL; the token is shown once.

**Any app (secret URL)** sources need no signature: the URL is the credential, so keep it private. They accept JSON or `application/x-www-form-urlencoded` bodies and at most 60 requests a minute. Beyond that the answer is `429` while a workflow is listening on the source; with none listening, the event is accepted and dropped.

**Signed** sources must sign every request. For Recharge, that is the `X-Recharge-Hmac-Sha256` header computed with your **API Client Secret** — which is not the same value as your API token. An unsigned or mis-signed request returns `403 invalid_signature`.

**Redeliveries are safe.** Providers resend events, and Recharge's dunning fires one event per charge retry. Each workflow's `event_id_path` identifies the underlying object, so repeat deliveries of the same event fold onto the run that already exists rather than messaging the customer again.

A `200` means the event was accepted, not that it matched anything — check `runs_started`, and see the source's status in Settings when it is unexpectedly `0`.

### Receive an event

**Method:** `POST` | **Endpoint:** `/v1/events/{token}`

**Description:** The URL a connected app (Recharge, your own system, or a no-code tool such as Zapier or Make) posts its events to. An event that matches an active workflow's "Webhook received" trigger starts a run. See [Inbound events](https://docs.ibubbl.com/inbound-events/overview.md) for setup.

**Authentication:** No API key. The `token` in the path (`ibev_` followed by 64 hex characters, copied from the event source in the dashboard) identifies the source. A signed source must also send its signature header: Recharge sends `X-Recharge-Hmac-Sha256`; a generic signed source sends `X-iBubbl-Signature: t=<unix seconds>,v1=<hex>`, an HMAC-SHA256 of `{t}.{raw body}` with the source's signing secret, no more than 5 minutes old. A secret-URL source sends no signature and is limited to 60 requests a minute.

#### Request Body

| Field | Type | Required | Description |
| - | - | - | - |
| (the app's event) | object | Yes | The connected app's own payload, sent unchanged as JSON (or form-encoded), at most 256 KB. iBubbl doesn't require any fields; your workflow maps the ones it needs. The signature is checked against these exact bytes, so don't re-serialize the body. |

```bash cURL theme={null}
# No API key: the token in the URL identifies the source.
# Sign the exact bytes you send, with the source's signing secret.
BODY='{"topic": "order/created", "customer": {"phone": "+14155551234", "first_name": "Sam"}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$IBUBBL_EVENT_SECRET" | awk '{print $NF}')

curl -X POST "https://api.ibubbl.com/v1/events/$IBUBBL_EVENT_SOURCE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-iBubbl-Signature: t=$T,v1=$SIG" \
  --data-raw "$BODY"
```

```typescript TypeScript theme={null}
import { createHmac } from "node:crypto";

// No API key: the token in the URL identifies the source.
const token = process.env.IBUBBL_EVENT_SOURCE_TOKEN;
const secret = process.env.IBUBBL_EVENT_SECRET ?? "";

// Serialize once, then sign and send that same string.
const body = JSON.stringify({
  topic: "order/created",
  customer: { phone: "+14155551234", first_name: "Sam" },
});
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");

const res = await fetch(`https://api.ibubbl.com/v1/events/${token}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-iBubbl-Signature": `t=${t},v1=${v1}`,
  },
  body,
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.runs_started);
```

```python Python theme={null}
import hashlib
import hmac
import json
import os
import time

import requests

# No API key: the token in the URL identifies the source.
TOKEN = os.environ["IBUBBL_EVENT_SOURCE_TOKEN"]
SECRET = os.environ["IBUBBL_EVENT_SECRET"]

# Serialize once, then sign and send those same bytes.
body = json.dumps({
    "topic": "order/created",
    "customer": {"phone": "+14155551234", "first_name": "Sam"},
}).encode()
t = str(int(time.time()))
v1 = hmac.new(SECRET.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()

res = requests.post(
    f"https://api.ibubbl.com/v1/events/{TOKEN}",
    headers={"Content-Type": "application/json", "X-iBubbl-Signature": f"t={t},v1={v1}"},
    data=body,
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["runs_started"])
```

```json 200 OK theme={null}
{
  "received": true,
  "runs_started": 1
}
```

```json 403 Forbidden theme={null}
{
  "detail": {
    "error": "invalid_signature",
    "message": "Signature verification failed."
  }
}
```

#### Responses

**200 OK**

```json theme={null}
{
  "received": true,
  "runs_started": 1
}
```

Every request that passes the signature check gets 200, including one that starts no run (`runs_started: 0`), so the sending app never retries or disables the hook. The reason an event started nothing is recorded on the event source.

**403 Forbidden**

```json theme={null}
{
  "detail": {
    "error": "invalid_signature",
    "message": "Signature verification failed."
  }
}
```

The signature header is missing or doesn't match.

**404 Not Found**

```json theme={null}
{
  "detail": {
    "error": "not_found",
    "message": "No such event source."
  }
}
```

The token is malformed, unknown or revoked.

**413 Content Too Large**

```json theme={null}
{
  "detail": {
    "error": "payload_too_large",
    "message": "Request body too large."
  }
}
```

**429 Too Many Requests**

```json theme={null}
{
  "detail": {
    "error": "rate_limited",
    "message": "Too many events for this source; try again in a minute."
  }
}
```

Only a secret-URL source over 60 requests a minute while a workflow is listening on it. Without a listener, the extra events are dropped with 200.


## OpenAPI

````yaml POST /v1/events/{token}
openapi: 3.1.0
info:
  title: iBubbl API
  version: v1
  description: >-
    The iBubbl REST API. Send iMessages, manage contacts, and subscribe to
    webhook events from your backend.


    Authenticate every request with a Bearer API key (`ib_pk_<hex>`) — generate
    one from your dashboard's API Access settings. See the
    [Authentication](/authentication) guide.
servers:
  - url: https://api.ibubbl.com
security:
  - BearerAuth: []
tags:
  - name: Messages
  - name: Contacts
  - name: Conversations
  - name: Webhooks
  - name: Inbound Events
paths:
  /v1/events/{token}:
    post:
      tags:
        - Inbound Events
      summary: Receive an event from a connected app
      description: >-
        Receive a webhook from a connected third-party app (for example
        Recharge) and start any workflow whose **Webhook received** trigger
        matches it.


        The URL you paste into the sending app carries its own credential, so
        this endpoint takes no `Authorization` header. Create a source in
        **Settings → Inbound events** to get the full URL; the token is shown
        once.


        **Any app (secret URL)** sources need no signature: the URL is the
        credential, so keep it private. They accept JSON or
        `application/x-www-form-urlencoded` bodies and at most 60 requests a
        minute. Beyond that the answer is `429` while a workflow is listening on
        the source; with none listening, the event is accepted and dropped.


        **Signed** sources must sign every request. For Recharge, that is the
        `X-Recharge-Hmac-Sha256` header computed with your **API Client Secret**
        — which is not the same value as your API token. An unsigned or
        mis-signed request returns `403 invalid_signature`.


        **Redeliveries are safe.** Providers resend events, and Recharge's
        dunning fires one event per charge retry. Each workflow's
        `event_id_path` identifies the underlying object, so repeat deliveries
        of the same event fold onto the run that already exists rather than
        messaging the customer again.


        A `200` means the event was accepted, not that it matched anything —
        check `runs_started`, and see the source's status in Settings when it is
        unexpectedly `0`.
      operationId: receive_event_v1_events__token__post
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            title: Token
      responses:
        '200':
          description: Event accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventAck'
        '403':
          description: Signature missing or does not match.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: No such source, or it has been revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: Body larger than 256 KB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: >-
            Secret-URL source over its limit of 60 requests a minute, while a
            workflow is listening on it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
      security: []
components:
  schemas:
    EventAck:
      properties:
        received:
          type: boolean
          title: Received
          description: Always `true` once the signature has been verified.
          examples:
            - true
        runs_started:
          type: integer
          title: Runs Started
          description: >-
            How many workflow runs this event started. `0` is a normal answer —
            it means no active workflow's trigger matched, or this event was a
            redelivery that de-duplicated onto an existing run.
          examples:
            - 1
      type: object
      required:
        - received
        - runs_started
      title: EventAck
      example:
        received: true
        runs_started: 1
    ApiError:
      properties:
        detail:
          allOf:
            - $ref: '#/components/schemas/ApiErrorDetail'
          description: >-
            Error envelope. Every `/v1` error response wraps a stable `error`
            code and a human-readable `message` in `detail`.
      type: object
      required:
        - detail
      title: ApiError
      example:
        detail:
          error: validation_error
          message: >-
            Exactly one of contact_id, to, chat_guid, or participant_phones is
            required
    ApiErrorDetail:
      properties:
        error:
          type: string
          title: Error
          description: >-
            Stable machine-readable error code. Use this for branching logic in
            your integration; the human-readable `message` is subject to change.
            See the Error Reference for the catalog.
          examples:
            - validation_error
        message:
          type: string
          title: Message
          description: Human-readable description of what went wrong.
          examples:
            - >-
              Exactly one of contact_id, to, chat_guid, or participant_phones is
              required
      type: object
      required:
        - error
        - message
      title: ApiErrorDetail
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ib_pk_<64 hex>
      description: >-
        Bearer API key. Format: `Authorization: Bearer ib_pk_<64 hex>` (70
        characters). Generate keys from Dashboard → Settings → API Access.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.