Widget pembayaran

Widget pembayaran

Hantar seseorang ke halaman Atli yang selamat untuk membayar atau menambah kad, apabila aplikasi anda tidak dapat memaparkan borang Stripe sendiri.

Bila hendak menggunakannya

  • Pembantu AI dan klien MCP, yang bercakap dengan orang dalam sembang dan tidak mempunyai skrin sendiri untuk meletakkan borang kad.

  • Membayar di luar aliran, seperti bot dalam Telegram atau WhatsApp, atau menyelesaikannya pada peranti lain atau kemudian.

  • Mana-mana klien lain yang tidak dapat memuatkan SDK Stripe, seperti terminal atau aplikasi suara.

PCI DSS
Borang halaman pembayaran, dengan kad dan Apple Pay

setup_intent

Orang itu menyimpan kad ke akaunnya tanpa membayar sekarang, supaya klien anda boleh membeli dengannya kemudian.

  1. Mulakan penambahan kad

    POST /user/payment-methods/setup-intent menjawab dengan tiga medan di sebelah client_secret.

    Untuk kad kumpulan, gunakan POST /groups/:groupId/cards/setup dan POST /groups/:groupId/cards/confirm dengan cara yang sama. Jawapannya menamakan id itu 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…"
    }
  2. Hantar pautan kepada orang itu

    Letakkan ketiga-tiga nilai itu dalam pautan ke halaman pembayaran, dengan type ditetapkan kepada setup_intent. Orang itu hanya melihat borang 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();
  3. Simpan kad

    POST /user/payment-methods/confirm menyimpan kad ke akaun apabila orang itu selesai. Kemudian GET /user/payment-methods menyenaraikannya.

    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 dalam pautan

ParameterWajibFungsinya
typeYa

payment_intent atau setup_intent. Ia mesti sepadan dengan jenis rahsia yang dimeterai.

sealed_secretYa

Nilai daripada respons, tepat seperti yang diterima.

key_idYa

Nilai daripada respons. Ia menamakan kunci tempat rahsia itu dimeterai.

seal_signatureYa

Nilai daripada respons. Halaman memeriksanya dan menolak pautan yang nilainya tiada atau tidak sepadan.

optionsTidak

options daripada respons, dikodkan dalam base64url. Buat masa ini ia mengandungi mesej yang dipaparkan halaman di atas borang, dalam bahasa permintaan anda. Ia hanya ada apabila API ada sesuatu untuk dipaparkan.

options_signatureTidak

Nilai daripada respons, tanpa diubah, bersama options. Jika ia tidak sepadan, halaman mengabaikan pilihan itu dan berfungsi seperti biasa.

langTidak

Bahasa halaman, salah satu daripada 15 bahasa laman ini. Tanpanya, bahasa yang dipilih orang itu di heyatli.com, kemudian bahasa pelayar.

themeTidak

dark atau light. Secara lalai, mengikut pelayar.

brandTidak

none menyembunyikan logo dan label Atli. Secara lalai, kedua-duanya dipaparkan.

redirect_success_urlTidak

Ke mana orang itu dihantar selepas itu. Mesti https pada heyatli.com atau salah satu subdomainnya.

redirect_cancel_urlTidak

Ke mana orang itu dihantar selepas itu. Mesti https pada heyatli.com atau salah satu subdomainnya.

redirect_failure_urlTidak

Ke mana orang itu dihantar selepas itu. Mesti https pada heyatli.com atau salah satu subdomainnya.

return_idTidak

Rujukan anda sendiri, ditambah pada pengubahan hala. Huruf, nombor, _ dan -, sehingga 128 aksara.

