# @delopay/sdk

TypeScript SDK for the [Delopay](https://delopay.net) payments API. Zero dependencies, works in Node 18+ and browsers.

## Installation

```bash
pnpm add @delopay/sdk
```

```bash
npm install @delopay/sdk
```

```bash
yarn add @delopay/sdk
```

## Quick Start

```typescript
import { Delopay } from '@delopay/sdk';

const delopay = new Delopay(process.env.DELOPAY_API_KEY!);

const payment = await delopay.payments.create({
  amount: 1000, // in minor units (€10.00)
  currency: 'EUR',
  description: 'Order #1234',
  customer_id: 'cus_abc123',
});

console.log(payment.payment_id, payment.status);
```

## Configuration

```typescript
const delopay = new Delopay(apiKey, {
  sandbox: true, // Use https://sandbox.delopay.net (default: false → production)
  baseUrl: 'https://…', // Override base URL entirely
  timeout: 30_000, // Request timeout in ms (default: 30 000)
});
```

**API keys:**

- `prd_…` / `snd_…` — server-side secret key. Full API access. Keep this private.
- `pk_prd_…` / `pk_snd_…` — client-side publishable key. Restricted to browser-safe operations.

## Usage Examples

### Create a payment

> **Never send raw card numbers through the SDK.** Delopay's API is not a
> raw-PAN endpoint: cards are collected on the Delopay **hosted checkout**
> (or via a payment link), so card data never touches your server and stays
> out of your PCI scope. Server-side you create the payment and hand the buyer
> off; you confirm server-side only with a saved `payment_token`, a
> `mandate_id`, or a redirect method (e.g. a PayPal wallet) — never card data.

```typescript
// Recommended: hosted checkout via a payment link.
const payment = await delopay.payments.create({
  amount: 2500,
  currency: 'EUR',
  payment_link: true,
  customer_id: 'cus_abc123',
  description: 'Order #1234',
  return_url: 'https://example.com/checkout/complete',
});

// Send the buyer here — they pick a method and enter card details on the
// hosted page. Delopay handles 3-D Secure and redirects.
console.log(payment.payment_link?.link);

// Fulfil on the payment_succeeded webhook, or re-check server-side:
const final = await delopay.payments.retrieve(payment.payment_id);
```

Confirm server-side **without card data** — with a saved token (off-session):

```typescript
const confirmed = await delopay.payments.confirm(pending.payment_id, {
  payment_token: savedPaymentToken, // from paymentMethods.listForCustomer()
  off_session: true,
  return_url: 'https://example.com/checkout/complete',
});

console.log(confirmed.status); // 'succeeded' | 'requires_customer_action' | …
```

Or with a redirect payment method that involves no card data at all:

```typescript
const paypal = await delopay.payments.create({
  amount: 2500,
  currency: 'EUR',
  confirm: true,
  payment_method: 'wallet',
  payment_method_type: 'paypal',
  payment_method_data: { wallet: { paypal_redirect: {} } },
  return_url: 'https://example.com/checkout/complete',
});
// paypal.next_action?.redirect_to_url → send the buyer there to approve
```

### Create a refund

```typescript
const refund = await delopay.refunds.create({
  payment_id: 'pay_abc123',
  amount: 1000, // partial refund; omit for full refund
  reason: 'Customer request',
});

console.log(refund.refund_id, refund.status);
```

### Manage customers

```typescript
const customer = await delopay.customers.create({
  name: 'Jane Doe',
  email: 'jane@example.com',
  metadata: { plan: 'pro' },
});

// List saved payment methods
const { customer_payment_methods } = await delopay.paymentMethods.listForCustomer(
  customer.customer_id,
);

// Use a saved method on a new payment
const payment = await delopay.payments.create({
  amount: 1000,
  currency: 'EUR',
  customer_id: customer.customer_id,
  payment_token: customer_payment_methods[0]?.payment_token,
  confirm: true,
});
```

### Handle disputes

```typescript
// Disputes for one payment come from the payment object:
const payment = await delopay.payments.retrieve('pay_abc123');

for (const dispute of payment.disputes ?? []) {
  console.log(dispute.dispute_id, dispute.dispute_stage, dispute.dispute_status);
}

// Or list disputes across the account, filtered by status:
const open = await delopay.disputes.list({ dispute_status: 'dispute_opened' });
```

### Inspect why a payment failed

```typescript
// Every attempt on a payment — including retries across connectors — with its
// full failure detail (raw code + Delopay-unified, human-readable reason).
const { size, data } = await delopay.payments.listAttempts('pay_abc123');

for (const attempt of data) {
  // e.g. "stripe failure — Insufficient funds (51)"
  console.log(
    `${attempt.connector ?? 'unknown'} ${attempt.status} — ` +
      `${attempt.unified_message ?? attempt.error_message ?? 'no error'}` +
      `${attempt.error_code ? ` (${attempt.error_code})` : ''}`,
  );
}
```

### Manage shops and gateways

```typescript
// Create a shop (business profile)
const shop = await delopay.shops.create(merchantId, {
  shop_name: 'My Online Store',
  webhook_url: 'https://example.com/webhooks/delopay',
  return_url: 'https://example.com/checkout/complete',
});

// Connect Stripe as a payment gateway
const gateway = await delopay.shops.gateways.connect(merchantId, shop.shop_id, {
  connector_type: 'payment_processor',
  connector_name: 'stripe',
  connector_account_details: {
    auth_type: 'HeaderKey',
    api_key: process.env.STRIPE_SECRET_KEY,
  },
  test_mode: true,
});

// List connected gateways
const gateways = await delopay.shops.gateways.list(merchantId, shop.shop_id);
```

### Subscriptions

Recurring billing runs through a billing processor connected to the shop (Stripe
Billing or PayPal). Every subscription call is **profile-scoped** — pass the
shop's `X-Profile-Id` so the backend can resolve the billing processor (you get
`IR_04` otherwise):

