Tiện ích thanh toán
Đưa ai đó đến một trang Atli an toàn để thanh toán hoặc thêm thẻ, khi ứng dụng của bạn không thể tự hiển thị biểu mẫu của Stripe.
Khi nào nên dùng
Trợ lý AI và ứng dụng khách MCP, vốn trò chuyện với mọi người trong khung chat và không có màn hình riêng để đặt biểu mẫu thẻ.
Thanh toán ngoài luồng, chẳng hạn bot trên Telegram hoặc WhatsApp, hoặc hoàn tất trên thiết bị khác hay để sau.
Bất kỳ ứng dụng khách nào khác không thể tải SDK của Stripe, chẳng hạn terminal hoặc ứng dụng giọng nói.

setup_intent
Người dùng lưu một thẻ vào tài khoản mà không thanh toán ngay, để ứng dụng khách của bạn có thể mua bằng thẻ đó sau.
Bắt đầu thêm thẻ
POST /user/payment-methods/setup-intenttrả lời kèm ba trường bên cạnhclient_secret.Với thẻ của một nhóm, hãy dùng
POST /groups/:groupId/cards/setupvàPOST /groups/:groupId/cards/confirmtheo cách tương tự. Phản hồi của chúng gọi id làsetup_intent_id.curl -X POST https://api.heyatli.com/user/payment-methods/setup-intent \ -H 'Authorization: Bearer YOUR_TOKEN' { "id": "seti_…", "client_secret": "seti_…_secret_…", "sealed_secret": "eyJhbGciOiJFQ0RILUVTIi…", "key_id": "QvWE2gHWYExKj2Yc…", "seal_signature": "c2lnbmF0dXJl…", "options": "{\"lang\":\"en\",\"message\":\"Plan for Australia\"}", "options_signature": "b3B0aW9ucw…" }Gửi cho người dùng một liên kết
Đặt ba giá trị vào một liên kết đến trang thanh toán, với
typelàsetup_intent. Người dùng chỉ thấy biểu mẫu của Stripe, không có số tiền.const link = new URL('https://heyatli.com/pay'); link.search = new URLSearchParams({ type: 'setup_intent', sealed_secret: response.sealed_secret, key_id: response.key_id, seal_signature: response.seal_signature, return_id: response.id, // Only when the response has them, and always together. options goes in // base64url-encoded (Buffer in Node; TextEncoder and btoa in a browser). ...(response.options && { options: Buffer.from(response.options).toString('base64url'), options_signature: response.options_signature, }), }).toString();Lưu thẻ
POST /user/payment-methods/confirmlưu thẻ vào tài khoản khi người dùng đã xong. Sau đóGET /user/payment-methodssẽ liệt kê thẻ đó.curl https://api.heyatli.com/user/payment-methods/confirm \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "setup_intent_id": "seti_…" }'
Những gì nằm trong liên kết
| Tham số | Bắt buộc | Tác dụng |
|---|---|---|
| type | Có |
|
| sealed_secret | Có | Giá trị từ phản hồi, đúng như khi nhận được. |
| key_id | Có | Giá trị từ phản hồi. Nó cho biết khóa mà bí mật được niêm phong cho. |
| seal_signature | Có | Giá trị từ phản hồi. Trang sẽ kiểm tra nó và từ chối liên kết thiếu hoặc có giá trị không khớp. |
| options | Không |
|
| options_signature | Không | Giá trị từ phản hồi, giữ nguyên, đi kèm |
| lang | Không | Ngôn ngữ của trang, một trong 15 ngôn ngữ của trang web này. Nếu không có, dùng ngôn ngữ người đó đã chọn trên heyatli.com, rồi đến ngôn ngữ của trình duyệt. |
| theme | Không |
|
| brand | Không |
|
| redirect_success_url | Không | Nơi chuyển người dùng đến sau đó. Phải là |
| redirect_cancel_url | Không | Nơi chuyển người dùng đến sau đó. Phải là |
| redirect_failure_url | Không | Nơi chuyển người dùng đến sau đó. Phải là |
| return_id | Không | Mã tham chiếu riêng của bạn, được thêm vào địa chỉ chuyển hướng. Chữ cái, chữ số, |
Cách xử lý liên kết
Truyền nguyên các giá trị.
URLSearchParamssẽ lo phần mã hóa.Chuyển cả tùy chọn trang. Khi phản hồi có
optionsvàoptions_signature, hãy thêm cả hai:optionsmã hóa base64url,options_signaturegiữ nguyên. Đừng sửa JSON trước khi mã hóa.Giữ trong một môi trường. Giá trị từ Sandbox chỉ mở được trên
staging.heyatli.com/pay, còn từ Production chỉ mở được trênheyatli.com/pay.Hãy coi nó như mật khẩu. Chỉ gửi cho chủ tài khoản và đừng để nó vào nhật ký.
Mỗi lần một đơn hàng. Giao dịch mua mới sẽ hủy đơn đang chờ trước đó và liên kết của đơn, nên đừng lưu liên kết.
Nếu thiếu ba trường này, hãy gọi lại. Đừng bao giờ đưa
client_secretchưa mã hóa vào liên kết.Chuyển hướng không chứng minh được gì. Chỉ tin vào bước kiểm tra cuối cùng.
Mã hóa và chữ ký hoạt động thế nào
Bạn không cần điều nào trong số này để gửi liên kết. Chúng ở đây để bạn xem điều gì xảy ra và kiểm tra những gì máy chủ trả về. Cả hai đều dùng khóa công khai, tại các địa chỉ này cho môi trường bạn đã chọn:
- Trang thanh toán
- https://heyatli.com/pay
- Khóa mã hóa
- https://heyatli.com/api/keys
- Khóa ký
- https://api.heyatli.com/keys
Mã hóa
sealed_secret là client_secret dưới dạng JWE gọn, một định dạng vốn đã an toàn trong URL. Nó được mã hóa bằng khóa công khai X25519 của trang web, nên chỉ máy chủ của trang web đọc được. key_id là id của khóa đó, và khớp với kid của JWE.
- alg
- ECDH-ES
- enc
- A256GCM
- kid
- key_id
- Key
- JWK: kty OKP, crv X25519, x = public_key
API làm việc này cho bạn, dùng khóa đầu tiên trong danh sách. Trong mã, nó là:
import { CompactEncrypt, importJWK } from 'jose';
const { keys: [key] } = await (await fetch('https://heyatli.com/api/keys')).json();
const publicKey = await importJWK(
{ kty: 'OKP', crv: 'X25519', x: key.public_key },
'ECDH-ES'
);
const sealedSecret = await new CompactEncrypt(new TextEncoder().encode(clientSecret))
.setProtectedHeader({ alg: 'ECDH-ES', enc: 'A256GCM', kid: key.id })
.encrypt(publicKey);Chữ ký
Vì ai cũng có thể mã hóa, một giá trị đã niêm phong không cho biết ai đã tạo ra nó. Vì vậy API ký nó bằng khóa riêng Ed25519 mà chỉ API nắm giữ. Thông điệp được ký là các byte UTF-8 của ba dòng này, nối với nhau bằng dấu xuống dòng, không có dấu nào ở cuối:
atli-seal-v1
<key_id>
<sealed_secret>API tạo chữ ký như sau, bằng khóa riêng không bao giờ rời khỏi API:
// What the API does. Only it holds the private key.
const message = new TextEncoder().encode(
`atli-seal-v1\n${keyId}\n${sealedSecret}`
);
const signature = await crypto.subtle.sign('Ed25519', privateKey, message);
const sealSignature = Buffer.from(signature).toString('base64url');seal_signature là chữ ký thô 64 byte dạng base64url không đệm, tức 86 ký tự. Mỗi môi trường ký bằng khóa riêng, và danh sách có thể chứa hai khóa trong lúc luân chuyển.
Trang thanh toán kiểm tra điều này trước khi mở bất cứ thứ gì, và từ chối liên kết thiếu hoặc không khớp. Để tự kiểm tra những gì máy chủ trả về, hãy dựng lại thông điệp đó và xác minh chữ ký bằng các khóa ký:
// In Node. sealedSecret, keyId and sealSignature come from the API response.
const { keys } = await (await fetch('https://api.heyatli.com/keys')).json();
const base64url = (value) => Uint8Array.from(Buffer.from(value, 'base64url'));
const message = new TextEncoder().encode(
`atli-seal-v1\n${keyId}\n${sealedSecret}`
);
// The list can hold two keys during a rotation: one match is enough.
const checks = keys.map(async (key) =>
crypto.subtle.verify(
'Ed25519',
await crypto.subtle.importKey('raw', base64url(key.public_key), 'Ed25519', false, ['verify']),
base64url(sealSignature),
message
)
);
const valid = (await Promise.all(checks)).some(Boolean);Đổi sealed_secret hoặc key_id sẽ làm hỏng chữ ký. Chữ ký không bao gồm type, các địa chỉ chuyển hướng hay các tham số khác, và không chứng minh người mở liên kết là chủ tài khoản. Liên kết đã ký vẫn dùng được chừng nào Stripe còn chấp nhận bí mật.
Tùy chọn trang
API trả về options dưới dạng văn bản JSON. Nó chứa message, văn bản thuần tối đa 300 ký tự, và lang, ngôn ngữ của thông báo. Có thể có thêm khóa khác, và trang bỏ qua những khóa nó không biết. Thông báo theo ngôn ngữ của yêu cầu: X-Locale, Accept-Language hoặc ?lang=, như ở mọi nơi trong API. Trong liên kết, nó ở dạng base64url nên không cần thoát ký tự và luôn ngắn với mọi ngôn ngữ. Tùy chọn chỉ là phần thêm: nếu thiếu hoặc không qua kiểm tra, trang vẫn nhận thanh toán mà không có chúng.
// response.options, as the API returns it
{"lang":"en","message":"Plan for Australia"}options_signature được tạo bằng cùng khóa, trên bốn dòng này, trong đó <options> là JSON đúng như API trả về. Trang giải mã giá trị trong liên kết và kiểm tra nó. Vì bí mật đã niêm phong là một phần của nó, tùy chọn chuyển sang liên kết khác sẽ không hợp lệ:
atli-options-v1
<key_id>
<sealed_secret>
<options>