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

# Webhooks

> Receive real-time notifications about account events — status changes, deposits, payouts, exchanges, and more.

![Webhooks — Vita Wallet Business API](https://mintcdn.com/vitawallet-c171bb38/4UkKy-vACfHm2ccw/api-reference/_assets/webhooks-hero.svg?fit=max\&auto=format\&n=4UkKy-vACfHm2ccw\&q=85\&s=a1733f36576274b0fe9448893b0bea6d)

Webhooks are the recommended way to receive real-time notifications from Vita about events in your account — transaction status changes, deposits, withdrawals, exchanges, and many more. Instead of polling the API to check for updates, your server receives an HTTP `POST` notification whenever something relevant happens.

This module gives you full control over:

> [card] Where to receive
>
> Configure your own webhook URL.

> [card] Which events
>
> Select from the available transaction categories.

> [card] Delivery monitoring
>
> Inspect recent events, delivery attempts, and your server's responses.

## Available endpoints

| Endpoint | Method | Purpose |
| - | - | - |
| Get Webhook Configuration | `GET` | Retrieve your current webhook URL, subscribed categories, and all categories available in the system. |
| Update Webhook Configuration | `PUT` | Set or update your webhook URL and the categories you want to receive. |
| Get Webhook Events | `GET` | Retrieve the 10 most recent events, including delivery status and server responses. |

## Authenticating requests to these endpoints

> [!WARNING]
>
> **Two distinct signatures.** This section covers the signature used when **you call Vita's webhook configuration endpoints**. It is **not** the same as the signature used to **verify webhooks Vita sends to you** — that one is covered below in *Webhook signature calculation*.

Every request to `GET /api/businesses/webhooks`, `PUT /api/businesses/webhooks`, and `GET /api/businesses/events` requires the following headers:

| Header | Description |
| - | - |
| `X-Login` | Your business login key. |
| `X-Trans-Key` | Your transaction key. |
| `X-Date` | ISO 8601 timestamp with timezone. Must be within **±1 minute** of the server clock, or the request is rejected. |
| `Authorization` | The request signature. Calculated differently from the webhook-verification signature — see the Business API authentication docs for the exact formula. |

Requests without these headers, or signed with the webhook-verification formula, will fail:

* No headers at all → `401`, generic error.
* Correct headers but wrong signature formula → `422`, `"Invalid signature"`.

> [!TIP]
>
> A ready-to-import Postman collection with correctly authenticated example requests is available alongside this documentation — use it instead of hand-building the examples below if you just want to test the endpoints quickly.

## Webhook signature calculation

Vita signs every webhook notification it sends to your server using an HMAC-SHA256 signature. The signature lets you verify that the request truly comes from Vita Business and that its content hasn't been modified in transit.

It's calculated using your `business_secret` and is included in the `Authorization` header alongside a timestamp in the `X-Date` header. Your server must reconstruct and validate this signature on every webhook delivery before processing the payload.

> [!WARNING]
>
> **Security-critical.** Never skip signature validation. An unverified webhook should be considered untrusted and rejected.

### Required credentials

| Credential | Description |
| - | - |
| `x_login` | Your business login key — a 40-character hexadecimal hash. |
| `x_date` | ISO 8601 timestamp with timezone (e.g., `2026-01-15T14:27:47+00:00`). |
| `business_secret` | Your private secret key used as the HMAC key. Never expose this value. |

### Signature calculation process

**Formula**

```
string_to_sign = x_login + x_date + JSON.stringify(data_hash)
signature      = HMAC-SHA256(business_secret, string_to_sign)
```

**1. Obtain x\_login**

The hexadecimal hash provided by Vita Business.

**2. Generate x\_date**

ISO 8601 timestamp with timezone.

**3. Build the data hash**

Use the event structure (see below).

**4. Convert to compact JSON**

No spaces, no line breaks.

**5. Concatenate directly**

`x_login + x_date + compact_json` — no separators.

**6. Calculate HMAC-SHA256**

Using your `business_secret` as the key.

**7. Send the signature**

In the `Authorization` header.

### Data hash structure

The data hash contains the following top-level fields:

```json theme={null}
{
  "event_type": "string",
  "event_id": "string (opaque identifier — see note below)",
  "created_at": "ISO8601 timestamp",
  "data": {
    /* event payload */
  }
}
```

> [!NOTE]
>
> **About `event_id`.** Despite its name and the UUID-like appearance in examples, this is an **opaque text identifier, not a validated UUID**. Most real values carry an extra numeric suffix (e.g. `f3065db9-1a97-493a-a8c2-97cbb84bf889-24544`). Do not validate its format — treat it as an opaque string.

### Worked example

**Inputs**

* `x_login`: `f2fe62688115b98e060533145ca6c32ec070bb9e`
* `x_date`: `2026-01-15T14:27:47+00:00`
* `business_secret`: `[your_secret_key]`

**Data hash** (regenerated from a real event, including previously undocumented fields)

```json theme={null}
{
  "event_type": "transaction.denied",
  "event_id": "c9a215f6-4e2c-47cb-9b60-1ace0a054359",
  "created_at": "2026-01-15T12:40:50Z",
  "data": {
    "id": "f3065db9-1a97-493a-a8c2-97cbb84bf889-24544",
    "order": "your-internal-order-reference",
    "total": "791500.0",
    "amount": "789000.0",
    "status": "denied",
    "category": "deposit",
    "currency": "clp",
    "fee_type": "amount",
    "statuses": [
      {
        "status": "denied",
        "created_at": "2026-01-15T12:40:50.601Z",
        "description": "That doesn't work",
        "transaction_operation_type": "updated"
      },
      {
        "status": "completed",
        "created_at": "2026-01-15T12:39:31.656Z",
        "description": null,
        "transaction_operation_type": "updated"
      }
    ],
    "fee_value": "2500.0",
    "total_fee": "2500.0",
    "created_at": "2026-01-14T21:02:24.930Z",
    "description": "Deposit 789000.0",
    "funding_source__user_name": null,
    "funding_source__user_document_type": null,
    "funding_source__user_document_number": null,
    "funding_source__user_email": null,
    "funding_source__account": null,
    "funding_source__ID_provider": null
  }
}
```

**Step 1 — Convert the data hash to compact JSON.** No spaces, no line breaks. Vita always sends the body already compact; those are the exact bytes that were signed. **Do not reformat, reorder, or re-serialize the body before validating — any change invalidates the signature.**

**Step 2 — Build the string to sign.** Direct concatenation, no separators:

```
{x_login}{x_date}{compact_json}
```

**Step 3 — Calculate HMAC-SHA256.** Key: `business_secret`. Message: the full string from step 2. Output: a 64-character hexadecimal signature.

**Step 4 — Final HTTP request headers:**

```http theme={null}
Content-Type: application/json
Authorization: V2-HMAC-SHA256, Signature: [calculated_64_char_signature]
X-Date: 2026-01-15T14:27:47+00:00
```

Body: the exact JSON object used to build the signature, as sent by Vita (always compact — this is not a stylistic choice on your end).

### How to validate an incoming signature

**1. Extract x\_date**

From the `X-Date` header.

**2. Use your x\_login**

Stored on your side.

**3. Take the received JSON body exactly as-is**

Do not modify, reformat, reorder, or re-serialize it.

**4. Reconstruct string\_to\_sign**

`x_login + x_date + body_json`.

**5. Calculate the expected signature**

HMAC-SHA256 with your `business_secret`.

**6. Compare**

With the one received in the `Authorization` header (after `V2-HMAC-SHA256, Signature:`).

**7. Accept or reject**

If the signatures match, the webhook is authentic. If they don't, reject the request.

### Important notes

* `x_login` is a hexadecimal hash (40 characters in the example above).
* The JSON of the data hash is always compact — no spaces, no line breaks.
* `x_date` must be ISO 8601 with timezone (e.g., `2026-01-15T14:27:47+00:00`).
* Concatenation is direct, with no separators: `x_login + x_date + json_string`.
* Authorization header format: `V2-HMAC-SHA256, Signature: {signature}`.
* The `x_date` used in the signature must match exactly the `X-Date` header sent in the request.
* The resulting signature is a 64-character hexadecimal hash.
* Use the body as received — never re-serialize the JSON before validating, as it may change byte-for-byte.

***

## API status reference

This reference describes all statuses used across Vita, organized by the entity they apply to:

* **Payment Orders** — used in Payins for collecting funds.
* **Payment Order Attempts** — individual payment trials within a Payment Order.
* **Transactions** — internal fund movements used in both Payins and Payouts.

Understanding these statuses is essential for correctly interpreting webhook events, building dashboards, and implementing reconciliation flows.

### Payins

#### Payment Order

A Payment Order is a payment request created to collect funds from a customer. It represents the entire payment lifecycle and can contain one or more attempts. Each Payment Order has:

* A unique identifier (`public_code` or `issue`).
* An expiration date.
* A final status reflecting the overall outcome of the collection process.

> [!NOTE]
>
> **Who can cancel a Payment Order.** You (the client) cannot cancel a Payment Order through the API — there is no cancel operation exposed. However, **Vita can cancel a Payment Order internally** for operational or compliance reasons. When that happens, you receive a `payment_order.cancelled` webhook and the order's final status becomes `cancelled`. Build your integration to handle this event and status explicitly — do not treat `cancelled` as an impossible or invalid state.

#### Payment Order Attempt

A Payment Order Attempt represents a single trial by the user to fulfill a Payment Order. Since users may run into issues during checkout — declined cards, insufficient funds, closed browser — each session or transaction try is logged as a separate Attempt.

This structure gives you a granular history: while several attempts might fail or be cancelled, the parent Payment Order remains valid until one attempt succeeds, the order is cancelled by Vita, or the overall time limit elapses.

**Understanding the relationship**

* A single Payment Order can contain multiple (N) Payment Order Attempts.
* A `payment_order_attempt.cancelled` event does not mean the payment link is cancelled — it only means that specific attempt failed.
* A user may start a new attempt after a failure, which could eventually move the Payment Order to `paid`.

**When to consider an order PAID**

An order is successfully settled when either of the following webhooks is received (matching the `public_code` or `issue` of the order):

* `payment_order.paid`
* `payment_order_attempt.paid`

**When to consider an order a terminal failure**

An order reaches a terminal, unsuccessful state when either of the following is received:

* `payment_order.expired` — the link's time limit elapsed without payment.
* `payment_order.cancelled` — Vita cancelled the order internally.

### Payment Order and Payment Order Attempt event payloads

> [!WARNING]
>
> **Subscription requirement.** `payment_order.*` and `payment_order_attempt.*` events are **only sent if you are subscribed to the `payment` category**. Being subscribed to `deposit` or other categories does not deliver these events. If you run a Payins integration based on payment links, make sure `payment` is included in your `categories` list — otherwise you will silently receive none of these notifications.

These payloads carry payer and payment-specific data (payer name, document, amount, currency, on-hold-related fields, spread, fixed cost, country code, and more) rather than the transaction-category attributes described further below. Match incoming events to your internal records using `public_code` or `issue`.

> [!NOTE]
>
> A full field-by-field reference for `payment_order.*` and `payment_order_attempt.*` payloads is pending — request the current schema from your integration contact if you need it before it's published here.

#### Payment Order statuses

| Status | Description |
| - | - |
| `pending` | Order created; payment confirmation not yet received (payment attempts may be in progress). |
| `paid` | Payment successfully credited. |
| `expired` | Order expired before payment (based on `expires_at` or business rules). |
| `cancelled` | Order cancelled by Vita internally, for operational or compliance reasons. Not triggerable by the client via the API. |
| `on_hold` | Retained for manual review or further verification. *(Whether Compliance will proactively contact you is pending confirmation from the Compliance team — treat it as unconfirmed until then.)* |
| `reimbursing` | Refund/reimbursement to the payer is currently in progress. |
| `reimbursed` | Refund/reimbursement successfully completed. |

#### Payment Order Attempt statuses

Payment Order Attempts use the same statuses as Payment Orders, applied to the specific attempt:

| Status | Description |
| - | - |
| `pending` | Attempt initiated; payment confirmation not yet received. |
| `paid` | Attempt successfully completed and credited. |
| `cancelled` | Attempt was cancelled (e.g., user abandoned checkout or payment was declined). |
| `expired` | Attempt expired due to inactivity or session timeout. |
| `on_hold` | Attempt retained for manual review. |
| `reimbursing` | Refund of this specific attempt is in progress. |
| `reimbursed` | Refund of this specific attempt completed. |

> [!NOTE]
>
> A `cancelled` attempt does not cancel the parent Payment Order. A new attempt may still be initiated.

### Transactions

Transactions track the internal movement of funds within Vita and are used in both Payins and Payouts (withdrawals, Vita Sent, crypto payouts, exchanges, etc.).

#### Transaction statuses

| Status | Description |
| - | - |
| `started` | Operation initialized. |
| `checking` | Under review. *(The specific claim that this means risk analysis or compliance review is pending confirmation — treat as unconfirmed until then.)* |
| `pending` | In process (validations, provider processing, or reconciliation). |
| `processed` | Internally processed: funds routed to destination. |
| `completed` | Funds successfully delivered to destination. |
| `denied` | Transaction rejected during processing. This applies broadly across transaction types (deposits can also be `denied`) — it is not limited to rejections by a destination bank. Review the `statuses` history and `description` fields for the specific rejection reason. |
| `failed` | Processing failure. |

### Status lifecycles at a glance

**Payment Order lifecycle**

```mermaid theme={null}
flowchart LR
  P[pending] -- attempt paid --> PAID[paid]
  P -- time limit expires --> EXP[expired]
  P -- cancelled by Vita --> CAN[cancelled]
  P -- manual review --> OH[on_hold]
  OH --> PAID
  OH --> EXP
  OH --> CAN
  OH --> REIM[reimbursing]
  REIM --> REIMD[reimbursed]
```

**Transaction lifecycle**

```mermaid theme={null}
flowchart LR
  S[started] --> C[checking] --> P[pending] --> PR[processed] --> COM[completed]
  PR --> DEN[denied]
  PR --> FAIL[failed]
```

### Important considerations

> [accordion] Webhooks drive status updates
>
> All status changes are communicated via Webhooks. Configure your webhook URL and category subscriptions to ensure you receive real-time updates.

> [accordion] Match by public\_code or issue
>
> For Payment Orders, always match incoming webhook events to your internal records using `public_code` or `issue`. Both `payment_order.*` and `payment_order_attempt.*` events relate to the same underlying order — and both require the `payment` category subscription.

> [accordion] A cancelled attempt is not a cancelled order
>
> This is one of the most common misinterpretations. A `payment_order_attempt.cancelled` event affects only that attempt — the parent order remains valid until it's paid, expired, or cancelled by Vita.

> [accordion] Final vs. intermediate statuses
>
> * **Final (terminal):** `paid`, `expired`, `cancelled`, `reimbursed`, `completed`, `denied`, `failed`, `time_out`.
> * **Intermediate:** `pending`, `checking`, `processed`, `started`, `on_hold`, `reimbursing`.
>
> Plan your integration logic to wait for a final status before considering an operation truly resolved.


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