# Webhooks

Rupi sends webhook events to your registered endpoint for all asynchronous state changes.

#### Configuration

Register webhook endpoints in the Rupi dashboard under **Settings → Webhooks**. Multiple endpoints can be registered. Each endpoint can be scoped to specific event types.

#### Signature Verification

Every webhook request includes a `Rupi-Signature` header. The signature is an HMAC-SHA256 of the raw request body, computed using your webhook secret.

**Verification example (Node.js)**

javascript

```javascript
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}
```

#### Event Types

| Event                               | Description                                                                              |
| ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `connection.wallet.created`         | A wallet connection was successfully established.                                        |
| `connection.platform.created`       | A platform connection (neobank, exchange, payroll, wallet) was successfully established. |
| `connection.revoked`                | A connection was revoked by the user or expired.                                         |
| `vip.generating`                    | A VIP report has started generating.                                                     |
| `vip.verified`                      | A VIP report was generated and all periods were verified.                                |
| `vip.partially_verified`            | A VIP report was generated with one or more unverified periods.                          |
| `vip.failed`                        | VIP generation failed. See `error_code`.                                                 |
| `income.change_detected`            | Verified monthly income changed by more than 10% vs. previous period.                    |
| `fraud.flagged`                     | A transaction was flagged during the fraud filter pass.                                  |
| `risk_score.generated`              | A new risk score was computed.                                                           |
| `payment_intent.created`            | A payment intent was created.                                                            |
| `payment_intent.approval_confirmed` | ERC-20 approval was detected on-chain.                                                   |
| `payment_intent.expired`            | A payment intent expired before approval.                                                |
| `charge.succeeded`                  | A disbursement charge was successfully executed on-chain.                                |
| `charge.failed`                     | A disbursement charge failed.                                                            |

#### Event Payload Structure

All webhook payloads follow this envelope:

json

```json
{
  "id": "evt_5UpJ...",
  "type": "vip.verified",
  "created_at": "2024-12-01T09:00:00Z",
  "api_version": "v1",
  "data": {
    "object": { }
  }
}
```

The `data.object` contains the full resource that triggered the event, using the same schema as the corresponding API response.

**Example: vip.verified**

json

```json
{
  "id": "evt_5UpJ...",
  "type": "vip.verified",
  "created_at": "2024-12-01T09:00:00Z",
  "api_version": "v1",
  "data": {
    "object": {
      "id": "vip_4DrM...",
      "user_id": "usr_01J2K...",
      "status": "verified",
      "verified_monthly_income_usd": 4500.00,
      "periods_verified": 6,
      "consistency_rate": 1.0,
      "generated_at": "2024-12-01T09:00:00Z"
    }
  }
}
```

**Example: fraud.flagged**

json

```json
{
  "id": "evt_9WxB...",
  "type": "fraud.flagged",
  "created_at": "2024-12-01T09:05:00Z",
  "api_version": "v1",
  "data": {
    "object": {
      "user_id": "usr_01J2K...",
      "wallet_id": "wlt_7TnQ...",
      "tx_hash": "0xwash_trade_txhash...",
      "fraud_type": "wash_trade",
      "amount_usd": 5000.00,
      "flagged_at": "2024-12-01T09:05:00Z",
      "details": "Transaction flagged as wash trade: funds returned to origin address within 4 blocks."
    }
  }
}
```

#### Retry Policy

Rupi retries failed webhook deliveries (non-2xx responses or connection timeouts) using exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 5 minutes  |
| 3       | 30 minutes |
| 4       | 2 hours    |
| 5       | 8 hours    |

After 5 failed attempts, the event is marked `failed` and no further retries are made. Failed events can be replayed manually from the Rupi dashboard.

***

### Error Handling

#### HTTP Status Codes

| Code  | Meaning                                                 |
| ----- | ------------------------------------------------------- |
| `200` | Success.                                                |
| `201` | Resource created.                                       |
| `202` | Request accepted, processing asynchronously.            |
| `400` | Bad request. See `error_code` and `message`.            |
| `401` | Invalid or missing API key.                             |
| `403` | API key does not have permission for this operation.    |
| `404` | Resource not found.                                     |
| `422` | Unprocessable entity. Required prerequisite is missing. |
| `429` | Rate limit exceeded. See `Retry-After` header.          |
| `500` | Internal server error.                                  |

#### Error Response Format

json

```json
{
  "error": {
    "code": "ERR_INCOME_MISMATCH",
    "message": "Verified income amount differs from expected payroll amount by more than the allowed tolerance.",
    "details": {
      "expected_amount": 4500.00,
      "received_amount": 5800.00,
      "tolerance_pct": 0.02,
      "actual_variance_pct": 0.289
    },
    "request_id": "req_1ZaH..."
  }
}
```

#### Custom Error Codes

| Error Code                   | HTTP Status | Description                                                                                         |
| ---------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
| `ERR_WASH_TRADE_DETECTED`    | 422         | One or more transactions were classified as wash trades and excluded from income calculation.       |
| `ERR_INCOME_MISMATCH`        | 422         | On-chain inflow amount differs from payroll record by more than the configured tolerance.           |
| `ERR_UNVERIFIED_SOURCE`      | 422         | Inbound transaction origin address is not in the platform's whitelisted disbursement wallet list.   |
| `ERR_CIRCULAR_FLOW`          | 422         | Funds were detected cycling between addresses in a pattern consistent with circular flow inflation. |
| `ERR_NO_VIP`                 | 404         | No VIP report exists for this user. Call `GET /vip-report` first.                                   |
| `ERR_MISSING_WALLET`         | 422         | User has no active wallet connection.                                                               |
| `ERR_MISSING_PLATFORM`       | 422         | User has no active platform connection (neobank, exchange, payroll, or other source).               |
| `ERR_PLATFORM_SYNC_FAILED`   | 502         | Rupi could not retrieve current data from the connected platform.                                   |
| `ERR_INSUFFICIENT_HISTORY`   | 422         | Wallet history is too short to generate a reliable VIP. Minimum is 1 verified period.               |
| `ERR_PAYMENT_INTENT_EXPIRED` | 410         | Payment intent has expired. Create a new intent.                                                    |
| `ERR_APPROVAL_NOT_CONFIRMED` | 422         | ERC-20 approval has not been confirmed on-chain.                                                    |

#### Rate Limits

| Endpoint group      | Limit                           |
| ------------------- | ------------------------------- |
| `/vip-report`       | 10 requests/minute per user     |
| `/risk-score`       | 20 requests/minute per user     |
| `/transactions`     | 60 requests/minute per API key  |
| All other endpoints | 120 requests/minute per API key |

Rate limit headers are included in all responses:

```
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 1701424860
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.rupi.global/core-documentation/markdown/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
