@swype-org/checkout
v0.2.7
Published
Lightweight merchant checkout SDK — open a hosted payment flow, handle completion, zero dependencies
Maintainers
Readme
@swype-org/checkout
Lightweight merchant checkout SDK — open a hosted Swype payment flow, handle completion, zero runtime dependencies.
Quick Start
npm install @swype-org/checkoutimport { Checkout } from '@swype-org/checkout';
const checkout = new Checkout({ signer: '/api/sign-payment' });
document.getElementById('deposit-btn')!.addEventListener('click', async () => {
try {
const { transfer } = await checkout.requestDeposit({
amount: 50,
chainId: 8453,
address: '0x...', // destination wallet
token: 'USDC',
});
console.log('Transfer complete:', transfer.id, transfer.status);
} catch (error) {
console.error('Deposit failed:', error);
}
});Important:
requestDeposit()must be called from a user-gesture handler (click, keypress, etc.) so the browser allows the popup to open.
React Hook
A dedicated React entry point eliminates all boilerplate:
npm install @swype-org/checkout reactimport { useSwypeCheckout } from '@swype-org/checkout/react';
function DepositButton() {
const { status, result, error, displayMessage, requestDeposit } = useSwypeCheckout({
signer: '/api/sign-payment',
});
return (
<>
<button
onClick={() =>
requestDeposit({ amount: 50, chainId: 8453, address: '0x...', token: 'USDC' })
}
disabled={status === 'signer-loading'}
>
{status === 'signer-loading' ? 'Preparing…' : 'Deposit'}
</button>
{error && <p>{displayMessage}</p>}
{result && <p>Transfer {result.transfer.id} complete!</p>}
</>
);
}The hook returns reactive status, result, error, displayMessage, and isActive values, plus requestDeposit, focus, and close actions. It manages the Checkout lifecycle and cleans up on unmount.
How It Works
Merchant App / SDK Merchant Signer Hosted Flow (popup)
│ │ │
│ 1. requestDeposit(request) │ │
│──────────────────────────────► │ │
│ │ 2. signer(data) or POST URL │
│ │ (includes webviewBaseUrl) │
│ 3. { signature, payload, ...} │ │
│◄───────────────────────────────│ │
│ │
│ 4. SDK builds URL, opens popup │
│───────────────────────────────────────────────────────────────► │
│ │
│ 5. User completes payment in hosted flow │
│ │
│ 6. postMessage: swype:transfer-complete │
│◄────────────────────────────────────────────────────────────── │
│ │
│ 7. Promise resolves with DepositResult │Signer Contract
The signer config option controls how the SDK obtains a signed payment link. It accepts either a URL string or a custom function, giving you full control over authentication, HTTP method, and request shape.
Using a URL string (simple)
When signer is a string, the SDK sends a POST with a JSON body to that URL and expects a SignerResponse back. This is the simplest integration:
const checkout = new Checkout({ signer: '/api/sign-payment' });Using a custom function (full control)
When signer is a function, the SDK calls it with a SignerRequest object and expects a Promise<SignerResponse>. Use this when you need control over the HTTP method, authentication, request transformation, or any other aspect of the signing call:
import type { SignerFunction } from '@swype-org/checkout';
const checkout = new Checkout({
signer: async (data) => {
const res = await fetch('/api/sign-payment', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: JSON.stringify({ ...data, orderId: 'order-123' }),
});
if (!res.ok) throw new Error(`Signer error: ${res.status}`);
return res.json();
},
});SignerRequest (input)
The data the SDK passes to your signer (as the JSON body for URL mode, or as the function argument for function mode):
| Field | Type | Description |
| --- | --- | --- |
| amount | number | USD amount to deposit (always > 0). |
| chainId | number | EVM chain ID for the destination (e.g. 8453 for Base). |
| address | string | Destination wallet address (0x-prefixed, 40 hex chars). |
| token | string | Token symbol on the destination chain (e.g. "USDC"). |
| callbackScheme | string \| null | Custom URL scheme for mobile deep-link callbacks. Always null for web. |
| url | string | Base webview URL the SDK will navigate to. Provided for logging/validation — your signer does not construct the final URL. |
| version | string | Protocol version (currently "v1"). |
| reference | string? | Merchant order or invoice ID for reconciliation. |
| metadata | object? | Arbitrary key-value pairs forwarded from the merchant app. |
Example:
{
"amount": 50,
"chainId": 8453,
"address": "0x...",
"token": "USDC",
"callbackScheme": null,
"url": "https://webview-app-staging.staging-swype.network",
"version": "v1",
"reference": "order-123",
"metadata": { "invoiceId": "INV-456" }
}What the signer must do
- Validate the request fields.
- Generate an idempotency key (UUID) for this payment.
- Build a payload — a base64url-encoded JSON string containing the payment parameters:
{
"amount": 50,
"chainId": 8453,
"address": "0x...",
"token": "USDC",
"idempotencyKey": "generated-uuid",
"callbackScheme": null,
"expiresAt": "2026-03-07T12:00:00Z",
"version": "v1"
}- Sign the payload string with your merchant private key (SHA-256) and base64url-encode the signature.
SignerResponse (output)
The response your signer must return (as JSON for URL mode, or as the resolved value for function mode):
| Field | Type | Description |
| --- | --- | --- |
| merchantId | string | Your merchant UUID. |
| payload | string | Base64url-encoded payment payload (see above). |
| signature | string | Base64url-encoded signature of the payload string. |
| expiresAt | string | ISO 8601 expiration timestamp for this payment link. |
| preview | object | Echo of the payment parameters for client-side display. |
| preview.amount | number | Deposit amount. |
| preview.chainId | number | Destination chain ID. |
| preview.address | string | Destination wallet address. |
| preview.token | string | Destination token symbol. |
| preview.idempotencyKey | string | The generated idempotency key. |
Example:
{
"merchantId": "uuid",
"payload": "base64url-encoded-payload",
"signature": "base64url-encoded-signature",
"expiresAt": "2026-03-07T12:00:00Z",
"preview": {
"amount": 50,
"chainId": 8453,
"address": "0x...",
"token": "USDC",
"idempotencyKey": "uuid"
}
}The SDK constructs the hosted flow URL by appending merchantId, payload, and signature as query parameters to the webviewBaseUrl, then navigates the popup to that URL. When the user completes payment the hosted flow sends a postMessage back and the SDK resolves the promise.
Configuration
const checkout = new Checkout({
// Required: URL string or custom async function (see "Signer Contract")
signer: '/api/sign-payment',
// Optional: base URL of the hosted payment webview app.
// Default: 'https://webview-app-staging.staging-swype.network'
webviewBaseUrl: 'https://webview-app-staging.staging-swype.network',
// Optional: origin of the hosted flow page (for postMessage validation).
// Derived from webviewBaseUrl when omitted.
hostedFlowOrigin: 'https://webview-app-staging.staging-swype.network',
// Optional: window.open feature string
popupFeatures: 'popup=yes,width=460,height=860',
// Optional: window name for the popup
popupWindowName: 'checkout-hosted-flow',
// Optional: max ms to wait for signer response (default: 15000)
signerTimeoutMs: 15_000,
// Optional: max ms for entire flow (signer + user completion)
flowTimeoutMs: 300_000,
// Optional: enable debug logging to console
debug: false,
});Observable Status
The Checkout instance exposes a reactive status property:
| Status | Meaning |
| ---------------- | ------------------------------------------- |
| idle | No active flow |
| signer-loading | Calling the merchant signer endpoint |
| popup-active | Hosted flow is open, waiting for user |
| completed | Transfer succeeded |
| error | Something failed |
checkout.on('status-change', (status) => {
console.log('Status:', status);
});
// Also available synchronously:
checkout.status; // current status
checkout.result; // last DepositResult (when completed)
checkout.error; // last CheckoutError (when error)
checkout.isActive; // true during signer-loading or popup-activeError Handling
Every error is a CheckoutError with a machine-readable code:
| Code | Meaning |
| ------------------------ | ----------------------------------------------- |
| POPUP_BLOCKED | Browser blocked the popup window |
| POPUP_CLOSED | User closed the popup before completing |
| SIGNER_REQUEST_FAILED | Signer returned a non-2xx response |
| SIGNER_NETWORK_ERROR | Network failure reaching the signer |
| SIGNER_RESPONSE_INVALID| Signer response missing required fields |
| SIGNER_TIMEOUT | Signer did not respond within signerTimeoutMs |
| FLOW_TIMEOUT | Entire flow exceeded flowTimeoutMs |
| INVALID_REQUEST | Bad input (amount, address, etc.) |
Use getDisplayMessage() for user-facing strings:
import { CheckoutError, getDisplayMessage } from '@swype-org/checkout';
try {
await checkout.requestDeposit({ ... });
} catch (err) {
if (err instanceof CheckoutError) {
showToast(getDisplayMessage(err));
}
}Events
checkout.on('complete', (result) => { /* DepositResult */ });
checkout.on('error', (error) => { /* CheckoutError */ });
checkout.on('close', () => { /* popup closed */ });
checkout.on('status-change', (status) => { /* CheckoutStatus */ });Lifecycle
// Focus the popup (e.g. when user clicks "Deposit" while flow is active)
checkout.focus();
// Close the popup without waiting for completion
checkout.close();
// Tear down and release all resources (call on unmount)
checkout.destroy();Metadata / Order Reference
Pass merchant-specific data through the flow for reconciliation:
await checkout.requestDeposit({
amount: 50,
chainId: 8453,
address: '0x...',
token: 'USDC',
reference: 'order-123',
metadata: { invoiceId: 'INV-456' },
});The reference and metadata are forwarded to your signer endpoint so you can correlate the payment with your internal records.
TypeScript
All types are exported:
import type {
CheckoutConfig,
CheckoutStatus,
DepositRequest,
DepositResult,
SignerFunction,
SignerRequest,
SignerResponse,
TransferSummary,
} from '@swype-org/checkout';
import type { CheckoutErrorCode } from '@swype-org/checkout';