# API Tạo hoặc cập nhật Webhook

- Anchor: `bankhub-vi-api-api-webhook-cap-nhat-webhook`
- URL: https://developer.sepay.vn/vi/bankhub/api/api-webhook/cap-nhat-webhook.md
- Description: API tạo webhook mới hoặc cập nhật webhook hiện có (upsert). Webhook được dùng để nhận thông báo sự kiện từ Bank Hub.

#### API Endpoint

**POST** `https://bankhub-api-sandbox.sepay.vn/v1/webhook`

API tạo webhook mới hoặc cập nhật webhook hiện có (upsert).

**Pattern upsert:**
- Tạo mới nếu merchant chưa có webhook
- Cập nhật nếu merchant đã có webhook
- Chỉ cho phép một webhook trên mỗi merchant
- Hỗ trợ partial update: chỉ gửi các field cần cập nhật

## 13 loại sự kiện (allow_events)

**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`

## Cấu hình allow_events
- `["*"]`: Wildcard - đăng ký tất cả sự kiện
- `[]` hoặc `null`: Không gửi bất kỳ sự kiện nào
- `["EVENT_1", "EVENT_2"]`: Chỉ gửi các sự kiện cụ thể

**Authentication:** bearerAuth

#### Request Body

**Request Body:**

- `webhook_url` (string (uri), optional): URL webhook endpoint (HTTPS required in production)
- `auth_type` (enum: `SECRET_KEY`, optional): Loại xác thực
- `secret_key` (string, optional): Secret key (sẽ gửi trong header X-Secret-Key)
- `active` (enum: `0`, `1`, optional): 1 = bật, 0 = tắt
- `allow_events` (array, optional): ["*"] = tất cả events, [] = không events nào

> **Lưu ý** (info)
> - API này yêu cầu Bearer Token trong header Authorization
> - Pattern upsert: tạo mới nếu chưa có, cập nhật nếu đã tồn tại
> - Chỉ cho phép một webhook trên mỗi merchant
> - Secret key đầy đủ được trả về trong response để tham khảo

#### Xử lý lỗi

#### API Response

**Response 200:** Upsert thành công

**Response 400:** Validation Error

**Response 401:** 

#### Code mẫu

**cURL:**
```bash
curl --request POST \
  --url https://bankhub-api-sandbox.sepay.vn/v1/webhook \
  --header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"webhook_url":"http://example.com","auth_type":"SECRET_KEY","secret_key":"string","active":0,"allow_events":["*"]}'
```

**PHP:**
```php
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://bankhub-api-sandbox.sepay.vn/v1/webhook",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}",
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer REPLACE_BEARER_TOKEN",
    "content-type: application/json"
  ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
```

**Python:**
```python
import http.client

conn = http.client.HTTPSConnection("bankhub-api-sandbox.sepay.vn")

payload = "{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}"

headers = {
    'Authorization': "Bearer REPLACE_BEARER_TOKEN",
    'content-type': "application/json"
    }

conn.request("POST", "/v1/webhook", payload, headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
```

**NodeJS:**
```javascript
const http = require("https");

const options = {
  "method": "POST",
  "hostname": "bankhub-api-sandbox.sepay.vn",
  "port": null,
  "path": "/v1/webhook",
  "headers": {
    "Authorization": "Bearer REPLACE_BEARER_TOKEN",
    "content-type": "application/json"
  }
};

const req = http.request(options, function (res) {
  const chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function () {
    const body = Buffer.concat(chunks);
    console.log(body.toString());
  });
});

req.write(JSON.stringify({
  webhook_url: 'http://example.com',
  auth_type: 'SECRET_KEY',
  secret_key: 'string',
  active: 0,
  allow_events: ['*']
}));
req.end();
```

**Java:**
```java
OkHttpClient client = new OkHttpClient();

MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}");
Request request = new Request.Builder()
  .url("https://bankhub-api-sandbox.sepay.vn/v1/webhook")
  .post(body)
  .addHeader("Authorization", "Bearer REPLACE_BEARER_TOKEN")
  .addHeader("content-type", "application/json")
  .build();

Response response = client.newCall(request).execute();
```

**Ruby:**
```ruby
require 'uri'
require 'net/http'
require 'openssl'

url = URI("https://bankhub-api-sandbox.sepay.vn/v1/webhook")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer REPLACE_BEARER_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}"

