# Idempotency

Lỗi mạng hay hết thời gian chờ khiến bạn không biết request đã tới nơi hay chưa. Nếu cứ gửi lại, khách có thể nhận cùng một email hai lần hoặc bị yêu cầu trả tiền hai lần. **Idempotency key** giúp việc gửi lại an toàn: Hạ Tầng chỉ làm việc đó một lần và trả cùng một kết quả cho mọi lần lặp lại.

## Hỗ trợ ở đâu

Gửi key trong header `Idempotency-Key`. Key là chuỗi bất kỳ do bạn chọn, tối đa 256 ký tự.

| Endpoint | Phạm vi của key | Lặp lại với cùng body | Lặp lại với body khác |
|---|---|---|---|
| `POST /emails` | Doanh nghiệp | `200` với cùng `id`; không gửi lại thư | `400 invalid_idempotent_request` |
| `POST /payments` | Doanh nghiệp và môi trường Thanh toán | `201` với cùng thanh toán | `400 invalid_idempotent_request` |
| `POST /chat/conversations/{id}/messages` | Hội thoại | `200` với cùng tin nhắn và `"duplicate": true` | `200` với tin nhắn đầu (body không được so sánh) |

`POST /emails/batch` và các endpoint khác không nhận idempotency key.

## Gửi idempotency key

Dùng key xác định nghiệp vụ, không phải lần gọi HTTP: mã đơn hàng, `welcome-<user id>`, `invoice-<số>-reminder-1`. Tạo key một lần, lưu cùng tác vụ, và dùng lại ở mọi lần thử.

**cURL**

```bash
curl -X POST https://api.hatang.dev/emails \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001-confirmation" \
  -d '{
    "from": "Shop <hello@shop.vn>",
    "to": "customer@example.com",
    "subject": "Đơn #1001 đã được xác nhận",
    "html": "<p>Cảm ơn bạn đã đặt hàng!</p>"
  }'
```

**Node.js**

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

// Email
await hatang.emails.send(
  { from: "Shop <hello@shop.vn>", to: "customer@example.com", subject: "Đơn #1001 đã được xác nhận", html: "<p>Cảm ơn!</p>" },
  { idempotencyKey: "order-1001-confirmation" },
);

// Thanh toán
await hatang.payments.create({ amount: 150000, description: "Đơn 1001" }, { idempotencyKey: "order-1001" });

// Trả lời chat
await hatang.chat.conversations.send("cnv_123...", { content: "Đơn của anh được giao trong hôm nay ạ." }, { idempotencyKey: "order-1001-shipped" });
```

**Python**

```python
from hatang import Hatang

hatang = Hatang()  # đọc HATANG_API_KEY
hatang.emails.send(
    {
        "from": "Shop <hello@shop.vn>",
        "to": "customer@example.com",
        "subject": "Đơn #1001 đã được xác nhận",
        "html": "<p>Cảm ơn bạn đã đặt hàng!</p>",
    },
    idempotency_key="order-1001-confirmation",
)
```

**PHP**

```php
$res = $client->post('/emails', [
    'headers' => ['Idempotency-Key' => 'order-1001-confirmation'],
    'json' => [
        'from' => 'Shop <hello@shop.vn>',
        'to' => 'customer@example.com',
        'subject' => 'Đơn #1001 đã được xác nhận',
        'html' => '<p>Cảm ơn bạn đã đặt hàng!</p>',
    ],
]);
```

## Key được lưu thế nào

- **Email và thanh toán.** Key được lưu cùng email hoặc thanh toán mà nó tạo ra, kèm bản băm của body request. Lần lặp lại với cùng key được so với bản băm đó. Key không hết hạn: còn email hay thanh toán thì key vẫn trỏ tới nó. Email được trả lại theo key không bị tính thêm vào hạn mức trong ngày.
- **Tin nhắn chat.** Key trở thành `client_id` của tin nhắn. Lần lặp lại trong cùng hội thoại trả về tin nhắn đã có với `"duplicate": true`. Widget và SDK di động cũng làm như vậy với `client_id` gửi kèm mỗi tin nhắn, nhờ đó tin nhắn gửi hai lần khi mạng chập chờn chỉ hiện một lần trong hộp thư.
- **Môi trường Thanh toán.** Key của thanh toán tính riêng theo môi trường, nên cùng một mã đơn dùng được một lần ở development và một lần ở production.

## Xung đột

| `name` | Status | Nguyên nhân | Nên làm gì |
|---|---|---|---|
| `invalid_idempotent_request` | 400 | Key đã được dùng với body **khác** | Bạn đã dùng lại key cho một nghiệp vụ khác. Tạo key mới; đừng bao giờ đổi body khi thử lại. |
| `concurrent_idempotent_requests` | 409 | Chỉ ở Email: hai request cùng key tới cùng một lúc | Chờ một chút rồi thử lại với cùng key; bạn nhận kết quả của request đầu. |

```json
{
  "statusCode": 400,
  "name": "invalid_idempotent_request",
  "message": "Same idempotency key used with a different request payload"
}
```

"Body khác" là bất kỳ giá trị nào thay đổi, như một mốc thời gian mới hay một id ngẫu nhiên được sinh lại trong payload. Hãy dựng payload một lần, lưu cùng key, và gửi lại đúng payload đó ở mọi lần thử.

## Lời khuyên khi thử lại

1. **Tạo key trước lần gọi đầu tiên** và lưu cùng tác vụ, để tiến trình có chết giữa chừng thì key vẫn còn.
2. **Chỉ thử lại lỗi tạm thời:** lỗi mạng, `429 rate_limit_exceeded` và `5xx`. Tăng thời gian chờ theo cấp số nhân và tôn trọng `retry-after` ([Giới hạn tần suất](https://hatang.dev/vi/docs/guides/getting-started/rate-limits.md)).
3. **Không thử lại lỗi `4xx`** ngoài `429` và `409 concurrent_idempotent_requests`; hãy sửa request.
4. **Coi lần trả lại là thành công.** `200` cho email lặp lại hay tin nhắn chat có `"duplicate": true` nghĩa là lần đầu đã thành công.
5. **Làm cho handler webhook cũng idempotent.** Lượt gửi webhook có thể lặp lại; xem [Webhook](https://hatang.dev/vi/docs/guides/getting-started/webhooks.md#make-your-handler-idempotent).

## Bước tiếp theo

- [Giới hạn tần suất](https://hatang.dev/vi/docs/guides/getting-started/rate-limits.md)
- [Lỗi](https://hatang.dev/vi/docs/guides/getting-started/errors.md)
- [Webhook](https://hatang.dev/vi/docs/guides/getting-started/webhooks.md)

Source: https://hatang.dev/vi/docs/guides/getting-started/idempotency
