# Create or Update Webhook API

- Anchor: `bankhub-en-api-api-webhook-cap-nhat-webhook`
- URL: https://developer.sepay.vn/en/bankhub/api/api-webhook/cap-nhat-webhook.md
- Description: API to create a new webhook or update an existing webhook (upsert). Webhooks are used to receive event notifications from Bank Hub.

**POST** `https://bankhub-api-sandbox.sepay.vn/v1/webhook`
```http
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
```


**Request Body**

<!-- Params table -->

**Headers**

<!-- Params table -->

> **Note** (info)
>
>
> * This API requires a Bearer Token in the Authorization header
> * Upsert pattern: creates a webhook if none exists, updates it if it already exists
> * Only one webhook is allowed per merchant
> * The full secret key is returned in the response for reference
>

| Code | Name | Description |
|------|------|-------------|
| 400 | Bad Request | Invalid URL, internal IP is blocked (SSRF protection), or invalid event names |
| 401 | Unauthorized | Invalid or expired access token |







**RESPONSE 200 - Upsert Successful**


```json
{
  "data": {
    "webhook_url": "https://merchant.com/webhook",
    "auth_type": "SECRET_KEY",
    "secret_key": "your-secret-key-here",
    "request_content_type": "application/json",
    "active": 1,
    "allow_events": ["BANK_ACCOUNT_LINKED", "BANK_ACCOUNT_UNLINKED"]
  }
}
```





**RESPONSE 400 - Validation Error**


```json
{
  "code": 400,
  "message": "Bad request",
  "errors": {
    "webhook_url": "Invalid URL or internal IP is used",
    "allow_events": "Invalid event name: INVALID_EVENT"
  }
}
```








---

### Usage Examples

{% node
title="Subscribe to all events"
code="curl --location '[https://bankhub-api-sandbox.sepay.vn/v1/webhook](https://bankhub-api-sandbox.sepay.vn/v1/webhook)' \\n  --header 'Content-Type: application/json' \\n  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\n  --data '{\n    "webhook_url": "[https://merchant.com/webhook\",\n](https://merchant.com/webhook\%22,\n)    "auth_type": "SECRET_KEY",\n    "secret_key": "your-secret-key-here",\n    "active": 1,\n    "allow_events": ["*"]\n  }'"
/%}

{% node
title="Disable all events"
code="curl --location '[https://bankhub-api-sandbox.sepay.vn/v1/webhook](https://bankhub-api-sandbox.sepay.vn/v1/webhook)' \\n  --header 'Content-Type: application/json' \\n  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\n  --data '{\n    "webhook_url": "[https://merchant.com/webhook\",\n](https://merchant.com/webhook\%22,\n)    "allow_events": []\n  }'"
/%}

{% node
title="Subscribe to specific events"
code="curl --location '[https://bankhub-api-sandbox.sepay.vn/v1/webhook](https://bankhub-api-sandbox.sepay.vn/v1/webhook)' \\n  --header 'Content-Type: application/json' \\n  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\n  --data '{\n    "webhook_url": "[https://merchant.com/webhook\",\n](https://merchant.com/webhook\%22,\n)    "auth_type": "SECRET_KEY",\n    "secret_key": "your-secret-key-here",\n    "active": 1,\n    "allow_events": [\n      "BANK_ACCOUNT_LINKED",\n      "BANK_ACCOUNT_UNLINKED",\n      "LINK_TOKEN_CREATED",\n      "LINK_SESSION_COMPLETED"\n    ]\n  }'"
/%}

---

> **Validation Rules** (warn)
>
> **Webhook URL:**
>
> * Must be HTTPS in production
> * Max 2000 characters
> * Must not use internal IP addresses (SSRF protection)
> * Blocked IP ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
>
> **Secret Key:**
>
> * Max 500 characters
> * Recommended: random string, 32–64 characters long
>
> **Allow Events:**
>
> * Only accepts valid event names from the whitelist (13 events)
> * `["*"]`: Wildcard to subscribe to all events
> * `[]` or `null`: Do not send any events
>

> **13 Webhook Events** (info)
>
> **Link Token Events (3):**
>
> * `LINK_TOKEN_CREATED`, `LINK_TOKEN_REVOKED`, `LINK_TOKEN_EXPIRED`
>
> **Link Session Events (7):**
>
> * `LINK_SESSION_INITIALIZED`, `LINK_SESSION_REINITIALIZED`, `LINK_SESSION_STATE_CHANGED`
> * `LINK_SESSION_COMPLETED`, `LINK_SESSION_FAILED`, `LINK_SESSION_ABANDONED`, `LINK_SESSION_EXPIRED`
>
> **Bank Account Events (3):**
>
> * `BANK_ACCOUNT_LINKED`, `BANK_ACCOUNT_UNLINKED`, `BANK_ACCOUNT_INACTIVATED`
>

> **Upsert Behavior** (info)
>
>
> * **Create**: If the merchant does not have a webhook, create a new webhook
> * **Update**: If the merchant already has a webhook, update the current configuration
> * **One webhook per merchant**: Only one webhook config is allowed per merchant
> * **Partial update**: Only send the fields you want to update (similar to merchant config)
>

> **Important Notes** (warn)
>
>
> * **SSRF Protection**: Internal IP addresses and localhost are blocked to protect against SSRF attacks
> * **Secret Key Display**: The full secret key is only shown in POST responses; GET responses are masked
> * **Event Validation**: Invalid event names will be rejected with a 400 error
> * **Active Flag**: If `active = 0`, no events will be sent even if `allow_events` is configured
> * **Immediate Effect**: Configuration changes apply immediately to all new events
>

> **Webhook Authentication** (info)
>
> When `auth_type = "SECRET_KEY"`, each webhook request includes the header:
> `X-Secret-Key: your-secret-key-here`
>

**Verify webhook in your endpoint:**


---