response = http.request(request)
puts response.read_body
```

**Go:**
```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://bankhub-api-sandbox.sepay.vn/v1/webhook"

	payload := strings.NewReader("{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer REPLACE_BEARER_TOKEN")
	req.Header.Add("content-type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

**.NET:**
```csharp
var client = new HttpClient();
var request = new HttpRequestMessage
{
    Method = HttpMethod.Post,
    RequestUri = new Uri("https://bankhub-api-sandbox.sepay.vn/v1/webhook"),
    Headers =
    {
        { "Authorization", "Bearer REPLACE_BEARER_TOKEN" },
    },
    Content = new StringContent("{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}")
    {
        Headers =
        {
            ContentType = new MediaTypeHeaderValue("application/json")
        }
    }
};
using (var response = await client.SendAsync(request))
{
    response.EnsureSuccessStatusCode();
    var body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

**Swift:**
```swift
import Foundation

let headers = [
  "Authorization": "Bearer REPLACE_BEARER_TOKEN",
  "content-type": "application/json"
]
let parameters = [
  "webhook_url": "http://example.com",
  "auth_type": "SECRET_KEY",
  "secret_key": "string",
  "active": 0,
  "allow_events": ["*"]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://bankhub-api-sandbox.sepay.vn/v1/webhook")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

**Kotlin:**
```kotlin
val client = OkHttpClient()

val mediaType = MediaType.parse("application/json")
val body = RequestBody.create(mediaType, "{\"webhook_url\":\"http://example.com\",\"auth_type\":\"SECRET_KEY\",\"secret_key\":\"string\",\"active\":0,\"allow_events\":[\"*\"]}")
val request = Request.Builder()
  .url("https://bankhub-api-sandbox.sepay.vn/v1/webhook")
  .post(body)
  .addHeader("Authorization", "Bearer REPLACE_BEARER_TOKEN")
  .addHeader("content-type", "application/json")
  .build()

val response = client.newCall(request).execute()
```

---

### Ví dụ sử dụng

---

> **Validation Rules** (warn)
> **Webhook URL:**
> - Phải là HTTPS trong production
> - Tối đa 2000 ký tự
> - Không được sử dụng IP nội bộ (SSRF protection)
> - Dải IP bị chặn: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
>
> **Secret Key:**
> - Tối đa 500 ký tự
> - Khuyến nghị: string ngẫu nhiên, độ dài 32-64 ký tự
>
> **Allow Events:**
> - Chỉ chấp nhận tên sự kiện hợp lệ từ whitelist (13 events)
> - `["*"]`: Wildcard để đăng ký tất cả sự kiện
> - `[]` hoặc `null`: Không gửi bất kỳ sự kiện nào

> **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)
> - **Tạo mới**: Nếu merchant chưa có webhook, tạo webhook mới
> - **Cập nhật**: Nếu merchant đã có webhook, cập nhật cấu hình hiện tại
> - **Một webhook per merchant**: Chỉ được có một webhook config per merchant
> - **Partial update**: Chỉ gửi các field cần update

> **Lưu ý quan trọng** (warn)
> - **SSRF Protection**: IP nội bộ và localhost bị chặn để bảo vệ khỏi SSRF attacks
> - **Secret Key Display**: Secret key đầy đủ chỉ hiển thị trong POST response, GET response sẽ che (mask)
> - **Event Validation**: Tên sự kiện không hợp lệ sẽ bị reject với error 400
> - **Active Flag**: Nếu `active = 0`, webhook sẽ không gửi bất kỳ event nào dù có config `allow_events`
> - **Immediate Effect**: Thay đổi cấu hình áp dụng ngay lập tức cho tất cả events mới

> **Webhook Authentication** (info)
> Khi `auth_type = "SECRET_KEY"`, mỗi webhook request sẽ có header:
> `X-Secret-Key: your-secret-key-here`
>
> **Verify webhook trong endpoint của bạn:**

```javascript
app.post('/webhook', (req, res) => {
  const receivedSecret = req.headers['x-secret-key'];
  const expectedSecret = 'your-secret-key-here';

  if (receivedSecret !== expectedSecret) {
    return res.status(401).send('Unauthorized');
  }

  // Process webhook event
  const event = req.body;
  console.log('Event:', event.event);

  res.status(200).send('OK');
});
```

---

### BankHub - Bank Account Linking API (en)
