@delopay/sdk
TypeScript SDK for the Delopay payments API. Zero dependencies, works in Node 18+ and browsers.
Installation
pnpm add @delopay/sdk
npm install @delopay/sdk
yarn add @delopay/sdk
Quick Start
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
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, amandate_id, or a redirect method (e.g. a PayPal wallet) — never card data.
// 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):
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:
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
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
const customer = await delopay.customers.create({
name: 'Jane Doe',
email: '[email protected]',
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,
});
Sell from the catalog
// A product belongs to a shop; every call names the shop last.
const product = await delopay.products.create(
{
name: 'Annual report',
kind: 'one_time',
prices: [{ amount: 2999, currency: 'EUR', is_default: true }],
},
'pro_abc123',
);
// Price a payment from the catalog: no amount, no currency — the server
// freezes the product's active default price (or a named `price_id`) onto it.
const payment = await delopay.payments.create({
product_id: product.id,
profile_id: 'pro_abc123',
payment_link: true,
});
// What the payment sold, as it was when it was created — unchanged by later
// catalog edits or deletion.
const sold = await delopay.payments.productLines(payment.payment_id);
Issue invoices
// Invoices and credit notes in your name, from a paid payment or subscription
// cycle. Amounts always come from the source.
const invoice = await delopay.invoices.create(
{ source: { payment_id: 'pay_abc123' }, finalize: true },
{ headers: { 'Idempotency-Key': 'invoice-order-1001' } },
);
const pdf = await delopay.invoices.downloadPdf(invoice.id);
// How they look and are taxed: the merchant default, or one shop's override.
const current = await delopay.invoiceSettings.retrieveForShop('pro_abc123');
await delopay.invoiceSettings.updateForShop('pro_abc123', { ...current, locale: 'de' });
Handle disputes
// 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
// 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
// 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):
const opts = { headers: { 'X-Profile-Id': profileId } };
Browse plans and estimate cost before creating anything:
// 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.cardwith 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:
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:
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:
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.
Bill on a catalog product. Instead of the processor's item_price_id, name a
recurring product of the shop (and optionally one of its prices). The server
resolves it to the reference the billing account holds and refuses the request
before anything is charged when the processor's price no longer matches. Check
first which prices can start a subscription:
const { prices } = await delopay.products.billingReadiness(['prd_pro'], profileId);
// state: 'ready' | 'unverified' | 'other_environment' | 'needs_reference'
await delopay.subscriptions.createAndConfirm(
{ product_id: 'prd_pro', customer_id: 'cus_abc123', payment_details: { payment_method_id: pm } },
opts,
);
await delopay.subscriptions.update(sub.id, { product_id: 'prd_pro', price_id: 'prc_yearly' }, opts);
Mirror the processor. subscriptions.sync({ subscription_ids }) (1 to 50) and
subscriptions.syncOne(id) read subscriptions back from the processor and mirror
what it reports; nothing is written there.
Take existing processor subscriptions over. subscriptions.importPreview
lists what a billing account holds and classifies each row; importApply adopts
up to 25 of them. Word capabilities.reason_code and each row's note_code
yourself; the English reason / note beside them are for API callers.
const preview = await delopay.subscriptions.importPreview({ merchant_connector_id: mca }, opts);
if (!preview.capabilities.listing || !preview.capabilities.adoption) {
// PayPal cannot be listed (`reason_code: 'listing_unsupported'`); an
// account may list without allowing adoption. Say why, and stop.
showUnavailable(preview.capabilities.reason_code);
} else {
// Rows whose buyer is already a DeloPay customer can be taken over as they
// are; `needs_customer` rows need a customer named first.
const picks = preview.items.flatMap((row) =>
row.classification === 'new' && row.customer_id
? [{ external_subscription_id: row.external_subscription_id, customer_id: row.customer_id }]
: [],
);
// The route takes 1 to 25 selections: send nothing when there is nothing
// to take over, and batch anything larger.
for (let i = 0; i < picks.length; i += 25) {
const { results } = await delopay.subscriptions.importApply(
{ merchant_connector_id: mca, items: picks.slice(i, i + 25) },
opts,
);
// One result per selection: `adopted`, `already_held`, or `failed` with a `reason_code`.
}
}
A buyer returning from the processor's checkout can read the subscription's
status with the return token (rt) the return URL carried, from the browser:
const session = new CheckoutSession({ merchantId, paymentId: subscriptionId });
const { status, awaiting_processor } = await session.subscriptionReturnStatus(rt);
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:
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.).
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:
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.
A live stream (delopay.live.stream(), dashboard JWT) throws with code: 'NOT_EVENT_STREAM' when the server answers 2xx with something other than text/event-stream, and with code: 'TIMEOUT' when it stays silent past idleTimeoutMs (default 45 s). It is never reconnected by the SDK: see the integration guide's Live updates for dashboards.
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:
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_… |
// Sandbox
const delopay = new Delopay(process.env.DELOPAY_API_KEY!, { sandbox: true });
License
MIT