npm package: @spacepay/client-sdk
Description: Lightweight SDK for SpacePay-like API - works in Node 18+ and modern browsers
Author: SpacePay
Latest version: 1.0.24

# SpacePay Client SDK

A lightweight, TypeScript-first SDK for integrating with SpacePay-like crypto payment gateways. Works in Node.js 18+ and modern browsers.

## Features

- 🚀 **Lightweight** - No heavy dependencies, minimal bundle size
- 🔒 **Type-safe** - Full TypeScript support with comprehensive type definitions
- 🌐 **Universal** - Works in Node.js 18+ and modern browsers
- ⚡ **Modern** - Uses native fetch API and modern JavaScript features
- 🛡️ **Robust** - Built-in error handling, timeout management, and retry logic

This package focuses on **external merchant APIs**: secret-key payments (server-side) and payment-secret flows (browser-safe checkout client).

## Installation

```bash
yarn add @spacepay/client-sdk
```

## Backend (Node.js)

```typescript
import { createBackendClient } from '@spacepay/client-sdk/backend'

const client = createBackendClient({
  apiBaseUrl: 'https://api.app.spacepay.co.uk', // optional
  publicKey: 'your_public_key_here',
  secretKey: 'your_secret_key_here', // make sure to never expose this key on frontend
})

// Create a payment
const payment = await client.createPayment({
  amount: 5000, // 5000 cents = $50.00
  currency: 'USD',
  orderId: 'order_123',
})

console.log('Payment:', payment)
console.log('Payment URL:', payment.paymentUrl)
console.log('Payment Secret:', payment.secret)
console.log('Payment ID:', payment.paymentId)
```

## Frontend (TypeScript / bundlers)

### Hosted Checkout

For the most simple integration, redirect the user to `paymentUrl` returned from the `createPayment` request. After the payment completes, the user is redirected back to the success URL you set when creating the payment. For this flow, you do not need to load the frontend SDK at all.

### Embedded Checkout

You can embed the SpacePay checkout UI into your own checkout page. We support 2 options, simple payment button, or full payment modal experience.

You can render a create order button and lazily create the payment on first click using fetchPaymentContext:

```ts
import { initPayment } from '@spacepay/client-sdk/frontend'

const checkoutButton = await initPayment({
  mode: 'embedded-button', // embedded-button, embedded-checkout, redirect
  fetchPaymentContext: async () => {
    // Called the first time the user clicks the \"Pay with crypto\" button
    const res = await fetch('/api/create-spacepay-order', { method: 'POST' })
    const { paymentId, paymentSecretKey } = await res.json()
    return { paymentId, paymentSecretKey }
  },
  onSuccess: (payload) => {
    // Called only in embedded-button and embedded-checkout modes
    console.log('Payment success', payload)
  },
})

checkoutButton.mount('#spacepay-checkout')
```

Alternatively, you can manage order lifecycle yourself and pass the payment details to the SDK:

```ts
import {
  initEmbeddedCheckoutButton,
  initEmbeddedCheckoutModal,
} from '@spacepay/client-sdk/frontend'

// Option 1 - Embedded button (inline iframe)
const checkoutButton = await initEmbeddedCheckoutButton({
  paymentId: 'payment_id_from_backend',
  paymentSecretKey: 'payment_secret_from_backend',
  onClose: (payload) => {
    console.log('Payment closed', payload)
  },
  onSuccess: (payload) => {
    console.log('Payment success', payload)
  },
  onError: (payload) => {
    console.log('Payment error', payload)
  },
})

checkoutButton.mount('#spacepay-checkout')
```

```ts
// Option 2 - Embedded full checkout in a modal
import {
  initEmbeddedCheckoutButton,
  initEmbeddedCheckoutModal,
} from '@spacepay/client-sdk/frontend'

const checkoutModal = await initEmbeddedCheckoutModal({
  paymentId: 'payment_id_from_backend',
  paymentSecretKey: 'payment_secret_from_backend',
  onClose: (payload) => {
    console.log('Payment closed', payload)
  },
  onSuccess: (payload) => {
    console.log('Payment success', payload)
  },
  onError: (payload) => {
    console.log('Payment error', payload)
  },
})

checkoutModal.mount()
```

