Tích hợp eNotary vào hệ thống của bạn
Gửi tài liệu PDF sang Phòng công chứng, trao đổi với cán bộ, và nhận kết quả đã ký ngay khi xong. Chỉ cần gọi API và nhận webhook.
https://api-dev.enotaryid.com/api/integrationhttps://api.enotaryid.com/api/integration1Giới thiệu & luồng tổng quan
Hệ thống của bạn gọi API để gửi tài liệu. eNotary gọi lại webhook của bạn khi có tin mới.
Chiều vào: bạn gọi eNotary
Gửi PDF, xem danh sách, xem chi tiết, tải file, viết trao đổi. Mọi yêu cầu kèm header X-API-Key.
Chiều ra: eNotary gọi bạn
Khi tài liệu được dùng, ký xong, bị từ chối hoặc có trao đổi mới, eNotary gửi webhook (HTTP POST, có chữ ký) tới địa chỉ của bạn.
Kịch bản có từ chối, gửi lại và trao đổi
Hình trên là đường đi thuận lợi. Thực tế hai bên thường qua lại vài lượt trước khi ký. Hình dưới là một kịch bản đầy đủ: Phòng công chứng từ chối một file, bạn gửi bản mới, hai bên trao đổi, rồi Phòng công chứng công chứng và ký.
Bản gửi lại là một file mới
File A bị từ chối là trạng thái cuối, không sửa hay thay nội dung được. Bản mới A′ có uuid riêng và không tự liên kết với A. Hãy đặt code có quy ước (ví dụ HD-001, HD-001-v2) để tự đối chiếu.
Từ chối cả bộ thì phải tạo bộ mới
Khi nhận shared_set.rejected, bộ dừng hẳn và không thêm file được nữa (API trả 409). Hãy tạo bộ mới bằng 4.1.
shared_file.sign_rejected (có reason, dossier). File trở lại để chọn vào hồ sơ khác, nội dung không đổi. Nếu cần sửa nội dung, hai bên trao đổi rồi bạn gửi file mới như bước ③.Vài từ cần biết
| Từ | Nghĩa |
|---|---|
| Bộ tài liệu | Một "hồ sơ" bạn gửi sang. Chứa nhiều file PDF. Có hai loại: công chứng (shared-notary-set) và chứng thực (shared-cert-set). |
| Tài liệu / file | Một file PDF nằm trong bộ. Mỗi file có uuid riêng. |
| Phòng công chứng | Đơn vị nhận hồ sơ của bạn trên eNotary. Mỗi khóa API gắn với đúng một đơn vị. |
| Webhook | eNotary tự gọi một địa chỉ HTTPS của bạn khi có sự kiện mới. |
Mã của bạn (code) | Mã bạn tự đặt cho bộ hoặc file để đối chiếu với hệ thống của mình. Hệ thống lưu thành externalCode. |
2Bắt đầu nhanh
Từ chưa có gì đến gửi được bộ tài liệu đầu tiên trong vài phút.
- Xin API key
Liên hệ eNotary để được cấp khóa (dạng
enk_…). Khóa gắn với tài khoản đối tác của bạn trong một Phòng công chứng.Giữ kín khóaKhóa chỉ hiện một lần lúc cấp. Hãy cất vào nơi lưu bí mật. Mất khóa thì xin cấp khóa mới. Không để khóa trong mã nguồn công khai hay trong URL. - Chọn địa chỉ gốc (base URL)
Dùng Dev để thử, dùng Production khi chạy thật. Nút Dev / Production ở góc trên sẽ đổi địa chỉ trong mọi ví dụ trên trang này.
Môi trường Base URL Dev https://api-dev.enotaryid.com/api/integrationProduction https://api.enotaryid.com/api/integrationĐừng dùng địa chỉ trang web quản trịAPI chỉ chạy ởapi-dev.enotaryid.comvàapi.enotaryid.com. Địa chỉ bạn mở trên trình duyệt để đăng nhập (ví dụadmin-dev.enotaryid.com) là trang web, không phải API: gọiPOSTvào đó sẽ nhận405 Not Alloweddạng trang HTML của nginx, và yêu cầu không tới được eNotary. - Gọi thử một yêu cầu
Lấy danh sách bộ tài liệu. Nếu nhận được JSON thay vì lỗi 401 là khóa đã dùng được.
curl "https://api-dev.enotaryid.com/api/integration/documents?page=1&pageSize=5" \ -H "X-API-Key: enk_xxxxxxxx"{ "statusCode": 200, "data": { "items": [], "total": 0, "page": 1, "pageSize": 5, "metadata": { "tenantCode": "ORG-DEMO" } }, "timestamp": "2026-10-12T08:00:00.000Z" } - Gửi bộ tài liệu đầu tiên
Cách dễ nhất là gửi file thật bằng multipart.
Ghi lạicurl -X POST "https://api-dev.enotaryid.com/api/integration/documents/upload" \ -H "X-API-Key: enk_xxxxxxxx" \ -F "code=HD-2026-001" \ -F "customerName=Nguyễn Văn A" \ -F "files=@hop-dong.pdf"data.uuidtrong phản hồi. Đó là mã của bộ. Bạn sẽ dùng nó để thêm file và viết trao đổi.Cần tài khoản đối tácMuốn tạo bộ hoặc thêm file, khóa phải gắn với tài khoản đối tác. Nếu không, bạn nhận lỗi 403. Khóa chỉ gắn tài khoản nội bộ thì chỉ đọc được. - Nhận kết quả Cách 1: đăng ký webhook để eNotary báo cho bạn. Cách 2: gọi chi tiết bộ định kỳ. Khi file ký xong, bạn tải bản đã ký bằng tải file.
3Quy ước chung
Những điều đúng cho mọi API bên dưới. Đọc một lần là đủ.
Header
| Tên | Bắt buộc | Giá trị |
|---|---|---|
X-API-Key | Có | Khóa API của bạn, ví dụ enk_xxxxxxxx. Đây là cách xác thực duy nhất. |
Content-Type | Có (khi có body) | application/json cho API JSON. multipart/form-data cho API upload (thư viện HTTP thường tự thêm). |
Khóa hết hạn, bị thu hồi hoặc sai sẽ nhận 401. Mỗi khóa chỉ thấy dữ liệu của đúng một Phòng công chứng. Đọc dữ liệu của đơn vị khác sẽ nhận 404 hoặc 403.
Dạng phản hồi
Phản hồi thành công luôn bọc trong một "phong bì" gồm statusCode, data, timestamp. Mọi ví dụ trong tài liệu này hiển thị đủ phong bì.
{
"statusCode": 200,
"data": { },
"timestamp": "2026-10-12T08:00:00.000Z"
}
statusCode trong body luôn là 200. Mã HTTP thật nằm ở dòng trạng thái của phản hồi (ví dụ 201 khi tạo mới). Hãy dựa vào mã HTTP. Riêng API tải file trả về dữ liệu file thô, không có phong bì.Khi lỗi, phản hồi có dạng sau (không có data):
{
"statusCode": 400,
"message": "Provide one of: id, uuid, code",
"error": "Bad Request"
}
message có thể là một chuỗi hoặc một mảng chuỗi (khi nhiều trường sai). Hai lỗi có thêm thông tin riêng: 409 kèm existingId, và 400 "File validation failed" kèm danh sách errors. Xem Mã lỗi.
Các loại mã: id, uuid, code, externalCode
| Tên | Ví dụ | Là gì |
|---|---|---|
id | 1001 | Số nội bộ của eNotary. Ít dùng. Chỉ API chi tiết bộ nhận ?id=. |
uuid | 3f2b8c1e-… | Mã định danh dài của bộ hoặc file. Dùng trong đường dẫn như /documents/{uuid}/files. |
code (trong phản hồi) | NS-26-000042NS-26-000042.1 | Mã do eNotary sinh. File có dạng "mã bộ + số thứ tự". |
externalCode | HD-2026-001 | Mã của bạn, là giá trị bạn gửi ở trường code lúc tạo. Là null nếu bạn không gửi. |
code
- Khi tạo,
codelà mã của bạn (lưu thànhexternalCode). GET /documentsvàGET /documents/detail:?code=tìm theo mã của bạn.GET /files/detail:?code=tìm theo mã hệ thống của file. Muốn tìm theo mã của bạn, dùng?externalCode=.
Phân trang
| Tham số | Mặc định | Giới hạn | Dùng ở |
|---|---|---|---|
page | 1 | ≥ 1 | Danh sách bộ, danh sách trao đổi |
pageSize | 20 (danh sách bộ) 50 (trao đổi) | tối đa 200 (giá trị lớn hơn bị hạ xuống 200) | Danh sách bộ, danh sách trao đổi |
Danh sách bộ trả items, total, page, pageSize. Danh sách trao đổi trả rows và count (tổng số). Danh sách bộ sắp xếp mới nhất trước. Danh sách trao đổi sắp xếp cũ nhất trước.
Ngày giờ
Mọi thời điểm đều theo ISO 8601, múi giờ UTC, ví dụ 2026-10-12T08:00:00.000Z. Giờ Việt Nam (UTC+7) bằng giờ này cộng 7 tiếng. Khi gửi (ví dụ notaryRequestAt, createdFrom), bạn có thể dùng định dạng này, hoặc có kèm múi giờ như 2026-10-12T15:00:00+07:00.
Giới hạn file
| Mục | Giới hạn |
|---|---|
| Loại file | Chỉ PDF. Hệ thống kiểm tra phần đầu file (%PDF-), không chỉ tin mimeType hay đuôi file. |
| Số file mỗi lần gửi | Tối đa 20 (tạo bộ hoặc thêm file). |
| Dung lượng mỗi file | Tối đa 150 MB (bằng giới hạn tải file trên web eNotary). Áp dụng cho cả ba cách gửi: upload, base64, url. |
| Tổng dung lượng mỗi lần gửi | Tối đa 300 MB (cộng tất cả file trong một yêu cầu). |
| Body JSON | Tối đa 150 MB cho cả body. Base64 làm file to thêm khoảng 33%, nên một body JSON chỉ chứa được khoảng 110 MB file thật. |
File lấy từ URL (type: "url") | Phải là https, trỏ ra địa chỉ công khai (chặn địa chỉ nội bộ). Chờ tối đa 15 giây, theo tối đa 3 lần chuyển hướng. Server tải ngay trong lúc gọi API. |
| Độ dài chữ | fileName ≤ 255, title ≤ 500, code ≤ 255. Xem từng API. |
Từ 110 MB đến 150 MB: dùng API upload (
multipart/form-data, xem 4.2, 5.2) hoặc gửi type: "url". Đừng nhét vào base64 vì body JSON sẽ vượt 150 MB.Trên 150 MB: không nhận. Hãy tách file hoặc nén PDF trước khi gửi.
Quy tắc "tất cả hoặc không gì": nếu một file lỗi, cả yêu cầu bị từ chối và không có gì được tạo.
Giới hạn tần suất
| Phạm vi | Giới hạn | Khi vượt |
|---|---|---|
| Mỗi API key | 60 yêu cầu / phút | 429 Too Many RequestsRate limit exceeded (60 req/min)hoặc (600 req/min) |
| Mỗi Phòng công chứng (cộng mọi khóa) | 600 yêu cầu / phút |
Hệ thống đếm theo cửa sổ 1 phút. Nhận 429 thì chờ rồi gọi lại, tăng dần thời gian chờ (1s, 2s, 4s…). Gọi tuần tự thay vì bắn song song thật nhiều. Giá trị trên là mặc định và có thể được cấu hình theo từng môi trường.
4Bộ tài liệu
Tạo một bộ, xem danh sách và xem chi tiết. Một bộ chứa nhiều file PDF.
| Loại bộ | ecmClassCode | ecmClassId | Ghi chú |
|---|---|---|---|
| Công chứng (mặc định) | shared-notary-set | 21 | Dùng đủ các trường. |
| Chứng thực | shared-cert-set | 22 | Không dùng contractType, assetInfo, notaryRequestAt. Hệ thống bỏ qua và trả null. |
Gửi ecmClassCode hoặc ecmClassId. Nếu gửi cả hai, ecmClassId được dùng. Không gửi thì mặc định là công chứng.
4.1 Tạo bộ tài liệu (JSON)
/api/integration/documentsTạo bộ mới. File gửi ngay trong JSON, dưới dạng base64 hoặc url. Bộ vừa tạo ở trạng thái READY và hiện ngay cho Phòng công chứng.
files) rồi thêm file sau bằng POST /documents/{uuid}/files với fileType: "ORIGINAL".403.Body
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
code | string | Có | Mã của bạn, tối đa 255 ký tự. Phải duy nhất trong Phòng công chứng. Trùng thì 409. |
ecmClassCode | string | Không | shared-notary-set hoặc shared-cert-set. |
ecmClassId | number | Không | 21 hoặc 22. |
customerName | string | Không | Họ tên khách hàng, tối đa 500 ký tự. |
customerIdCode | string | Không | Số CCCD / giấy tờ của khách, tối đa 64 ký tự. |
contractType | string | Không | Loại hợp đồng, tối đa 64 ký tự. Chỉ cho bộ công chứng. |
assetInfo | string | Không | Thông tin tài sản. Chỉ cho bộ công chứng. |
notaryRequestAt | string (ISO 8601) | Không | Ngày giờ muốn công chứng. Chỉ cho bộ công chứng. |
files | array | Không | Tối đa 20 phần tử. Xem bảng bên dưới. Bỏ trống để tạo bộ rỗng. |
Mỗi phần tử trong files
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
type | string | Có | base64 hoặc url. |
base64Data | string | Có nếu type=base64 | Nội dung PDF mã hóa base64. Chấp nhận cả dạng data:application/pdf;base64,…. |
url | string | Có nếu type=url | Đường dẫn https tới file PDF. Xem giới hạn. |
fileName | string | Có | Tên file, tối đa 255 ký tự. |
code | string | Không | Mã của bạn cho file này, tối đa 255 ký tự. Trả lại ở externalCode. |
title | string | Không | Tên hiển thị, tối đa 500 ký tự. Bỏ trống thì dùng fileName. |
mimeType | string | Không | Nếu gửi, phải là application/pdf. |
size | number | Không | Dung lượng (byte), chỉ để tham khảo. Hệ thống tự đo dung lượng thật. |
Ví dụ yêu cầu
curl -X POST "https://api-dev.enotaryid.com/api/integration/documents" \
-H "X-API-Key: enk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"code": "HD-2026-001",
"ecmClassCode": "shared-notary-set",
"customerName": "Nguyễn Văn A",
"customerIdCode": "079123456789",
"contractType": "Công chứng mua bán nhà đất",
"notaryRequestAt": "2026-10-15T08:00:00.000Z",
"files": [
{
"type": "base64",
"base64Data": "JVBERi0xLjQK...",
"fileName": "cccd.pdf",
"code": "FILE-A",
"title": "CCCD bản sao"
},
{
"type": "url",
"url": "https://files.example.vn/hop-dong.pdf",
"fileName": "hop-dong.pdf",
"code": "FILE-B",
"title": "Hợp đồng"
}
]
}'
{
"statusCode": 200,
"data": {
"id": 1001,
"code": "NS-26-000042",
"externalCode": "HD-2026-001",
"uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34",
"ecmClassId": 21,
"ecmClassName": "Bộ tài liệu công chứng chia sẻ",
"documentType": 17,
"partnerStatus": "READY",
"signStatus": "NOT_SIGNED",
"status": 1,
"files": [
{
"id": 2001,
"uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12",
"code": "NS-26-000042.1",
"externalCode": "FILE-A",
"fileName": "cccd.pdf",
"title": "CCCD bản sao",
"documentType": 13,
"section": "ORIGINAL",
"status": 1,
"size": 12345,
"mimeType": "application/pdf"
},
{
"id": 2002,
"uuid": "b47a0e63-8c15-4f2d-93be-1d6a7c2e5f08",
"code": "NS-26-000042.2",
"externalCode": "FILE-B",
"fileName": "hop-dong.pdf",
"title": "Hợp đồng",
"documentType": 13,
"section": "ORIGINAL",
"status": 1,
"size": 54321,
"mimeType": "application/pdf"
}
],
"createdAt": "2026-10-12T08:00:00.000Z",
"metadata": { "tenantCode": "ORG-DEMO" }
},
"timestamp": "2026-10-12T08:00:00.000Z"
}
Các trường trong phản hồi
| Tên | Kiểu | Mô tả |
|---|---|---|
| Bộ | ||
id | number | Số nội bộ của bộ. |
code | string | Mã bộ do eNotary sinh. |
externalCode | string | null | Mã của bạn (trường code bạn đã gửi). |
uuid | string | Mã định danh của bộ. Lưu lại để thêm file, viết trao đổi. |
ecmClassId, ecmClassName | number, string | Loại bộ: 21 công chứng, 22 chứng thực, kèm tên hiển thị. |
documentType | number | Luôn là 17 (bộ tài liệu chia sẻ). |
partnerStatus | string | READY sau khi tạo. Xem Trạng thái. |
signStatus | string | Hiện luôn là NOT_SIGNED. Đừng dựa vào trường này để biết đã ký. Hãy dùng trạng thái của file hoặc webhook. |
status | number | Mã trạng thái nội bộ của bộ lúc tạo. Dùng partnerStatus thay thế. |
createdAt | string | Thời điểm tạo (UTC). |
metadata.tenantCode | string | null | Mã Phòng công chứng sở hữu bộ. |
Mỗi phần tử trong files | ||
id, uuid | number, string | Số nội bộ và mã định danh của file. Dùng uuid để tải file, viết trao đổi. |
code | string | Mã file do eNotary sinh, dạng "mã bộ.số thứ tự" (bắt đầu từ 1). |
externalCode | string | null | Mã của bạn cho file (trường files[].code). |
fileName, title | string | Tên file và tên hiển thị. |
documentType, section | number, string | 13 và ORIGINAL (Tài liệu gốc). Xem các khối tài liệu. |
status | number | Trạng thái file. 1 = Chờ sử dụng. |
size, mimeType | number, string | Dung lượng (byte) và application/pdf. |
Lỗi thường gặp
| Mã | Khi nào |
|---|---|
| 400 | Thiếu code, sai kiểu dữ liệu, quá 20 file, ecmClassId hoặc ecmClassCode không hợp lệ, hoặc có file lỗi (kèm danh sách errors, xem Mã lỗi). |
| 401 | Thiếu hoặc sai X-API-Key. |
| 403 | Khóa chưa gắn tài khoản đối tác. |
| 409 | Mã code đã có trong Phòng công chứng. Phản hồi kèm existingId. |
| 429 | Vượt giới hạn tần suất. |
{
"message": "A shared set with code \"HD-2026-001\" already exists for this organization",
"existingId": 1001
}
Muốn gửi lại an toàn khi bị mất kết nối: gọi chi tiết bộ theo ?code= trước. Nếu đã có bộ thì dùng luôn, không tạo lại.
4.2 Tạo bộ tài liệu (multipart)
/api/integration/documents/uploadGiống 4.1 nhưng gửi file trực tiếp bằng multipart/form-data. Phù hợp khi file lớn hoặc bạn không muốn mã hóa base64. Cần tài khoản đối tác như 4.1.
Các trường form
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
code | text | Có | Mã của bạn, tối đa 255 ký tự, duy nhất trong Phòng công chứng. |
ecmClassCode / ecmClassId | text | Không | Như 4.1. |
customerName, customerIdCode, contractType, assetInfo, notaryRequestAt | text | Không | Như 4.1 (cùng giới hạn độ dài). |
files | file | Không | Lặp lại trường này cho mỗi file. Tối đa 20 file, mỗi file tối đa 150 MB, chỉ PDF. |
titles | text (mảng JSON) | Không | Tên hiển thị, khớp theo thứ tự file. Ví dụ ["CCCD", "Hợp đồng"]. Bỏ trống thì dùng tên file. |
codes | text (mảng JSON) | Không | Mã của bạn cho từng file, khớp theo thứ tự file. Ví dụ ["FILE-A", "FILE-B"]. |
codes chỉ có 2 phần tử thì file thứ 3 không có mã. Chuỗi JSON hỏng bị bỏ qua mà không báo lỗi.files, đổi kiểu sang File; nhiều file thì thêm nhiều dòng cùng key files. Không đặt key dạng files[0][file] hay files[0][title]: hệ thống không nhận và trả lỗi 400. Tên hiển thị gửi chung trong một dòng titles với giá trị là mảng JSON, ví dụ ["Hợp đồng vay vốn"]. Đừng quên dòng code (bắt buộc) và header X-API-Key.Ví dụ yêu cầu
curl -X POST "https://api-dev.enotaryid.com/api/integration/documents/upload" \
-H "X-API-Key: enk_xxxxxxxx" \
-F "code=HD-2026-002" \
-F "customerName=Trần Thị B" \
-F "files=@cccd.pdf" \
-F "files=@hop-dong.pdf" \
-F 'titles=["CCCD bản sao", "Hợp đồng"]' \
-F 'codes=["FILE-A", "FILE-B"]'
Giống hệt 4.1. Lỗi cũng giống, thêm hai lỗi riêng của upload: 413 khi một file quá 150 MB, và 400 khi file không phải PDF (ví dụ Route này chỉ nhận: pdf).
4.3 Danh sách bộ tài liệu
/api/integration/documentsLiệt kê các bộ của Phòng công chứng bạn, mới nhất trước. Có phân trang và bộ lọc. Tất cả tham số đều không bắt buộc.
Tham số query
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
page | number | Không | Trang, bắt đầu từ 1. Mặc định 1. |
pageSize | number | Không | Số bộ mỗi trang. Mặc định 20, tối đa 200. |
code | string | Không | Tìm đúng theo mã của bạn. |
codeLike | string | Không | Tìm mã của bạn có chứa chữ này (không phân biệt hoa thường). Ví dụ HD-2026. |
ecmClassCode / ecmClassId | string / number | Không | Lọc theo loại bộ (công chứng hoặc chứng thực). |
partnerStatus | string | Không | DRAFT, READY hoặc REJECTED. Giá trị khác bị bỏ qua (không lọc). |
createdFrom | string (ISO 8601) | Không | Tạo từ thời điểm này trở đi. |
createdTo | string (ISO 8601) | Không | Tạo đến thời điểm này. |
Tham số signStatus được nhận nhưng hiện chưa có tác dụng lọc.
Ví dụ yêu cầu
curl -G "https://api-dev.enotaryid.com/api/integration/documents" \
-H "X-API-Key: enk_xxxxxxxx" \
--data-urlencode "codeLike=HD-2026" \
--data-urlencode "partnerStatus=READY" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=20"
{
"statusCode": 200,
"data": {
"items": [
{
"id": 1001,
"code": "NS-26-000042",
"externalCode": "HD-2026-001",
"uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34",
"ecmClassId": 21,
"ecmClassName": "Bộ tài liệu công chứng chia sẻ",
"customerName": "Nguyễn Văn A",
"customerIdCode": "079123456789",
"partnerStatus": "READY",
"signStatus": "NOT_SIGNED",
"fileCount": 2,
"createdAt": "2026-10-12T08:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"metadata": { "tenantCode": "ORG-DEMO" }
},
"timestamp": "2026-10-12T08:05:00.000Z"
}
Các trường của mỗi phần tử
| Tên | Kiểu | Mô tả |
|---|---|---|
id, uuid, code, externalCode | number, string | Như ở các loại mã. |
ecmClassId, ecmClassName | number, string | Loại bộ. |
customerName, customerIdCode | string | Thông tin khách (có thể vắng nếu bạn không gửi). |
partnerStatus | string | DRAFT, READY hoặc REJECTED. |
signStatus | string | Hiện luôn là NOT_SIGNED. |
fileCount | number | Số file trong bộ. |
createdAt | string | Thời điểm tạo (UTC). |
4.4 Chi tiết bộ tài liệu
/api/integration/documents/detailXem một bộ kèm toàn bộ file và liên kết tải. Gửi một trong ba tham số. Nếu gửi nhiều, thứ tự ưu tiên là id, rồi uuid, rồi code. Không gửi cái nào thì 400. Bộ không tồn tại hoặc thuộc đơn vị khác thì 404.
Tham số query
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
id | number | Chọn 1 | Số nội bộ của bộ. |
uuid | string | Chọn 1 | Mã định danh của bộ. |
code | string | Chọn 1 | Mã của bạn (externalCode). |
Ví dụ yêu cầu
curl -G "https://api-dev.enotaryid.com/api/integration/documents/detail" \
-H "X-API-Key: enk_xxxxxxxx" \
--data-urlencode "code=HD-2026-001"
{
"statusCode": 200,
"data": {
"id": 1001,
"code": "NS-26-000042",
"externalCode": "HD-2026-001",
"uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34",
"ecmClassId": 21,
"ecmClassName": "Bộ tài liệu công chứng chia sẻ",
"documentType": 17,
"partnerStatus": "READY",
"signStatus": "NOT_SIGNED",
"customerName": "Nguyễn Văn A",
"customerIdCode": "079123456789",
"contractType": "Công chứng mua bán nhà đất",
"assetInfo": null,
"notaryRequestAt": "2026-10-15T08:00:00.000Z",
"files": [
{
"id": 2001,
"uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12",
"code": "NS-26-000042.1",
"externalCode": "FILE-A",
"fileName": "cccd.pdf",
"title": "CCCD bản sao",
"documentType": 13,
"section": "ORIGINAL",
"status": 1,
"size": 12345,
"downloadUrl": "https://storage.example/presigned-link?X-Amz-Signature=...",
"signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..."
}
],
"createdAt": "2026-10-12T08:00:00.000Z",
"completedAt": null,
"metadata": { "tenantCode": "ORG-DEMO" }
},
"timestamp": "2026-10-12T08:10:00.000Z"
}
Các trường trong phản hồi
| Tên | Kiểu | Mô tả |
|---|---|---|
Các trường id, uuid, code, externalCode, ecmClass*, documentType, partnerStatus, signStatus, createdAt, metadata giống mục 4.1. | ||
customerName, customerIdCode | string | Thông tin khách (vắng nếu bạn không gửi). |
contractType, assetInfo, notaryRequestAt | string | null | Chỉ có giá trị với bộ công chứng. Bộ chứng thực luôn null. |
completedAt | string | null | Thời điểm hoàn tất bộ. null nếu chưa hoàn tất. |
files[] | array | Gồm mọi file trong bộ, kể cả file do Phòng công chứng thêm (như lời chứng). Mỗi phần tử có id, uuid, code, externalCode, fileName, title, documentType, section, status, size, downloadUrl, signUrl. |
files[].signUrl, downloadUrl | string | Liên kết tải có chữ ký, sống một thời gian (mặc định 7 ngày). Hai trường có cùng giá trị. Cần liên kết ổn định thì dùng tải file. |
5Tài liệu (file)
Thêm file vào bộ có sẵn, xem chi tiết một file và tải file về.
Các khối tài liệu trong một bộ
Mỗi file thuộc đúng một "khối", xác định bởi documentType. Phản hồi có sẵn section để bạn khỏi tự đổi số.
documentType | section | Tên khối | Ai tạo | Ý nghĩa |
|---|---|---|---|---|
13 | ORIGINAL | Tài liệu gốc | Bạn | File bạn gửi lên (lúc tạo bộ, hoặc thêm sau). |
19 | CERT_STATEMENT | Lời chứng | Phòng công chứng | Lời chứng chưa ghép. Bạn tải về nếu muốn tự ghép và ký. Bạn không gửi được khối này. |
15 | MERGED | Tài liệu ghép | Bạn hoặc Phòng công chứng | Đã ghép lời chứng, chưa ký. |
16 | SIGNED | Tài liệu đã ký số | Bạn | Đã ghép và đã ký ở phía bạn. |
14 | COMPLETED | Đã ký (kết quả) | Hệ thống | Bản kết quả sau khi công chứng viên ký xong. Bạn không gửi được khối này. |
documentType sang 14. uuid không đổi. Hãy tra theo uuid đó để lấy bản đã ký.5.1 Thêm file vào bộ có sẵn (JSON)
/api/integration/documents/{uuid}/filesĐẩy thêm file vào một bộ đã tạo. Hay dùng để gửi bản đã ghép và đã ký của bạn (xem luồng tự ghép lời chứng), hoặc bổ sung văn bản gốc cho bộ rỗng. Cần tài khoản đối tác.
Tham số đường dẫn
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
uuid | string | Có | uuid của bộ, không phải của file. Lấy từ phản hồi tạo bộ, từ chi tiết bộ, hoặc từ data.sharedSet.uuid trong webhook. |
files[]. Endpoint này cần cái ngoài cùng. Gửi nhầm uuid file sẽ nhận 404 Shared set not found.Body
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
fileType | string | Có | Chỉ nhận 3 giá trị:ORIGINAL: Tài liệu gốc.SIGNED: Tài liệu đã ký số (đã ghép và đã ký).MERGED: Tài liệu ghép (đã ghép, chưa ký). |
files | array | Có | Từ 1 đến 20 phần tử. Cấu trúc giống files ở 4.1 (type, base64Data hoặc url, fileName, code, title). |
Bạn không thể tự gửi vào khối "Lời chứng" hay "Đã ký (kết quả)". API cũng không nhận documentType dạng số.
Ví dụ yêu cầu
curl -X POST "https://api-dev.enotaryid.com/api/integration/documents/3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34/files" \
-H "X-API-Key: enk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileType": "SIGNED",
"files": [
{
"type": "url",
"url": "https://files.example.vn/hop-dong-da-ky.pdf",
"fileName": "hop-dong-da-ky.pdf",
"code": "HD-2026-001-SIGNED"
}
]
}'
data là một mảng các file vừa thêm.
{
"statusCode": 200,
"data": [
{
"id": 2003,
"uuid": "c58d2f91-4e07-4a6b-8d13-7f0e9b3a6c45",
"code": null,
"externalCode": "HD-2026-001-SIGNED",
"fileName": "hop-dong-da-ky.pdf",
"title": "hop-dong-da-ky.pdf",
"documentType": 16,
"section": "SIGNED",
"status": 1,
"size": 204800,
"downloadUrl": "https://storage.example/presigned-link?X-Amz-Signature=...",
"signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..."
}
],
"timestamp": "2026-10-12T09:00:00.000Z"
}
fileType: "ORIGINAL" sẽ phát sự kiện shared_file.created giống file gửi lúc tạo bộ. Hai loại còn lại không phát sự kiện này.Lỗi thường gặp
| Mã | Khi nào |
|---|---|
| 400 | fileType sai, files rỗng hoặc quá 20, file không phải PDF, file quá 150 MB, tổng quá 300 MB, URL không phải https hoặc trỏ vào địa chỉ nội bộ. |
| 403 | Khóa chưa gắn tài khoản đối tác (…must be bound to a partner account to append files). |
| 404 | Bộ không tồn tại hoặc thuộc đơn vị khác. |
| 409 | Bộ đã bị Phòng công chứng từ chối. Không thêm file được nữa. |
| 429 | Vượt giới hạn tần suất, hoặc Phòng công chứng đã dùng hết dung lượng lưu trữ. |
5.2 Thêm file vào bộ có sẵn (multipart)
/api/integration/documents/{uuid}/files/uploadBản multipart/form-data của 5.1. Cùng quy tắc, giới hạn và mã lỗi. uuid trong đường dẫn vẫn là uuid của bộ.
Các trường form
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
fileType | text | Có | ORIGINAL, SIGNED hoặc MERGED. |
files | file | Có | Lặp lại cho mỗi file. Từ 1 đến 20 file, mỗi file tối đa 150 MB, chỉ PDF. |
titles | text (mảng JSON) | Không | Tên hiển thị, khớp theo thứ tự file. |
codes | text (mảng JSON) | Không | Mã của bạn cho từng file, khớp theo thứ tự file. |
Ví dụ yêu cầu
curl -X POST "https://api-dev.enotaryid.com/api/integration/documents/3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34/files/upload" \
-H "X-API-Key: enk_xxxxxxxx" \
-F "fileType=SIGNED" \
-F "files=@hop-dong-da-ky.pdf" \
-F 'codes=["HD-2026-001-SIGNED"]'
Giống 5.1: data là mảng file vừa thêm. Thiếu file thì 400 At least one file is required. fileType sai thì 400 fileType must be one of: ORIGINAL, SIGNED, MERGED.
5.3 Chi tiết file
/api/integration/files/detailXem thông tin một file (không tải nội dung), kèm tóm tắt bộ chứa nó. Gửi một trong ba tham số. Thứ tự ưu tiên: uuid, rồi code, rồi externalCode.
Tham số query
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
uuid | string | Chọn 1 | Mã định danh của file. |
code | string | Chọn 1 | Mã hệ thống của file, ví dụ NS-26-000042.1. |
externalCode | string | Chọn 1 | Mã của bạn cho file, ví dụ FILE-A. |
Ví dụ yêu cầu
curl -G "https://api-dev.enotaryid.com/api/integration/files/detail" \
-H "X-API-Key: enk_xxxxxxxx" \
--data-urlencode "externalCode=FILE-A"
{
"statusCode": 200,
"data": {
"id": 2001,
"uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12",
"code": "NS-26-000042.1",
"externalCode": "FILE-A",
"fileName": "cccd.pdf",
"title": "CCCD bản sao",
"documentType": 13,
"section": "ORIGINAL",
"status": 1,
"size": 12345,
"mimeType": "application/pdf",
"downloadUrl": "https://storage.example/presigned-link?X-Amz-Signature=...",
"signUrl": "https://storage.example/presigned-link?X-Amz-Signature=...",
"sharedSet": {
"id": 1001,
"uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34",
"code": "NS-26-000042",
"externalCode": "HD-2026-001",
"customerName": "Nguyễn Văn A"
},
"metadata": { "tenantCode": "ORG-DEMO" }
},
"timestamp": "2026-10-12T09:10:00.000Z"
}
Các trường của file giống mục 4.1, thêm downloadUrl và signUrl (liên kết tải có thời hạn, mặc định 7 ngày). sharedSet là tóm tắt bộ chứa file. 400 khi không gửi tham số nào, 404 File not found khi không thấy hoặc thuộc đơn vị khác.
5.4 Tải file
/api/integration/files/{fileUuid}/downloadTải nội dung file. Phản hồi là dữ liệu file thô (không có phong bì JSON). Luôn là phiên bản mới nhất: nếu file đã ký xong, bạn nhận bản đã ký.
Tham số đường dẫn
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
fileUuid | string | Có | uuid của file. |
Ví dụ yêu cầu
curl -L "https://api-dev.enotaryid.com/api/integration/files/9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12/download" \
-H "X-API-Key: enk_xxxxxxxx" \
-o ket-qua.pdf
Header Content-Type theo loại file, Content-Disposition: attachment; filename="…" (tên file được mã hóa URL). Thân phản hồi là các byte của file.
Lỗi thường gặp
| Mã | Khi nào |
|---|---|
| 404 | File not found hoặc File content not available. |
| 403 | Access denied: file thuộc đơn vị khác. |
Luồng tự ghép lời chứng
Dùng khi bạn muốn tự ghép lời chứng và tự ký ở hệ thống của mình. Khác luồng thường đúng một điểm: ai ghép lời chứng.
| # | Ai làm | Việc làm | Kết quả |
|---|---|---|---|
| 1 | Bạn | POST /documents: tạo bộ kèm văn bản gốc | File ở khối Tài liệu gốc (13) |
| 2 | Phòng công chứng | Gửi file lời chứng lên bộ | File ở khối Lời chứng (19) |
| 3 | eNotary | Gửi webhook shared_file.cert_statement_uploaded | data.file.signUrl tải được ngay |
| 4 | Bạn | Tải lời chứng, ghép vào văn bản, ký (ngoài eNotary) | - |
| 5 | Bạn | POST /documents/{uuid}/files với fileType: "SIGNED" | File ở khối Đã ký số (16) |
| 6 | Phòng công chứng | Chọn file (16) vào hồ sơ, công chứng viên ký | Webhook shared_file.used |
| 7 | eNotary | Hồ sơ xong, trả file về bộ | File thành khối 14, webhook shared_file.completed |
shared_file.cert_statement_uploaded nếu đã chọn sự kiện này khi đăng ký webhook. Chưa chọn thì tự gọi chi tiết bộ và lọc files[] theo documentType = 19. File lời chứng thường là PDF, nhưng có thể là file Word (doc, docx), nên đừng giả định đuôi file.6Trao đổi
Kênh nhắn tin hai chiều giữa bạn và Phòng công chứng, gắn với cả bộ hoặc từng file.
Khi bạn gửi trao đổi, cán bộ Phòng công chứng đang xử lý bộ sẽ nhận thông báo trong ứng dụng eNotary. Bạn không cần làm gì thêm. Mỗi trao đổi mới (của bất kỳ ai) còn phát webhook shared.comment_created tới địa chỉ của bạn.
Viết nội dung bằng Markdown
Trường content là văn bản Markdown (chuẩn GitHub). Hệ thống lưu nguyên văn bạn gửi, rồi tự tạo ra HTML đã lọc sạch để hiển thị.
| Bạn gõ | Hiển thị | ||||
|---|---|---|---|---|---|
**chữ đậm** | chữ đậm | ||||
*chữ nghiêng* | chữ nghiêng | ||||
~~gạch ngang~~ | |||||
- mục một |
| ||||
1. bước một |
| ||||
[xem tại đây](https://example.com) | xem tại đây | ||||
`ma-ho-so` | ma-ho-so | ||||
``` | khối mã (hiển thị trong khung mã) | ||||
> trích dẫn | trích dẫn | ||||
| Cột A | Cột B | |
| ||||
# Tiêu đề--- | Tiêu đề (cấp 1 đến 6) và đường kẻ ngang. |
- Một lần xuống dòng (Enter) là một dòng mới. Không cần chừa dòng trống.
- Tối đa 10.000 ký tự (sau khi cắt khoảng trắng đầu và cuối). Nội dung rỗng bị từ chối.
- Nên dùng Markdown, không dùng HTML. Khi hiển thị, hệ thống chỉ giữ các thẻ định dạng an toàn;
<script>, ảnh,stylevà thuộc tính sự kiện (onclick…) đều bị loại bỏ. - Chưa hỗ trợ ảnh. Dòng ảnh
không hiển thị trongcontentHtml. - Liên kết chỉ nhận
http,https,mailto. Liên kết luôn mở tab mới.
6.1 Trao đổi cấp bộ
/api/integration/documents/{uuid}/commentsLấy luồng trao đổi của một bộ, cũ nhất trước.
| Tên | Vị trí | Bắt buộc | Mô tả |
|---|---|---|---|
uuid | đường dẫn | Có | uuid của bộ. |
page | query | Không | Mặc định 1. |
pageSize | query | Không | Mặc định 50, tối đa 200. |
curl "https://api-dev.enotaryid.com/api/integration/documents/3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34/comments?page=1&pageSize=50" \
-H "X-API-Key: enk_xxxxxxxx"
{
"statusCode": 200,
"data": {
"rows": [
{
"id": 8890,
"content": "<p>Nhờ bổ sung CCCD mặt sau.</p>",
"contentHtml": "<p>Nhờ bổ sung CCCD mặt sau.</p>",
"createdAt": "2026-09-30T07:30:00.000Z",
"author": { "type": "ORG", "name": "Nguyễn Văn A" }
},
{
"id": 8892,
"content": "Đã bổ sung **CCCD mặt sau**.\n\n- Nhờ phòng kiểm tra giúp\n- Cảm ơn",
"contentHtml": "<p>Đã bổ sung <strong>CCCD mặt sau</strong>.</p>\n<ul>\n<li>Nhờ phòng kiểm tra giúp</li>\n<li>Cảm ơn</li>\n</ul>",
"createdAt": "2026-10-12T08:00:00.000Z",
"author": { "type": "API", "name": "Đối tác A" }
}
],
"count": 2
},
"timestamp": "2026-10-12T08:01:00.000Z"
}
Phần tử đầu là một trao đổi cũ (viết trước 12/10/2026): content là HTML đã lưu từ trước. Hiển thị thì luôn dùng contentHtml. Xem đối tượng comment.
/api/integration/documents/{uuid}/commentsGửi một trao đổi mới lên bộ.
Body
| Tên | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
content | string | Có | Nội dung dạng Markdown, từ 1 đến 10.000 ký tự. Xem cách viết. |
curl -X POST "https://api-dev.enotaryid.com/api/integration/documents/3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34/comments" \
-H "X-API-Key: enk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"content": "Đã bổ sung **CCCD mặt sau**.\n\n- Nhờ phòng kiểm tra giúp\n- Cảm ơn"}'
{
"statusCode": 200,
"data": {
"id": 8892,
"content": "Đã bổ sung **CCCD mặt sau**.\n\n- Nhờ phòng kiểm tra giúp\n- Cảm ơn",
"contentHtml": "<p>Đã bổ sung <strong>CCCD mặt sau</strong>.</p>\n<ul>\n<li>Nhờ phòng kiểm tra giúp</li>\n<li>Cảm ơn</li>\n</ul>",
"createdAt": "2026-10-12T08:00:00.000Z",
"author": { "type": "API", "name": "Đối tác A" }
},
"timestamp": "2026-10-12T08:00:00.000Z"
}
6.2 Trao đổi cấp file
Giống 6.1 nhưng gắn với một file. Dùng uuid của file. Cấu trúc yêu cầu và phản hồi y hệt cấp bộ.
/api/integration/files/{fileUuid}/commentsLấy luồng trao đổi riêng của một file. Hỗ trợ page và pageSize như 6.1.
curl "https://api-dev.enotaryid.com/api/integration/files/9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12/comments" \
-H "X-API-Key: enk_xxxxxxxx"
/api/integration/files/{fileUuid}/commentsGửi trao đổi về đúng một file. Body: { "content": "…" } như 6.1.
curl -X POST "https://api-dev.enotaryid.com/api/integration/files/9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12/comments" \
-H "X-API-Key: enk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"content": "File này em gửi nhầm bản cũ, nhờ phòng **bỏ qua** giúp."}'
Đối tượng comment
| Tên | Kiểu | Mô tả |
|---|---|---|
id | number | Mã của trao đổi. Dùng để chống xử lý trùng. |
content | string | Nội dung gốc dạng Markdown. Trao đổi cũ (viết trước 12/10/2026) là HTML đã lưu từ trước, dạng <p>…</p>. |
contentHtml | string | HTML đã lọc sạch, hiển thị thẳng được. Chỉ chứa thẻ an toàn (đoạn, danh sách, bảng, liên kết, mã…). |
createdAt | string | Thời điểm gửi (UTC). |
author.type | string | ORG = Phòng công chứng. PARTNER = người của đối tác gõ trên web. API = gửi qua API này. |
author.name | string | null | Tên người gửi. null nếu không xác định. |
contentHtml. Muốn lưu hoặc cho người dùng sửa lại thì dùng content. Đừng tự biến content thành HTML mà không lọc.Luồng trao đổi
shared.comment_created bắn cho mọi trao đổi, kể cả trao đổi do chính bạn gửi qua API.data.comment.author.type === "API". Nếu không, mỗi câu trả lời lại sinh một sự kiện mới và lặp mãi.Lỗi của API trao đổi
| Mã | Khi nào |
|---|---|
| 400 | content rỗng (content must not be empty hoặc Nội dung trao đổi không được để trống) hoặc quá 10.000 ký tự. |
| 401 | Thiếu hoặc sai X-API-Key. |
| 403 | Access denied: bộ hoặc file thuộc đơn vị khác. |
| 404 | Shared set not found, File not found hoặc File is not part of a shared set. |
| 429 | Vượt giới hạn tần suất. |
7Webhook
eNotary tự gọi địa chỉ của bạn khi có chuyện xảy ra. Bạn không cần hỏi đi hỏi lại.
Tổng quan
Khi có sự kiện (file được dùng, ký xong, bị từ chối, có trao đổi mới…), eNotary gửi một yêu cầu HTTP POST chứa JSON tới địa chỉ HTTPS của bạn. Mỗi gói tin có chữ ký để bạn biết nó thật sự đến từ eNotary.
- Nhận
POSTvà đọc nguyên văn body (raw body). - Kiểm tra chữ ký trong
X-Webhook-Signature. Sai thì trả401và bỏ qua. - Chống trùng theo
X-Webhook-Id. - Trả HTTP 2xx ngay. Việc nặng hãy đẩy sang hàng đợi nền của bạn.
Đăng ký webhook
Việc này do quản trị viên eNotary làm cho Phòng công chứng của bạn. Bạn gửi cho họ hai thứ:
- Địa chỉ webhook (URL) của bạn.
- Danh sách sự kiện bạn muốn nhận.
Họ gửi lại cho bạn Webhook Secret. Secret chỉ hiện một lần, hãy cất kỹ. Bạn dùng nó để kiểm tra chữ ký. Quản trị viên cũng có thể gửi thử một gói tin (Test ping), bật hoặc tắt webhook, đổi secret, xem nhật ký gửi và gửi lại thủ công.
- Phải là HTTPS và trỏ tới địa chỉ công khai. Hệ thống chặn địa chỉ nội bộ như
10.x,172.16-31.x,192.168.x,127.x,169.254.x. - eNotary không đi theo chuyển hướng (301, 302…). Mã 3xx bị tính là thất bại. Hãy đưa đúng địa chỉ cuối cùng.
Bạn chỉ nhận những sự kiện đã chọn. Nếu dùng trao đổi hoặc luồng tự ghép lời chứng, nhớ chọn thêm shared.comment_created và shared_file.cert_statement_uploaded.
Header và gói tin
| Header | Ví dụ | Ý nghĩa |
|---|---|---|
Content-Type | application/json | Body là JSON. |
User-Agent | eNotary-Webhook/1.0 | Tên bên gửi. |
X-Webhook-Event | shared_file.used | Loại sự kiện (giống type trong body). |
X-Webhook-Id | evt_3f9a1c2b… | Mã duy nhất của sự kiện (giống id trong body). Dùng để chống trùng. |
X-Webhook-Signature | sha256=9b2c… | Chữ ký HMAC-SHA256 của body, có tiền tố sha256=. |
Mọi sự kiện dùng chung một "phong bì":
{
"id": "evt_3f9a1c2b4d5e6f7a8b9c0d1e2f3a4b5c",
"type": "shared_file.used",
"createdAt": "2026-10-12T08:20:00.123Z",
"metadata": { "tenantCode": "ORG-DEMO" },
"data": { }
}
Kiểm tra chữ ký
Công thức: signature = "sha256=" + HEX( HMAC_SHA256( secret, rawBody ) ). So sánh với header X-Webhook-Signature bằng phép so sánh thời gian cố định (constant-time) để tránh bị dò.
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.ENOTARY_WEBHOOK_SECRET;
// Cần raw body: KHÔNG dùng express.json() cho đường dẫn này
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body; // Buffer
const received = req.header('X-Webhook-Signature') || '';
const expected = 'sha256=' +
crypto.createHmac('sha256', SECRET).update(raw).digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const eventId = req.header('X-Webhook-Id');
const evt = JSON.parse(raw.toString('utf8'));
// 1) Bỏ qua nếu eventId đã xử lý rồi
// 2) Đẩy việc nặng vào hàng đợi nền
// 3) Trả 2xx ngay
res.sendStatus(200);
});
import hmac, hashlib, json, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ENOTARY_WEBHOOK_SECRET"].encode()
@app.post("/webhook")
def webhook():
raw = request.get_data() # bytes, đúng nguyên văn
received = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
event_id = request.headers.get("X-Webhook-Id")
evt = json.loads(raw)
# Bỏ qua nếu event_id đã xử lý, đẩy việc nặng vào hàng đợi, trả 2xx ngay
return "", 200
<?php
$secret = getenv('ENOTARY_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$eventId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';
$evt = json_decode($raw, true);
// Bỏ qua nếu $eventId đã xử lý, đẩy việc nặng vào hàng đợi, trả 2xx ngay
http_response_code(200);
func webhook(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body) // raw body
if err != nil {
w.WriteHeader(400)
return
}
received := r.Header.Get("X-Webhook-Signature")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(raw)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(received), []byte(expected)) {
w.WriteHeader(401)
return
}
eventID := r.Header.Get("X-Webhook-Id")
_ = eventID // bỏ qua nếu đã xử lý, đẩy việc nặng vào hàng đợi
w.WriteHeader(200)
}
@PostMapping("/webhook")
public ResponseEntity<Void> webhook(
@RequestBody byte[] raw,
@RequestHeader(value = "X-Webhook-Signature", defaultValue = "") String received,
@RequestHeader(value = "X-Webhook-Id", defaultValue = "") String eventId) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(raw));
if (!MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
received.getBytes(StandardCharsets.UTF_8))) {
return ResponseEntity.status(401).build();
}
// Bỏ qua nếu eventId đã xử lý, đẩy việc nặng vào hàng đợi, trả 2xx ngay
return ResponseEntity.ok().build();
}
[HttpPost("/webhook")]
public async Task<IActionResult> Webhook()
{
using var ms = new MemoryStream();
await Request.Body.CopyToAsync(ms);
var raw = ms.ToArray(); // raw body
string received = Request.Headers["X-Webhook-Signature"].ToString();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
string expected = "sha256=" + Convert.ToHexString(h.ComputeHash(raw)).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
return Unauthorized();
string eventId = Request.Headers["X-Webhook-Id"].ToString();
// Bỏ qua nếu eventId đã xử lý, đẩy việc nặng vào hàng đợi, trả 2xx ngay
return Ok();
}
Các sự kiện
Có 9 loại sự kiện. Mỗi loại có type riêng. Cột "Cấp" cho biết data.file có giá trị (file) hay null (bộ).
type | Cấp | Khi nào |
|---|---|---|
shared_set.created | Bộ | Bộ mới được tạo xong (do bạn gọi API). |
shared_file.created | File | File được thêm vào bộ: lúc tạo bộ, hoặc thêm với fileType: "ORIGINAL". Mỗi file một sự kiện. |
shared_file.cert_statement_uploaded | File | Phòng công chứng gửi lời chứng lên bộ. Mỗi file một sự kiện. Dùng cho luồng tự ghép. |
shared_file.used | File | File được kéo vào hồ sơ công chứng hoặc chứng thực. |
shared_file.completed | File | File đã ký xong, kết quả trả về chính file đó (khối 14). |
shared_file.rejected | File | Phòng công chứng từ chối riêng một file (file còn ở trạng thái "Chờ sử dụng"). Có reason và actor. |
shared_file.sign_rejected | File | Hồ sơ ký bị từ chối giữa chừng. File quay lại để chọn vào hồ sơ khác. Có reason, actor, dossier. |
shared_set.rejected | Bộ | Phòng công chứng từ chối cả bộ. Có reason và actor. |
shared.comment_created | Bộ hoặc file | Có trao đổi mới của bất kỳ ai. data.file = null là trao đổi cấp bộ. Xem Trao đổi. |
Cấu trúc payload
Mọi sự kiện có chung một dạng data. Khóa nào không áp dụng thì có giá trị null, khóa không bao giờ biến mất.
{
"id": "evt_3f9a1c2b4d5e6f7a8b9c0d1e2f3a4b5c",
"type": "shared_file.used",
"createdAt": "2026-10-12T08:20:00.123Z",
"metadata": { "tenantCode": "ORG-DEMO" },
"data": {
"sharedSet": {
"id": 1001,
"uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34",
"code": "NS-26-000042",
"externalCode": "HD-2026-001",
"customerName": "Nguyễn Văn A",
"status": 30,
"statusLabel": "Đã phát hành"
},
"file": {
"id": 2001,
"uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12",
"code": "NS-26-000042.1",
"externalCode": "FILE-A",
"fileName": "cccd.pdf",
"documentType": 13,
"size": 12345,
"status": 32,
"statusLabel": "Đang dùng",
"signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..."
},
"occurredAt": "2026-10-12T08:20:00.000Z",
"actor": null,
"reason": null,
"dossier": null,
"comment": null
}
}
Giải thích các trường
| Tên | Kiểu | Mô tả |
|---|---|---|
| Phong bì (mọi sự kiện) | ||
id | string | Mã duy nhất của sự kiện (evt_…). Dùng để chống trùng. |
type | string | Loại sự kiện, xem bảng trên. |
createdAt | string | Lúc eNotary tạo gói tin (UTC). |
metadata.tenantCode | string | null | Mã Phòng công chứng. Hữu ích nếu một địa chỉ nhận cho nhiều đơn vị. |
data.sharedSet (luôn có) | ||
id, uuid | number, string | Số nội bộ và mã định danh của bộ. |
code | string | Mã bộ do eNotary sinh. |
externalCode | string | null | Mã của bạn. Dùng để đối chiếu với hệ thống của bạn. |
customerName | string | null | Tên khách hàng. |
status, statusLabel | number, string | Trạng thái bộ: 1 Nháp, 30 Đã phát hành, 31 Từ chối. Xem Trạng thái. |
data.file (null với sự kiện cấp bộ) | ||
id, uuid | number, string | Số nội bộ và mã định danh của file. |
code, externalCode | string | null | Mã hệ thống và mã của bạn. |
fileName | string | null | Tên file. |
documentType | number | null | Khối tài liệu, xem bảng khối. |
size | number | null | Dung lượng (byte). |
status, statusLabel | number, string | Trạng thái file: 1 Chờ sử dụng, 32 Đang dùng, 33 Hoàn thành, 34 Bị từ chối, 35 Ký bị từ chối. |
signUrl | string | null | Liên kết tải file (bản mới nhất), sống mặc định 7 ngày. Có ở mọi sự kiện cấp file khi file có nội dung. Nên tải sớm. |
Các trường còn lại trong data | ||
occurredAt | string | Lúc sự kiện xảy ra (UTC). |
actor | object | null | { id, name } của người thao tác. Chỉ có ở sự kiện từ chối. |
reason | string | null | Lý do. Có ở shared_file.rejected, shared_file.sign_rejected, shared_set.rejected. |
dossier | object | null | { id, code } của hồ sơ bị từ chối ký. Chỉ có ở shared_file.sign_rejected. |
comment | object | null | Chỉ có ở shared.comment_created. Gồm id, content (Markdown), contentHtml, createdAt, author (type, id, name). Giống đối tượng comment, riêng author có thêm id. |
Ví dụ từng sự kiện
Mỗi ví dụ chỉ hiện type và data, đã rút gọn. Các khóa không nêu vẫn có trong gói tin với giá trị null. Phần phong bì (id, createdAt, metadata) như mục trên.
shared_set.createdBộ mới tạo
{
"type": "shared_set.created",
"data": {
"sharedSet": { "id": 1001, "uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34", "code": "NS-26-000042", "externalCode": "HD-2026-001", "customerName": "Nguyễn Văn A", "status": 30, "statusLabel": "Đã phát hành" },
"file": null,
"occurredAt": "2026-10-12T08:00:00.000Z"
}
}shared_file.createdFile được thêm vào bộ
{
"type": "shared_file.created",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12", "code": "NS-26-000042.1", "externalCode": "FILE-A", "fileName": "cccd.pdf", "documentType": 13, "size": 12345, "status": 1, "statusLabel": "Chờ sử dụng", "signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..." },
"occurredAt": "2026-10-12T08:00:00.000Z"
}
}shared_file.cert_statement_uploadedPhòng công chứng gửi lời chứng
Mỗi file lời chứng một sự kiện. Nếu gửi nhiều file, gom theo data.sharedSet.uuid. Tải thẳng bằng data.file.signUrl, không cần gọi thêm API.
{
"type": "shared_file.cert_statement_uploaded",
"data": {
"sharedSet": { "id": 1001, "uuid": "3f2b8c1e-7a4d-4e5f-9b21-0c6d8e1a2f34", "code": "NS-26-000042", "externalCode": "HD-2026-001", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2010, "uuid": "e91a6b24-3c58-4d7f-b0a2-6c4f8d1e7a39", "fileName": "loi-chung.pdf", "documentType": 19, "size": 51200, "status": 1, "statusLabel": "Đã gửi", "signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..." },
"occurredAt": "2026-10-12T08:30:00.000Z"
}
}shared_file.usedFile được kéo vào hồ sơ
{
"type": "shared_file.used",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "code": "NS-26-000042.1", "fileName": "cccd.pdf", "documentType": 13, "status": 32, "statusLabel": "Đang dùng" },
"occurredAt": "2026-10-12T09:00:00.000Z"
}
}shared_file.completedKý xong, trả kết quả
Tải bản đã ký ngay bằng data.file.signUrl, hoặc dùng API tải file với data.file.uuid.
{
"type": "shared_file.completed",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12", "code": "NS-26-000042.1", "fileName": "cccd.pdf", "documentType": 14, "status": 33, "statusLabel": "Hoàn thành", "signUrl": "https://storage.example/presigned-link?X-Amz-Signature=..." },
"occurredAt": "2026-10-13T14:25:30.000Z"
}
}shared_file.rejectedPhòng công chứng từ chối một file
{
"type": "shared_file.rejected",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "code": "NS-26-000042.1", "fileName": "cccd.pdf", "status": 34, "statusLabel": "Bị từ chối" },
"occurredAt": "2026-10-12T16:00:00.000Z",
"actor": { "id": 123, "name": "Phạm Văn X" },
"reason": "CCCD hết hạn"
}
}shared_file.sign_rejectedHồ sơ ký bị từ chối, file quay lại
{
"type": "shared_file.sign_rejected",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "code": "NS-26-000042.1", "fileName": "cccd.pdf", "status": 35, "statusLabel": "Ký bị từ chối" },
"occurredAt": "2026-10-13T10:15:00.000Z",
"actor": { "id": 456, "name": "Trần Thị Y" },
"reason": "Chữ ký chưa rõ",
"dossier": { "id": 5000, "code": "HS-2026-99999" }
}
}shared_set.rejectedPhòng công chứng từ chối cả bộ
{
"type": "shared_set.rejected",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "customerName": "Nguyễn Văn A", "status": 31, "statusLabel": "Từ chối" },
"file": null,
"occurredAt": "2026-10-12T17:30:00.000Z",
"actor": { "id": 123, "name": "Phạm Văn X" },
"reason": "Không đủ điều kiện công chứng"
}
}shared.comment_created (cấp bộ)Trao đổi mới trên cả bộ
data.file là null. Trao đổi do cán bộ Phòng công chứng viết (author.type là ORG).
{
"type": "shared.comment_created",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": null,
"occurredAt": "2026-10-12T07:30:00.000Z",
"comment": {
"id": 8891,
"content": "Nhờ bổ sung **CCCD mặt sau**.",
"contentHtml": "<p>Nhờ bổ sung <strong>CCCD mặt sau</strong>.</p>",
"createdAt": "2026-10-12T07:30:00.000Z",
"author": { "type": "ORG", "id": 45, "name": "Nguyễn Văn A" }
}
}
}shared.comment_created (cấp file)Trao đổi mới trên một file
data.file là một object. Trao đổi này do chính bạn gửi qua API (author.type là API), nên hệ thống tự động của bạn cần bỏ qua.
{
"type": "shared.comment_created",
"data": {
"sharedSet": { "id": 1001, "code": "NS-26-000042", "status": 30, "statusLabel": "Đã phát hành" },
"file": { "id": 2001, "uuid": "9d1e5c70-2b3a-4c88-a6f1-5e7b0d4c9a12", "code": "NS-26-000042.1", "fileName": "cccd.pdf", "documentType": 13, "status": 1, "statusLabel": "Chờ sử dụng" },
"occurredAt": "2026-10-12T08:05:00.000Z",
"comment": {
"id": 8893,
"content": "File này gửi nhầm bản cũ, nhờ phòng **bỏ qua**.",
"contentHtml": "<p>File này gửi nhầm bản cũ, nhờ phòng <strong>bỏ qua</strong>.</p>",
"createdAt": "2026-10-12T08:05:00.000Z",
"author": { "type": "API", "id": 77, "name": "Đối tác A" }
}
}
}Gửi lại, quá giờ và chống trùng
| Cơ chế | Hành vi |
|---|---|
| Thế nào là thành công | Địa chỉ của bạn trả mã 2xx. Mã khác (kể cả 3xx), quá giờ hoặc lỗi kết nối đều tính là thất bại. |
| Thời gian chờ | 30 giây mỗi lần gửi. |
| Số lần thử | Tối đa 6 lần (gồm lần đầu). |
| Lịch gửi lại | Ngay → 1 giây → 10 giây → 60 giây → 10 phút → 1 giờ. |
| Chống trùng | Mỗi lần gửi lại giữ nguyên X-Webhook-Id. Hãy lưu mã đã xử lý và bỏ qua mã gặp lại. |
| Hết lượt | Thử đủ 6 lần vẫn thất bại thì đánh dấu EXHAUSTED. Quản trị viên eNotary xem được nhật ký và gửi lại thủ công. |
| Tự tắt khi lỗi kéo dài | Nếu có 20 sự kiện liên tiếp hết lượt mà chưa lần nào thành công, webhook của bạn bị tự tắt để tránh gửi vô ích. Một lần thành công sẽ đặt lại bộ đếm. Muốn bật lại, nhờ quản trị viên eNotary. |
Thời gian chờ, số lần thử và hạn của liên kết tải là giá trị mặc định, có thể được cấu hình theo từng môi trường.
Khuyến nghị
- Luôn kiểm tra chữ ký trước khi làm bất cứ việc gì khác.
- Trả 2xx thật nhanh. Tải file, ghi cơ sở dữ liệu… hãy đẩy sang tiến trình nền để khỏi quá 30 giây và bị gửi lại.
- Chống trùng theo
X-Webhook-Idhoặcidtrong body. - Tải file sớm.
signUrlhết hạn sau 7 ngày (mặc định). Cần lâu hơn thì dùng API tải file. - Đối chiếu khi quan trọng. Với quyết định lớn, gọi chi tiết bộ để xác nhận trạng thái hiện tại.
- Giữ kín secret như mật khẩu. Nghi bị lộ thì nhờ eNotary đổi secret.
- Chọn đúng sự kiện. Chỉ đăng ký những sự kiện bạn thật sự xử lý.
8Trạng thái
REST API trả trạng thái bộ dưới dạng chữ. Webhook trả dạng số kèm nhãn tiếng Việt.
Trạng thái của bộ
partnerStatus (REST) | status (webhook) | statusLabel | Ý nghĩa |
|---|---|---|---|
| DRAFT | 1 | Nháp | Đang tạo, Phòng công chứng chưa thấy. Bộ tạo qua API chuyển sang READY ngay. |
| READY | 30 | Đã phát hành | Phòng công chứng thấy và dùng được. |
| REJECTED | 31 | Từ chối | Phòng công chứng từ chối cả bộ. Trạng thái cuối. Không thêm file được nữa. |
Trạng thái của file
status | statusLabel | Ý nghĩa | Webhook |
|---|---|---|---|
| 1 | Chờ sử dụng | Mới nhận, chưa dùng. | shared_file.created |
| 32 | Đang dùng | Đã kéo vào một hồ sơ. | shared_file.used |
| 33 | Hoàn thành | Đã ký xong và trả kết quả. documentType chuyển thành 14. | shared_file.completed |
| 34 | Bị từ chối | Phòng công chứng từ chối riêng file này. Trạng thái cuối. | shared_file.rejected |
| 35 | Ký bị từ chối | Hồ sơ ký bị hủy giữa chừng. File được chọn lại. | shared_file.sign_rejected |
Lời chứng (documentType 19) không đi qua các bước trên. Nhãn của nó luôn là "Đã gửi".
shared_file.completed, hoặc status = 33, hoặc documentType = 14 (section = COMPLETED) khi bạn gọi chi tiết file. Nhớ rằng signStatus của bộ hiện chưa phản ánh việc ký.9Mã lỗi
Nội dung lỗi (message) là chữ gốc từ hệ thống nên có thể là tiếng Anh hoặc tiếng Việt. Hãy dựa vào mã HTTP trước.
| HTTP | message | Ý nghĩa | Cách xử lý |
|---|---|---|---|
| Xác thực | |||
| 401 | Missing X-API-Key header | Yêu cầu không có header khóa. | Thêm header X-API-Key. |
| 401 | Invalid or revoked API key | Khóa sai hoặc đã bị thu hồi. | Kiểm tra lại khóa. Xin cấp khóa mới nếu cần. |
| 401 | API key has expired | Khóa đã hết hạn. | Xin eNotary cấp khóa mới. |
| Quyền | |||
| 403 | This API key must be bound to a partner account to create shared documents | Khóa không gắn tài khoản đối tác, nên không tạo được bộ. | Nhờ eNotary gắn khóa với tài khoản đối tác. |
| 403 | This API key must be bound to a partner account to append files | Như trên, nhưng cho việc thêm file. | Như trên. |
| 403 | Access denied | Bộ hoặc file thuộc Phòng công chứng khác (khi tải file hoặc trao đổi). | Kiểm tra lại uuid và khóa bạn đang dùng. |
| Dữ liệu gửi lên | |||
| 400 | File validation failed kèm mảng errors | Có file lỗi. Không có gì được tạo. | Đọc errors[] (có index, fileName, error), sửa file lỗi rồi gửi lại. Xem bảng bên dưới. |
| 400 | Mảng chữ, ví dụ code must be a string, files must contain no more than 20 elements | Một hoặc nhiều trường sai kiểu, thiếu hoặc quá giới hạn. | Đọc từng dòng trong message và sửa trường tương ứng. |
| 400 | A shared set may contain at most 20 files | Quá 20 file. | Chia thành nhiều lần: tạo bộ rồi thêm file. |
| 400 | Invalid ecmClassId: … / Invalid ecmClassCode: … | Loại bộ không hợp lệ. | Dùng 21, 22, shared-notary-set hoặc shared-cert-set. |
| 400 | Provide one of: id, uuid, code | Chi tiết bộ thiếu tham số tìm. | Thêm một trong id, uuid, code. |
| 400 | Provide exactly one selector: uuid | code | externalCode | Chi tiết file thiếu tham số tìm. | Thêm một trong ba tham số. |
| 400 | fileType must be one of: ORIGINAL, SIGNED, MERGED hoặc …one of the following values… | fileType sai khi thêm file. | Dùng đúng một trong ba giá trị. |
| 400 | At least one file is required | Thêm file bằng multipart mà không có file. | Gửi ít nhất một file trong trường files. |
| 400 | Route này chỉ nhận: pdf, Định dạng .xxx không được phép, File rỗng hoặc không hợp lệ | Upload file không phải PDF hoặc file rỗng. | Chỉ gửi file PDF thật, có nội dung. |
| 400 | content must not be empty, Nội dung trao đổi không được để trống, Nội dung trao đổi tối đa 10000 ký tự | Trao đổi rỗng hoặc quá dài. | Viết 1 đến 10.000 ký tự. |
| 404 | Shared set not found | Không thấy bộ, hoặc bộ thuộc đơn vị khác. Thường do dùng nhầm uuid file. | Dùng uuid của bộ. Kiểm tra lại bằng chi tiết bộ. |
| 404 | File not found, File content not available, File is not part of a shared set | Không thấy file, file chưa có nội dung, hoặc file không thuộc bộ chia sẻ nào. | Kiểm tra uuid file. |
| 409 | A shared set with code "…" already exists for this organization kèm existingId | Mã code đã có. | Dùng mã khác, hoặc lấy bộ cũ bằng chi tiết bộ ?code=. |
| 409 | Shared set was rejected — cannot append files | Bộ đã bị từ chối. | Tạo bộ mới với mã khác. |
| 413 | Kích thước file vượt quá giới hạn cho phép | Upload có file quá 150 MB. | Giảm dung lượng file. |
| Giới hạn và hệ thống | |||
| 405 | Trang HTML 405 Not Allowed (nginx), không phải JSON | Gọi nhầm vào địa chỉ trang web (ví dụ admin-dev.enotaryid.com), yêu cầu không tới API. | Đổi sang base URL https://api-dev.enotaryid.com/api/integration (Dev) hoặc https://api.enotaryid.com/api/integration (Production). |
| 429 | Rate limit exceeded (60 req/min) hoặc (600 req/min) | Gọi quá nhanh. | Chờ rồi gọi lại, tăng dần thời gian chờ. |
| 429 | Đã sử dụng hết dung lượng lưu trữ… | Phòng công chứng đã hết dung lượng lưu trữ (gặp khi thêm file). | Liên hệ eNotary để được hỗ trợ. Chờ không giúp được. |
| 5xx | tùy lỗi | Lỗi phía eNotary. | Thử lại sau ít phút. Nếu kéo dài, báo cho eNotary kèm thời điểm và mã yêu cầu. |
Lỗi chi tiết của từng file (errors[].error)
{
"message": "File validation failed",
"errors": [
{ "index": 1, "fileName": "hop-dong.pdf", "error": "File is not a valid PDF (missing %PDF- header)" }
]
}
error | Ý nghĩa |
|---|---|
File is not a valid PDF (missing %PDF- header) | Nội dung không phải PDF. |
File exceeds 150 MB | File quá 150 MB. |
Total set exceeds 300 MB | Tổng các file trong một lần gửi quá 300 MB. |
Only application/pdf is accepted, got … | mimeType khai báo không phải PDF. |
Empty base64 payload, Missing base64Data, Missing url | Thiếu nội dung file. |
SSRF guard: only https URLs allowed, got … | URL không phải https. |
SSRF guard: … (địa chỉ nội bộ, không phân giải được…) | URL trỏ vào địa chỉ nội bộ hoặc không truy cập được. |
Unexpected content-type: … (expected application/pdf) | Máy chủ chứa file trả về kiểu không phải PDF. |
Fetch failed: …, HTTP 404, Too many redirects | Không tải được file từ URL (lỗi mạng, hết hạn, quá 15 giây hoặc quá 3 lần chuyển hướng). |
10Câu hỏi thường gặp
Tôi nhận lỗi 404 "Shared set not found" khi thêm file. Sai ở đâu?
Rất có thể bạn dùng uuid của file thay vì uuid của bộ. Uuid của bộ nằm ngoài cùng trong phản hồi tạo bộ (data.uuid), còn uuid của file nằm trong data.files[].
Tôi có thể tạo bộ rỗng rồi thêm file sau không?
Có. Bỏ trống files khi tạo, sau đó gọi thêm file với fileType: "ORIGINAL".
Tôi gửi lại yêu cầu tạo bộ sau khi bị đứt mạng. Có bị tạo trùng không?
Không. Mã code phải duy nhất trong Phòng công chứng, nên lần gửi lại nhận 409 kèm existingId. Bạn cũng có thể gọi chi tiết bộ với ?code= trước để biết bộ đã có chưa.
Tôi có gửi được file Word hoặc ảnh không?
Không. Khi tạo bộ hoặc thêm file, chỉ nhận PDF. Hãy đổi sang PDF trước khi gửi.
Làm sao biết một file đã được ký xong?
Nhận webhook shared_file.completed, hoặc gọi chi tiết file và xem status = 33 (hoặc documentType = 14). Trường signStatus của bộ hiện luôn là NOT_SIGNED, đừng dùng nó.
Sao tôi nhận cùng một webhook nhiều lần?
Khi địa chỉ của bạn không trả 2xx kịp, eNotary gửi lại với cùng X-Webhook-Id. Hãy lưu mã đã xử lý và bỏ qua mã trùng. Xem Gửi lại và chống trùng.
Webhook của tôi đột nhiên không đến nữa?
Nếu địa chỉ của bạn lỗi liên tục (20 sự kiện liên tiếp hết lượt gửi), hệ thống tự tắt webhook. Sửa lỗi phía bạn rồi nhờ quản trị viên eNotary bật lại. Cũng kiểm tra địa chỉ có là HTTPS công khai, và không chuyển hướng.
Hệ thống của tôi bị lặp vô hạn khi dùng trao đổi?
Webhook shared.comment_created cũng bắn cho trao đổi do chính bạn gửi qua API. Hãy bỏ qua sự kiện có data.comment.author.type === "API".
Liên kết signUrl hết hạn thì sao?
Liên kết sống mặc định 7 ngày. Hết hạn, bạn vẫn tải được bằng API tải file (dùng uuid của file và X-API-Key), hoặc gọi lại chi tiết bộ/file để nhận liên kết mới.
Tôi bị lỗi 429. Phải làm gì?
Bạn gọi quá 60 yêu cầu mỗi phút cho một khóa (hoặc 600 cho cả đơn vị). Chờ rồi gọi lại, tăng dần thời gian chờ. Nếu message nói về dung lượng lưu trữ thì đó là hết dung lượng, hãy liên hệ eNotary.
Một file có dùng được cho nhiều hồ sơ không?
Không. Khi đã "Đang dùng" trong một hồ sơ, file không chọn được cho hồ sơ khác. Cần cùng một văn bản cho nhiều hồ sơ thì gửi nhiều file.
Tôi cần giới hạn tần suất cao hơn hoặc thêm khóa API?
Liên hệ đầu mối kỹ thuật eNotary được cung cấp khi onboarding.
11Sandbox (chỉ Dev)
Tự đóng vai Phòng công chứng để test đủ trạng thái và webhook, không cần chờ ai thao tác.
api-dev.enotaryid.com, Production trả 404. Trạng thái và webhook là thật. File "hoàn thành" chỉ là bản gốc đóng dấu SANDBOX, không có chữ ký số.Cách test trong 4 bước
- Tạo bộ như bình thường bằng 4.1. Ghi lại
uuidcủa bộ và của file. - Đóng vai Phòng công chứng: gọi API giả lập với sự kiện cần test, ví dụ từ chối file:
curl -X POST "https://api-dev.enotaryid.com/api/integration/sandbox/documents/{uuid}/simulate" \ -H "X-API-Key: enk_xxxxxxxx" -H "Content-Type: application/json" \ -d '{ "event": "shared_file.rejected", "fileUuid": "{fileUuid}" }' - Kiểm tra hệ thống của bạn: nhận được webhook
shared_file.rejected, và gọi chi tiết file thấystatus = 34. - Lặp lại với sự kiện khác (gửi bản mới, trao đổi,
shared_file.usedrồishared_file.completed…). Muốn test chống trùng thì gửi lại webhook.
Lệnh cho từng sự kiện: xem lệnh mẫu. Không viết code cũng test được: dùng nút trên giao diện web.
11.1 Giả lập một sự kiện
/api/integration/sandbox/documents/{uuid}/simulateuuid là uuid của bộ. Body:
| Tên | Bắt buộc | Mô tả |
|---|---|---|
event | Có | Tên sự kiện, xem bảng dưới. |
fileUuid | Tuỳ | uuid của file, cần cho sự kiện cấp file. |
reason / content | Không | Lý do từ chối / nội dung trao đổi. |
delaySeconds | Không | Gửi webhook trễ 0–300 giây. Trạng thái vẫn đổi ngay. |
Các sự kiện
event | Làm được khi | Kết quả |
|---|---|---|
shared_file.used | File "Chờ sử dụng" hoặc "Ký bị từ chối" | Đang dùng (32) |
shared_file.completed | File "Đang dùng" | Hoàn thành (33) |
shared_file.sign_rejected | File "Đang dùng" | Ký bị từ chối (35) |
shared_file.rejected | File gốc "Chờ sử dụng" | Bị từ chối (34) |
shared_set.rejected | Bộ READY | Bộ bị từ chối (31) |
shared_file.cert_statement_uploaded | Bộ READY | Thêm 1 file lời chứng |
shared.comment_created | Bộ chưa bị từ chối | Thêm 1 trao đổi của Phòng công chứng |
Gọi sai trạng thái trả 409, không đổi gì và không có webhook. Muốn chạy lại từ đầu thì tạo bộ mới. Ví dụ kịch bản đầy đủ: làm lần lượt theo hình ở mục 1.
Lệnh mẫu (copy là chạy)
Điền 4 biến đầu một lần, sau đó chạy lệnh của sự kiện cần test. SET là uuid của bộ, FILE là uuid của file.
BASE="https://api-dev.enotaryid.com/api/integration"
KEY="enk_xxxxxxxx" # khoá API Dev của bạn
SET="uuid-cua-bo"
FILE="uuid-cua-file"
# Kéo file vào hồ sơ → shared_file.used
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.used","fileUuid":"'$FILE'"}'
# Ký xong → shared_file.completed (file phải đang "Đang dùng")
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.completed","fileUuid":"'$FILE'"}'
# Hồ sơ ký bị từ chối → shared_file.sign_rejected (file phải đang "Đang dùng")
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.sign_rejected","fileUuid":"'$FILE'","reason":"Thiếu chữ ký"}'
# Phòng công chứng từ chối file → shared_file.rejected
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.rejected","fileUuid":"'$FILE'","reason":"Thiếu trang 2"}'
# Phòng công chứng từ chối cả bộ → shared_set.rejected
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_set.rejected","reason":"Sai đơn vị"}'
# Phòng công chứng gửi lời chứng → shared_file.cert_statement_uploaded
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.cert_statement_uploaded"}'
# Phòng công chứng nhắn tin → shared.comment_created
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared.comment_created","content":"Nhờ bổ sung **CCCD**"}'
# Bất kỳ sự kiện nào, webhook đến trễ 60 giây: thêm "delaySeconds":60
curl -X POST "$BASE/sandbox/documents/$SET/simulate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"event":"shared_file.used","fileUuid":"'$FILE'","delaySeconds":60}'
# Xem webhook đã gửi của bộ
curl "$BASE/sandbox/documents/$SET/webhooks" -H "X-API-Key: $KEY"
# Gửi lại một webhook (EVT = trường "id" của webhook đã nhận)
curl -X POST "$BASE/sandbox/webhooks/$EVT/resend" -H "X-API-Key: $KEY"
11.2 Gửi lại một webhook
/api/integration/sandbox/webhooks/{eventId}/resendGửi lại đúng gói tin cũ, cùng id, để test chống trùng. eventId là trường id của webhook bạn đã nhận.
Xem các webhook đã gửi của một bộ: GET /api/integration/sandbox/documents/{uuid}/webhooks.
11.3 Test bằng giao diện web
Không cần viết code: đăng nhập web Dev, làm 4 bước dưới đây. Ảnh chụp từ môi trường Dev thật.



id).
Thay đổi gần đây
22/10/2026 Sandbox trên Dev
- Mới mục 11: tự giả lập thao tác của Phòng công chứng và gửi lại webhook trên Dev. API Production không đổi.
22/10/2026 Thêm kịch bản từ chối, gửi lại và trao đổi
- Thêm hình kịch bản đầy đủ ở mục 1: Phòng công chứng từ chối file, bạn gửi bản mới vào cùng bộ, hai bên trao đổi, rồi công chứng và ký. Mỗi bước ghi rõ API gọi và webhook nhận.
- Ghi rõ: bản gửi lại là file mới (uuid mới), từ chối cả bộ thì phải tạo bộ mới, phát hiện sai sau khi file đã vào hồ sơ thì nhận
shared_file.sign_rejected. - API không thay đổi.
21/10/2026 Thêm hướng dẫn tránh lỗi thường gặp
- Nhấn mạnh không gọi API qua địa chỉ trang web quản trị (
admin-dev…): sẽ nhận405 Not Allowed. Thêm dòng405vào bảng mã lỗi. - Thêm hướng dẫn điền form-data trong Postman cho API tạo bộ bằng multipart: key
files,titlesdạng mảng JSON, bắt buộccode. - API không thay đổi.
12/10/2026 Trao đổi chuyển sang Markdown
- Gửi trao đổi: body
{ "content": "…" }nay nhận Markdown (tối đa 10.000 ký tự). Khi hiển thị chỉ giữ thẻ định dạng an toàn; script, ảnh và style bị loại bỏ. - Ý nghĩa của
contentđã đổi: trước đây là HTML đã escape. Nay là Markdown gốc. Trao đổi cũ (viết trước ngày này) giữ nguyên HTML đã lưu. - Trường mới trong API trao đổi và webhook
shared.comment_created(data.comment):contentHtml(HTML đã lọc, hiển thị thẳng được, áp dụng cho cả trao đổi cũ lẫn mới). - Khi bạn gửi trao đổi, cán bộ Phòng công chứng xử lý bộ nhận thông báo trong ứng dụng. Bạn không cần làm gì thêm.
- Việc cần làm: nếu bạn từng hiển thị
contentnhư HTML, hãy chuyển sang dùngcontentHtml.
12/10/2026 Viết lại tài liệu và đính chính
- Tài liệu được viết lại: thêm địa chỉ Dev/Production, mục lục, sơ đồ và ví dụ cho từng API.
- Đính chính:
partnerStatuslà chữ (DRAFT,READY,REJECTED), không phải số. Danh sách bộ dùngpageSize(không phảilimit). Giới hạn body JSON là 150 MB.signStatushiện luôn làNOT_SIGNED. Phản hồi thêm file nằm trong phong bì (datalà mảng).signUrltrong webhook có ở mọi sự kiện cấp file. - Bổ sung: webhook tự tắt sau 20 sự kiện liên tiếp hết lượt gửi.