```typescript
const opts = { headers: { 'X-Profile-Id': profileId } };
```

**Browse plans and estimate cost** before creating anything:

```typescript
// List plans (or addons) with their prices. An item the processor lists but
// cannot sell right now comes back with `available: false` and no prices;
// skip it rather than offering it.
const plans = await delopay.subscriptions.getItems({ item_type: 'plan' }, opts);
const priceId = plans.find((item) => item.available)?.price_id[0]?.price_id;
if (!priceId) {
  throw new Error('No purchasable plan on this shop');
}

// Preview what the customer will be charged
const estimate = await delopay.subscriptions.getEstimate({ item_price_id: priceId }, opts);
console.log(estimate.amount, estimate.currency, estimate.interval); // 1500 'EUR' 'Month'
```

> **Never send raw card numbers.** The subscription API rejects
> `payment_method_data.card` with a raw PAN. Cards are collected client-side by
> the connector's hosted fields (Stripe Elements) so the card never touches your
> server, keeping raw card data out of your PCI scope. Confirm with a hosted
> checkout session or a previously-saved token, as shown below.

**Recommended: hosted checkout.** Create the subscription server-side, then send
the buyer to the Delopay hosted checkout with the returned `client_secret`. The
buyer enters their card in the connector iframe; you never handle the PAN:

```typescript
const pending = await delopay.subscriptions.create(
  {
    item_price_id: priceId,
    customer_id: 'cus_abc123',
    payment_details: { return_url: 'https://example.com/subscription/complete' },
  },
  opts,
);

// Redirect the buyer to the hosted checkout to enter their card.
const checkoutUrl =
  `https://checkout.delopay.net/pay/${merchantId}/${pending.id}` +
  `?cs=${encodeURIComponent(pending.client_secret ?? '')}`;
// → res.redirect(checkoutUrl)

// Activation arrives via the subscription/invoice webhooks; never trust the
// client. Reconcile with subscriptions.retrieve(pending.id, opts).
```

**Saved payment method (off-session).** If the customer already has a saved,
tokenized payment method, confirm server-side with the token — still no PAN:

```typescript
const sub = await delopay.subscriptions.createAndConfirm(
  {
    item_price_id: priceId,
    customer_id: 'cus_abc123',
    payment_details: {
      payment_method: 'card',
      payment_method_id: savedPaymentMethodId, // token, not a card number
      setup_future_usage: 'off_session',
      return_url: 'https://example.com/subscription/complete',
    },
  },
  opts,
);

if (sub.redirect_url) {
  // Some processors (e.g. PayPal) still need buyer approval — redirect there.
} else {
  console.log(sub.status); // 'active'
}
```

You can also split create and confirm — call `subscriptions.confirm(id, …)` with
the `client_secret` and a `payment_token` once the buyer has a token. Same rule:
a `payment_token` / `payment_method_id`, never a raw card.

**Manage the lifecycle.** Pause, resume, and cancel take optional timing and
proration controls; called with no body they act immediately:

```typescript
await delopay.subscriptions.pause(sub.id, { pause_option: 'end_of_term' }, opts);
await delopay.subscriptions.resume(sub.id, undefined, opts);
await delopay.subscriptions.cancel(
  sub.id,
  { cancel_option: 'immediately', credit_option_for_current_term_charges: 'prorate' },
  opts,
);

// Retrieve one, or list for the profile
const current = await delopay.subscriptions.retrieve(sub.id, opts);
const all = await delopay.subscriptions.list({ limit: 20 }, opts);
```

Each billing cycle raises an invoice (`sub.invoice`) with its own payment leg
(`sub.payment`); track cycle outcomes via the subscription/invoice webhooks.

### Platform fee rules

Price the platform fee by payment method, connector, amount, currency or card
network. Build the rule program with `feeProgram()` — rules are tried in order,
first match wins, otherwise the default applies:

```typescript
import { Delopay, feeProgram } from '@delopay/sdk';

