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

# Portal.io API Webhooks: Real-Time Event Notifications

> Subscribe to Portal.io webhook events to receive real-time HTTP POST notifications when proposals, payments, and orders change status in your account.

Webhooks let your system react to events in Portal.io without polling the API continuously. When a subscribed event occurs — such as a proposal being approved or an AI build completing — Portal.io sends an HTTP POST request to your configured endpoint with a JSON payload describing the event.

<Tip>
  Use webhook events alongside targeted API calls to keep your system in sync. For example, listen for `Proposal Status Changed` to trigger a workflow, then call `GET /public/proposals/{ProposalId}` to fetch the full updated proposal details.
</Tip>

## Available events

Portal.io fires webhooks for the following event types:

| Event name                        | Triggered when                                                                                                   |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `Proposal Build Status Changed`   | The AI Builder finishes (or fails) building a proposal from an approved outline.                                 |
| `Proposal Outline Status Changed` | The AI outline generation status changes — for example, when the outline moves from `Generating` to `Completed`. |
| `Proposal Status Changed`         | A proposal's status changes — for example, from `Draft` to `Sent`, or from `Sent` to `Approved`.                 |

In addition, Portal.io supports the following Zapier trigger events, which fire on the same underlying data changes:

| Zapier trigger         | Fires when                              |
| ---------------------- | --------------------------------------- |
| Person Modification    | A contact record is created or updated. |
| Payment Status Change  | A payment's status changes.             |
| Proposal Status Change | A proposal's status changes.            |
| Order Status Change    | An order's status changes.              |

## Subscribing to webhooks

Create a webhook subscription by calling `POST /public/webhook/subscribe` with your endpoint URL and the events you want to receive.

```bash theme={null}
curl -X POST https://api.portal.io/public/webhook/subscribe \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d Url=https://yourapp.example.com/portal/webhooks \
  -d Description="Production+webhook" \
  -d Events=proposal.status_changed
```

The response includes:

* `subscriptionId` — use this to update or delete the subscription later
* `secretKey` — use this to verify that incoming webhook requests are genuinely from Portal.io (see [Verifying webhook signatures](#verifying-webhook-signatures))
* `url`, `description`, `enabled`, and `events`

Your endpoint URL must use HTTPS and must be publicly routable. Portal.io rejects private, loopback, and link-local addresses.

## Managing subscriptions

| Operation              | Endpoint                                              |
| ---------------------- | ----------------------------------------------------- |
| Create subscription    | `POST /public/webhook/subscribe`                      |
| List all subscriptions | `GET /public/webhook/subscriptions`                   |
| Update a subscription  | `POST /public/webhook/subscription/{SubscriptionId}`  |
| Delete a subscription  | `DELETE /public/webhook/unsubscribe/{SubscriptionId}` |

## What your endpoint receives

When an event fires, Portal.io sends an HTTP POST to your endpoint with:

* `Content-Type: application/json`
* An `X-Webhook-Signature` header for verification (see below)
* A JSON body describing the event

Your endpoint must respond with a **2xx HTTP status code** to acknowledge receipt. If Portal.io does not receive a 2xx response, it retries the delivery on this schedule:

| Attempt   | Delay after previous failure |
| --------- | ---------------------------- |
| 1st retry | 1 minute                     |
| 2nd retry | 5 minutes                    |
| 3rd retry | 30 minutes                   |
| 4th retry | 2 hours                      |
| 5th retry | 12 hours                     |

## Verifying webhook signatures

Every webhook request includes an `X-Webhook-Signature` header so you can confirm the request came from Portal.io and has not been tampered with.

**Header format:**

```text theme={null}
X-Webhook-Signature: t=1710000000,v1=5f2b3e7a9d1c4f8e6b2a3c9d7f4e1a6b5c3d2f7e9a1b4c6d8e0f2a3b5c7d9e1
```

Where:

* `t` — Unix timestamp of when the webhook was generated
* `v1` — HMAC-SHA256 signature

**Signature calculation:**

```text theme={null}
signature = HMAC_SHA256(secretKey, t + "." + requestBody)
```

For example, if `t` is `1710000000` and the request body is `{"id":10025,"number":4123,...}`, the signed message is:

```text theme={null}
1710000000.{"id":10025,"number":4123,...}
```

**Verification steps:**

<Steps>
  <Step title="Read the raw request body">
    Use the raw bytes exactly as received — do not parse or normalize the JSON before verifying.
  </Step>

  <Step title="Extract the signature header values">
    Parse `X-Webhook-Signature` to extract `t` and `v1`.
  </Step>

  <Step title="Rebuild the signed message">
    Concatenate `t`, a literal `.`, and the raw request body.
  </Step>

  <Step title="Compute the expected signature">
    Run `HMAC_SHA256(secretKey, signedMessage)` using your subscription's `secretKey`.
  </Step>

  <Step title="Compare and validate">
    Compare your computed value with `v1`. Reject the request if they do not match, or if `t` is more than 5 minutes old.
  </Step>
</Steps>

## Zapier integration

Portal.io supports Zapier natively. If you are building automations with Zapier rather than a direct integration, the same underlying events — person modifications, payment status changes, proposal status changes, and order status changes — are available as Zapier triggers. Connect the Portal.io app in Zapier to configure these without writing any code.