### Show payment details (optional)

Use the frontend checkout client for browser-safe usage (no secret key), and build your own UI. For they payment itself, redirect the user to `paymentUrl` (Hosted Checkout) or use Embedded Checkout option (below).

```ts
import {
  createCheckoutClient,
  initEmbeddedCheckoutButton,
  initEmbeddedCheckoutModal,
} from '@spacepay/client-sdk/frontend'

const paymentClient = createCheckoutClient({
  // Optional, defaults to https://api.app.spacepay.co.uk
  apiBaseUrl: 'https://api.app.spacepay.co.uk',
  publicKey: 'pk_...',
  paymentSecret: 'ps_...',
})

// Check payment status using payment client
const paymentStatus = await paymentClient.getPaymentStatus(paymentId)
console.log('Payment status:', paymentStatus.status)

// Get payment details using payment client
const paymentDetails = await paymentClient.getPaymentDetails(paymentId)
console.log('Payment details:', paymentDetails)
console.log('Deposit address:', paymentDetails.depositAddress?.address)

// Get active quotes for the payment using payment client
const quotes = await paymentClient.getActiveQuotes(paymentId)
console.log('Active quotes:', quotes)
```

## CDN / Browser (global)

If you prefer a `<script>` tag, you can use the prebuilt browser bundle:

```html
<script src="https://pub-e21c1fba794f48a6b4ec1facf5d68801.r2.dev/latest/spacepay-full.bundle.min.js"></script>
<script>
  ;(async function () {
    const checkout = await window.SpacePaySDK.initEmbeddedCheckoutButton({
      paymentId: 'payment_id_from_backend',
      paymentSecretKey: 'payment_secret_from_backend',
      onClose: (payload) => {
        console.log('Payment closed', payload)
      },
      onSuccess: (payload) => {
        console.log('Payment success', payload)
      },
      onError: (payload) => {
        console.log('Payment error', payload)
      },
    })

    checkout.mount('#spacepay-checkout')
  })()
</script>
```

The same bundle also exposes `SpacePaySDK.createBackendClient` and `SpacePaySDK.createCheckoutClient` for backend and frontend integration.

## API Reference

### Backend client options

```typescript
type BackendClientOptions = {
  apiBaseUrl?: string // defaults to https://api.app.spacepay.co.uk
  publicKey: string // Merchant public (access) key; sent as X-SpacePay-Access-Key
  secretKey: string // Sent as X-SpacePay-Secret-Key; never expose on the frontend
  timeoutMs?: number // default 30000
}
```

### Checkout client options

```typescript
type CheckoutClientOptions = {
  apiBaseUrl?: string
  publicKey: string
  paymentSecret: string // Sent as X-SpacePay-Payment-Secret
  timeoutMs?: number
}
```

### Creating payments

```typescript
interface CreatePaymentRequest {
  orderId: string
  currency: Currency // e.g. Currency.USD — only USD is supported by the API
  amount?: number // For default type `payment`: required by the API, minimum 250 cents. Ignored for type `deposit`.
  type?: 'payment' | 'deposit' // default `payment`
  redirectUrl?: string
  customMetadata?: string
}

const payment = await backendClient.createPayment({
  amount: 10000, // 10000 cents = $100.00 (must be ≥ 250 for type payment)
  currency: Currency.USD,
  orderId: 'order_456',
})
```

Optionally pass `{ idempotencyKey: '...' }` as the second argument so retries of the same logical create reuse the same key and the server can deduplicate.

### Listing payments

```typescript
const page = await backendClient.listPayments({
  status: 'pending',
  limit: 50,
  offset: 0,
})
// page.data — PaymentDto[]; page.pagination — totals / hasMore, etc.
```

### Getting payment details

Use **`getPaymentDetails`** to load the full payment record, including **`status`**.

```typescript
const details = await backendClient.getPaymentDetails('payment_id_here')
```

### Creating withdrawals

Send funds from the merchant balance to a recipient address. Minimum **`amountInCents`** is **100**.