Cara mengendalikan pautan

  • Hantar nilai tanpa mengubahnya. URLSearchParams mengendalikan pengekodan.

  • Hantar juga pilihan halaman. Jika respons mengandungi options dan options_signature, tambah kedua-duanya: options dikodkan base64url, options_signature seperti yang diterima. Jangan ubah JSON sebelum mengekodnya.

  • Kekal dalam satu persekitaran. Nilai Sandbox hanya dibuka di staging.heyatli.com/pay, dan nilai Production hanya di heyatli.com/pay.

  • Layan seperti kata laluan. Hantar hanya kepada pemilik akaun, dan jangan biarkan ia masuk ke dalam log.

  • Satu pesanan pada satu masa. Pembelian baharu membatalkan pesanan tertunda sebelumnya dan pautannya, jadi jangan simpan pautan.

  • Jika ketiga-tiga medan tiada, buat panggilan sekali lagi. Jangan sekali-kali letak client_secret biasa dalam pautan.

  • Ubah hala tidak membuktikan apa-apa. Percaya hanya pada semakan dalam langkah terakhir.

Cara penyulitan dan tandatangan berfungsi

Anda tidak memerlukan semua ini untuk menghantar pautan. Ia ada supaya anda dapat melihat apa yang berlaku dan menyemak apa yang dikembalikan pelayan. Kedua-duanya menggunakan kunci awam, pada alamat ini untuk persekitaran yang anda pilih:

Halaman pembayaran
https://heyatli.com/pay
Kunci penandatanganan
https://api.heyatli.com/keys

Penyulitan

sealed_secret ialah client_secret sebagai JWE padat, format yang sudah selamat dalam URL. Ia disulitkan kepada kunci awam X25519 laman, jadi hanya pelayan laman yang boleh membacanya. key_id ialah id kunci itu, dan sepadan dengan kid 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 dalam senarai. Dalam kod, ia 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);

Tandatangan

Oleh kerana sesiapa boleh menyulitkan, nilai yang dimeterai tidak menunjukkan siapa yang mencipta. Jadi API menandatanganinya dengan kunci peribadi Ed25519 yang hanya dipegangnya. Mesej yang ditandatangani ialah bait UTF-8 bagi tiga baris ini, digabungkan dengan baris baharu, tanpa baris baharu di hujung:

atli-seal-v1
<key_id>
<sealed_secret>

Beginilah API membentuk tandatangan, dengan kunci peribadi yang tidak pernah keluar daripadanya:

// 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 ialah tandatangan mentah 64 bait dalam base64url tanpa lapik, iaitu 86 aksara. Setiap persekitaran menandatangani dengan kuncinya sendiri, dan senarai boleh mengandungi dua semasa penggiliran.

Halaman pembayaran memeriksa ini sebelum membuka apa-apa, dan menolak pautan yang tidak mempunyainya atau tidak sepadan. Untuk menyemak sendiri apa yang dikembalikan pelayan, bina semula mesej itu dan sahkan tandatangan 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 merosakkan tandatangan. Ia tidak meliputi type, pengubahan hala atau parameter lain, dan tidak membuktikan bahawa orang yang membuka pautan memiliki akaun itu. Pautan bertandatangan terus berfungsi selagi Stripe menerima rahsia itu.

Pilihan halaman

API memulangkan options sebagai teks JSON. Ia mengandungi message, teks biasa sehingga 300 aksara, dan lang, bahasa mesej itu. Kunci lain mungkin ditambah, dan halaman mengabaikan kunci yang tidak dikenalinya. Mesej mengikut bahasa permintaan anda: X-Locale, Accept-Language atau ?lang=, seperti di seluruh API. Dalam pautan ia berbentuk base64url, jadi tidak perlu dilepaskan dan kekal pendek dalam apa jua bahasa. Pilihan hanyalah tambahan: jika tiada atau gagal disemak, halaman tetap menerima bayaran tanpanya.

// response.options, as the API returns it
{"lang":"en","message":"Plan for Australia"}

options_signature dibuat dengan kunci yang sama ke atas empat baris ini, dengan <options> ialah JSON tepat seperti yang dipulangkan API. Halaman menyahkod nilai daripada pautan dan menyemaknya. Oleh sebab rahsia yang dimeterai termasuk di dalamnya, pilihan yang dipindahkan ke pautan lain tidak akan lulus:

atli-options-v1
<key_id>
<sealed_secret>
<options>