Widżet płatności
Wyślij kogoś na bezpieczną stronę Atli, by zapłacił lub dodał kartę, gdy Twoja aplikacja nie może sama pokazać formularza Stripe.
Kiedy z tego korzystać
Asystenci AI i klienci MCP, którzy rozmawiają z ludźmi na czacie i nie mają własnego ekranu na formularz karty.
Płacenie poza przepływem, na przykład przez bota w Telegramie lub WhatsAppie, albo dokończenie na innym urządzeniu lub później.
Każdy inny klient, który nie może załadować SDK Stripe, na przykład terminal lub aplikacja głosowa.

setup_intent
Osoba zapisuje kartę na swoim koncie bez płacenia teraz, żeby Twój klient mógł nią później kupować.
Rozpocznij dodawanie karty
POST /user/payment-methods/setup-intentodpowiada trzema polami obokclient_secret.Dla karty grupy użyj
POST /groups/:groupId/cards/setupiPOST /groups/:groupId/cards/confirmw ten sam sposób. W ich odpowiedziach identyfikator nazywa się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…" }Wyślij osobie link
Umieść te trzy wartości w linku do strony płatności, z
typeustawionym nasetup_intent. Osoba zobaczy tylko formularz Stripe, bez kwoty.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();Zapisz kartę
POST /user/payment-methods/confirmzapisuje kartę na koncie, gdy osoba skończy. PotemGET /user/payment-methodsją wyświetla.curl https://api.heyatli.com/user/payment-methods/confirm \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "setup_intent_id": "seti_…" }'
Co trafia do linku
| Parametr | Wymagany | Co robi |
|---|---|---|
| type | Tak |
|
| sealed_secret | Tak | Wartość z odpowiedzi, dokładnie taka, jaka przyszła. |
| key_id | Tak | Wartość z odpowiedzi. Wskazuje klucz, dla którego zaszyfrowano sekret. |
| seal_signature | Tak | Wartość z odpowiedzi. Strona ją sprawdza i odrzuca link, w którym jej brakuje lub się nie zgadza. |
| options | Nie |
|
| options_signature | Nie | Wartość z odpowiedzi, bez zmian, razem z |
| lang | Nie | Język strony, jeden z 15 używanych w tej witrynie. Bez niego język wybrany przez tę osobę na heyatli.com, a potem język przeglądarki. |
| theme | Nie |
|
| brand | Nie |
|
| redirect_success_url | Nie | Dokąd wysłać osobę potem. Musi to być |
| redirect_cancel_url | Nie | Dokąd wysłać osobę potem. Musi to być |
| redirect_failure_url | Nie | Dokąd wysłać osobę potem. Musi to być |
| return_id | Nie | Twój własny identyfikator, dodany do przekierowania. Litery, cyfry, |
Jak traktować link
Przekazuj wartości bez zmian. Kodowaniem zajmuje się
URLSearchParams.Przekaż też opcje strony. Jeśli odpowiedź zawiera
optionsioptions_signature, dodaj oba:optionszakodowane w base64url,options_signaturebez zmian. Nie zmieniaj JSON-a przed zakodowaniem.Trzymaj się jednego środowiska. Wartości z Sandbox otwierają się tylko na
staging.heyatli.com/pay, a z Production tylko naheyatli.com/pay.Traktuj go jak hasło. Wysyłaj go tylko właścicielowi konta i nie zapisuj w logach.
Jedno zamówienie naraz. Nowy zakup anuluje wcześniejsze oczekujące zamówienie i jego link, więc nie przechowuj linków.
Jeśli brakuje trzech pól, wykonaj wywołanie ponownie. Nigdy nie umieszczaj jawnego
client_secretw linku.Przekierowanie niczego nie dowodzi. Ufaj tylko sprawdzeniu w ostatnim kroku.
Jak działają szyfrowanie i podpis
Nic z tego nie jest potrzebne do wysłania linku. Jest tu, byś mógł zobaczyć, co się dzieje, i sprawdzić, co zwraca serwer. Oba używają kluczy publicznych, pod tymi adresami dla wybranego środowiska:
- Strona płatności
- https://heyatli.com/pay
- Klucze szyfrowania
- https://heyatli.com/api/keys
- Klucze podpisujące
- https://api.heyatli.com/keys
Szyfrowanie
sealed_secret to client_secret jako zwarty JWE, format już bezpieczny w adresie URL. Jest zaszyfrowany kluczem publicznym X25519 witryny, więc odczyta go tylko jej serwer. key_id to id tego klucza i odpowiada kid w JWE.
- alg
- ECDH-ES
- enc
- A256GCM
- kid
- key_id
- Key
- JWK: kty OKP, crv X25519, x = public_key
API robi to za Ciebie, używając pierwszego klucza z listy. W kodzie wygląda to tak:
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);Podpis
Ponieważ każdy może szyfrować, zaszyfrowana wartość nie pokazuje, kto ją utworzył. Dlatego API podpisuje ją prywatnym kluczem Ed25519, który ma tylko ono. Podpisana wiadomość to bajty UTF-8 tych trzech wierszy, połączonych znakiem nowego wiersza, bez żadnego na końcu:
atli-seal-v1
<key_id>
<sealed_secret>Tak API tworzy podpis, kluczem prywatnym, który nigdy go nie opuszcza:
// 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 to surowy 64-bajtowy podpis w base64url bez dopełnienia, czyli 86 znaków. Każde środowisko podpisuje własnym kluczem, a podczas rotacji lista może zawierać dwa.
Strona płatności sprawdza to, zanim cokolwiek otworzy, i odrzuca link, w którym tego brakuje lub się nie zgadza. Aby samodzielnie sprawdzić, co zwrócił serwer, odtwórz tę wiadomość i zweryfikuj podpis kluczami podpisującymi:
// 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);Zmiana sealed_secret lub key_id psuje podpis. Nie obejmuje on type, przekierowań ani pozostałych parametrów i nie dowodzi, że osoba otwierająca link jest właścicielem konta. Podpisany link działa tak długo, jak Stripe akceptuje sekret.
Opcje strony
API zwraca options jako tekst JSON. Zawiera on message, zwykły tekst do 300 znaków, oraz lang, język wiadomości. Mogą dojść kolejne klucze, a nieznane strona pomija. Wiadomość jest w języku Twojego zapytania: X-Locale, Accept-Language lub ?lang=, jak wszędzie w API. W linku jest w base64url, więc nie wymaga escapowania i pozostaje krótka w każdym języku. Opcje to dodatek: jeśli ich brak lub nie przejdą weryfikacji, strona przyjmie płatność bez nich.
// response.options, as the API returns it
{"lang":"en","message":"Plan for Australia"}options_signature powstaje tym samym kluczem nad tymi czterema wierszami, gdzie <options> to JSON dokładnie w takiej postaci, w jakiej zwróciło go API. Strona dekoduje wartość z linku i sprawdza właśnie ją. Pieczęć jest częścią podpisu, więc opcje przeniesione do innego linku nie przejdą:
atli-options-v1
<key_id>
<sealed_secret>
<options>