```typescript
interface CreateWithdrawalRequest {
  amountInCents: number // minimum 100
  chainId: number
  recipientAddress: string
  orderId?: string
  customMetadata?: string
  currency?: Currency // API defaults to USD
}

const withdrawal = await backendClient.createWithdrawal(
  {
    amountInCents: 10_000,
    chainId: 1,
    recipientAddress: '0x...',
    orderId: 'internal_ref_optional',
    currency: Currency.USD,
  },
  { idempotencyKey: 'optional-stable-key' }
)
```

For withdrawals, use the same optional `{ idempotencyKey: '...' }` second argument when you need retry-safe creates.

### Listing withdrawals

```typescript
const withdrawalsPage = await backendClient.listWithdrawals({
  status: 'pending',
  limit: 50,
  offset: 0,
})
// withdrawalsPage.data — WithdrawalDto[]; withdrawalsPage.pagination — same shape as payments
```

### Getting withdrawal details

```typescript
const one = await backendClient.getWithdrawalDetails(withdrawal.id)
```

### Payment status and refresh (checkout client)

Use the checkout client with the **payment secret** for **`getPaymentStatus`** and **`refreshPaymentStatus`** when polling or refreshing payment state from the browser.

```typescript
const checkout = createCheckoutClient({
  publicKey: 'pk_...',
  paymentSecret: 'from_create_payment_response',
})
const status = await checkout.getPaymentStatus('payment_id_here')
// { id, status }

await checkout.refreshPaymentStatus('payment_id_here', {
  transactionHash: '0x...', // all optional
})
```

### Quotes (checkout client)

```typescript
// Default quote (no contract / chain — per API)
await checkout.createOrUpdateQuote(paymentId, {})

// Specific token
await checkout.createOrUpdateQuote(paymentId, {
  contractAddress: '0x...',
  chainId: 1,
})
```

### Payment (incl. deposits) and withdrawal states:

Payment `status` (and `PaymentStatusDto.status`):

- `pending` — created, awaiting funds
- `processing` — transfer observed / confirming
- `completed` — settled successfully
- `failed` — failed or rejected
- `expired` — expired before completion
- `cancelled` — cancelled

Withdrawal `status` (on `WithdrawalDto`):

- `pending` — created, awaiting processing
- `processing` — in progress
- `completed` — settled successfully
- `failed` — failed or rejected
- `cancelled` — cancelled

## SDK Development

### Prerequisites

- Node.js 18+
- npm or yarn

### Setup

```bash
# Install dependencies
yarn install

# Build the project (includes format checking)
yarn build

# Development mode with watch
yarn dev

# Clean build artifacts
yarn clean

# Format code with Prettier
yarn format

# Check code formatting
yarn format:check

# Lint code with ESLint
yarn lint

# Fix linting issues automatically
yarn lint:fix
```

### Project Structure

```
src/
├── index.ts              # Re-exports frontend + backend factories
├── frontend.ts           # Browser-safe entry (checkout client, embeds, utils)
├── backend.ts            # Node / server entry (secret key client)
├── client/               # HTTP clients
│   ├── base-client.ts
│   ├── backend-client.ts
│   ├── checkout-client.ts
│   ├── spacepay.ts
│   └── index.ts
├── embedded/             # Embedded checkout / wallet UI helpers
├── types/
│   ├── index.ts
│   ├── payment.ts
│   ├── config.ts
│   └── errors.ts
└── utils/
    ├── index.ts
    ├── formatting.ts
    ├── url.ts
    └── validation.ts
```

### Code Quality

This project uses both [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/) for code quality:

**Prettier** - Code formatting (`.prettierrc`):

**ESLint** - Code linting (`eslint.config.js`)

The build process automatically checks both formatting and linting, ensuring code quality standards are met.

## Browser Support

This SDK works in modern browsers that support:

- Fetch API
- AbortController

For older browsers, you may need to include polyfills for these features.

## Node.js Support

- **Node.js 18+**: Full support with native fetch API
- **Node.js 16-17**: Requires `node-fetch` polyfill
- **Node.js <16**: Not supported

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request

## Support

For support and questions:

- Create an issue on GitHub
- Check the examples directory
- Review the API documentation