const delopay = new Delopay(process.env.DELOPAY_API_KEY ?? '');

const algorithm = feeProgram()
  .rule({ name: 'crypto', when: { paymentMethod: 'crypto' }, fee: { percentage: 1.0 } })
  .rule({
    name: 'card_on_cryptomus',
    when: { paymentMethod: 'card', connector: 'cryptomus' },
    fee: { percentage: 2.0 },
  })
  .otherwise({ percentage: 3.0 })
  .build();

await delopay.fees.rules.upsert({ algorithm }, 'merchant_abc123');

const program = await delopay.fees.rules.retrieve('merchant_abc123'); // or null
await delopay.fees.rules.delete('merchant_abc123'); // revert to flat schedules
```

Merchants without a rule program keep their existing flat fee schedules / volume
tier unchanged.

### Webhook verification

Delopay signs each outgoing webhook with HMAC-SHA512 over the raw request body and delivers the hex-encoded digest in the `X-Webhook-Signature-512` header. Use `express.raw()` (not `express.json()`) so the bytes reach the verifier unchanged.

The verified event matches the wire body: `{ merchant_id, event_id, event_type, content: { type, object }, timestamp }`. `event_type` says what happened (e.g. `'payment_succeeded'`); `content.type` tags the payload kind (e.g. `'payment_details'`) — narrow on it to get a typed `content.object` (the payment/refund/dispute, with `payment_id` etc.).

```typescript
import express from 'express';
import { Delopay } from '@delopay/sdk';

app.post('/webhooks/delopay', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.header('x-webhook-signature-512') ?? '';
  const secret = process.env.DELOPAY_WEBHOOK_SECRET!;

  let event;
  try {
    event = await Delopay.webhooks.verify(req.body, signature, secret);
  } catch {
    return res.status(400).send('Invalid signature');
  }

  if (event.content.type === 'payment_details') {
    const payment = event.content.object; // typed: PaymentResponse
    switch (event.event_type) {
      case 'payment_succeeded':
        // fulfil order — payment.payment_id, payment.amount, payment.currency
        break;
      case 'payment_failed':
        // notify customer — payment.error_message
        break;
    }
  }

  res.json({ received: true });
});
```

## Error Handling

All errors are instances of `DelopayError`:

```typescript
import { Delopay, DelopayError, DelopayAuthenticationError } from '@delopay/sdk';

try {
  const payment = await delopay.payments.retrieve('pay_does_not_exist');
} catch (err) {
  if (err instanceof DelopayAuthenticationError) {
    // status: 401 — invalid or missing API key
    console.error('Check your API key');
  } else if (err instanceof DelopayError) {
    console.error(err.message); // human-readable message
    console.error(err.status); // HTTP status code
    console.error(err.code); // machine-readable error code
    console.error(err.type); // error category (e.g. 'not_found')
    console.error(err.data); // structured context for select codes (see below)
  }
}
```

**Error classes:**

| Class                        | When                               |
| ---------------------------- | ---------------------------------- |
| `DelopayError`               | Base class for all API errors      |
| `DelopayAuthenticationError` | `401` — invalid or missing API key |

Network timeouts throw `DelopayError` with `code: 'TIMEOUT'`. Network failures throw with `code: 'NETWORK'`.

An export (`delopay.export.*`, `disputes.workspaceExport`) is held to the record count the server announces in `X-Delopay-Record-Count`: a file that arrives with a different number of records — or ends part-way through a line or a JSON document — throws with `code: 'EXPORT_INCOMPLETE'` (`err.data` is `{ expected_records, received_records }`) instead of resolving to a file that is missing rows. Pass `onProgress` in an export's options to follow the download, and `signal` to cancel it.

**Structured error context (`err.data`):** populated for a small set of codes that benefit from a machine-readable hint. Currently:

| `err.code` | `err.data` shape               | Meaning                                                                    |
| ---------- | ------------------------------ | -------------------------------------------------------------------------- |
| `UR_48`    | `{ retry_after_secs: number }` | TOTP attempt counter locked out — wait this many seconds before retrying.  |
| `UR_63`    | `{ retry_after_secs: number }` | Auth-endpoint rate limit tripped — wait this many seconds before retrying. |

## TypeScript

The SDK is written in strict TypeScript. All request and response shapes are fully typed. Import types directly when needed:

```typescript
import type { PaymentResponse, PaymentCreateRequest, Currency } from '@delopay/sdk';
```

## Environments

| Environment | Base URL                      | API key prefix       |
| ----------- | ----------------------------- | -------------------- |
| Production  | `https://api.delopay.net`     | `prd_…` / `pk_prd_…` |
| Sandbox     | `https://sandbox.delopay.net` | `snd_…` / `pk_snd_…` |

```typescript
// Sandbox
const delopay = new Delopay(process.env.DELOPAY_API_KEY!, { sandbox: true });
```

## License

MIT
