Widget pembayaran
Arahkan seseorang ke halaman Atli yang aman untuk membayar atau menambahkan kartu, saat aplikasi Anda tidak bisa menampilkan formulir Stripe sendiri.
Kapan memakainya
Asisten AI dan klien MCP, yang berbicara dengan orang lewat obrolan dan tidak punya layar sendiri untuk menaruh formulir kartu.
Membayar di luar alur, misalnya bot di Telegram atau WhatsApp, atau menyelesaikannya di perangkat lain atau nanti.
Klien lain mana pun yang tidak bisa memuat SDK Stripe, seperti terminal atau aplikasi suara.

setup_intent
Orang tersebut menyimpan kartu ke akunnya tanpa membayar sekarang, supaya klien Anda bisa membeli dengan kartu itu nanti.
Mulai menambahkan kartu
POST /user/payment-methods/setup-intentmenjawab dengan tiga kolom di sampingclient_secret.Untuk kartu grup, pakai
POST /groups/:groupId/cards/setupdanPOST /groups/:groupId/cards/confirmdengan cara yang sama. Jawabannya menyebut id itusetup_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…" }Kirim tautan kepada orangnya
Taruh ketiga nilai itu dalam tautan ke halaman pembayaran, dengan
typediatur kesetup_intent. Orang tersebut hanya melihat formulir Stripe, tanpa jumlah.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();Simpan kartu
POST /user/payment-methods/confirmmenyimpan kartu ke akun setelah orang tersebut selesai. LaluGET /user/payment-methodsmencantumkannya.curl https://api.heyatli.com/user/payment-methods/confirm \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "setup_intent_id": "seti_…" }'
Apa yang masuk ke tautan
| Parameter | Wajib | Fungsinya |
|---|---|---|
| type | Ya |
|
| sealed_secret | Ya | Nilai dari respons, persis seperti yang diterima. |
| key_id | Ya | Nilai dari respons. Ini menyebut kunci tempat rahasia disegel. |
| seal_signature | Ya | Nilai dari respons. Halaman memeriksanya dan menolak tautan yang nilainya hilang atau tidak cocok. |
| options | Tidak |
|
| options_signature | Tidak | Nilai dari respons, tanpa diubah, bersama |
| lang | Tidak | Bahasa halaman, salah satu dari 15 bahasa situs ini. Tanpanya, bahasa yang dipilih orang itu di heyatli.com, lalu bahasa browser. |
| theme | Tidak |
|
| brand | Tidak |
|
| redirect_success_url | Tidak | Ke mana orang tersebut diarahkan setelahnya. Harus |
| redirect_cancel_url | Tidak | Ke mana orang tersebut diarahkan setelahnya. Harus |
| redirect_failure_url | Tidak | Ke mana orang tersebut diarahkan setelahnya. Harus |
| return_id | Tidak | Referensi Anda sendiri, ditambahkan ke pengalihan. Huruf, angka, |
Cara memperlakukan tautan
Teruskan nilainya tanpa diubah.
URLSearchParamsmenangani pengodeannya.Teruskan juga opsi halaman. Jika respons berisi
optionsdanoptions_signature, tambahkan keduanya:optionsdienkode base64url,options_signatureapa adanya. Jangan mengubah JSON sebelum mengenkodenya.Tetap di satu lingkungan. Nilai Sandbox hanya terbuka di
staging.heyatli.com/pay, dan nilai Production hanya diheyatli.com/pay.Perlakukan seperti kata sandi. Kirim hanya ke pemilik akun, dan jangan biarkan masuk ke log.
Satu pesanan dalam satu waktu. Pembelian baru membatalkan pesanan tertunda sebelumnya beserta tautannya, jadi jangan simpan tautan.
Jika ketiga kolom itu tidak ada, lakukan panggilan lagi. Jangan pernah menaruh
client_secretbiasa di tautan.Pengalihan tidak membuktikan apa pun. Percayai hanya pemeriksaan di langkah terakhir.
Cara kerja enkripsi dan tanda tangan
Anda tidak memerlukan semua ini untuk mengirim tautan. Ini ada agar Anda bisa melihat apa yang terjadi dan memeriksa apa yang dikembalikan server. Keduanya memakai kunci publik, di alamat berikut untuk lingkungan yang Anda pilih:
- Halaman pembayaran
- https://heyatli.com/pay
- Kunci enkripsi
- https://heyatli.com/api/keys
- Kunci penandatanganan
- https://api.heyatli.com/keys
Enkripsi
sealed_secret adalah client_secret sebagai JWE ringkas, format yang sudah aman di URL. Isinya dienkripsi ke kunci publik X25519 situs, jadi hanya server situs yang bisa membacanya. key_id adalah id kunci itu, dan cocok dengan kid milik JWE.
- alg
- ECDH-ES
- enc
- A256GCM
- kid
- key_id
- Key
- JWK: kty OKP, crv X25519, x = public_key
API melakukannya untuk Anda, dengan kunci pertama dari daftar. Dalam kode, bentuknya seperti ini:
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);Tanda tangan
Karena siapa pun bisa mengenkripsi, nilai yang disegel tidak menunjukkan siapa pembuatnya. Maka API menandatanganinya dengan kunci privat Ed25519 yang hanya dimilikinya. Pesan yang ditandatangani adalah byte UTF-8 dari tiga baris ini, digabung dengan baris baru, tanpa baris baru di akhir:
atli-seal-v1
<key_id>
<sealed_secret>Beginilah API membentuk tanda tangan, dengan kunci privat yang tidak pernah keluar darinya:
// 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 adalah tanda tangan mentah 64 byte dalam base64url tanpa padding, yaitu 86 karakter. Setiap lingkungan menandatangani dengan kuncinya sendiri, dan daftar bisa memuat dua kunci selama rotasi.
Halaman pembayaran memeriksa ini sebelum membuka apa pun, dan menolak tautan yang tidak memilikinya atau tidak cocok. Untuk memeriksa sendiri apa yang dikembalikan server, susun ulang pesan itu dan verifikasi tanda tangannya dengan kunci penandatanganan:
// 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);Mengubah sealed_secret atau key_id merusak tanda tangan. Tanda tangan tidak mencakup type, pengalihan, atau parameter lain, dan tidak menunjukkan bahwa orang yang membuka tautan adalah pemilik akun. Tautan bertanda tangan tetap berfungsi selama Stripe menerima rahasianya.
Opsi halaman
API mengembalikan options sebagai teks JSON. Isinya message, teks biasa hingga 300 karakter, dan lang, bahasa pesannya. Kunci lain bisa ditambahkan, dan halaman mengabaikan kunci yang tidak dikenalnya. Pesan mengikuti bahasa permintaan Anda: X-Locale, Accept-Language, atau ?lang=, seperti di seluruh API. Di tautan, nilainya base64url sehingga tidak perlu di-escape dan tetap pendek dalam bahasa apa pun. Opsi hanyalah tambahan: jika tidak ada atau gagal diperiksa, halaman tetap menerima pembayaran tanpanya.
// response.options, as the API returns it
{"lang":"en","message":"Plan for Australia"}options_signature dibuat dengan kunci yang sama atas empat baris ini, dengan <options> adalah JSON persis seperti yang dikembalikan API. Halaman mendekode nilai dari tautan dan memeriksanya. Karena segel termasuk di dalamnya, opsi yang dipindahkan ke tautan lain tidak akan lolos:
atli-options-v1
<key_id>
<sealed_secret>
<options>