@havenpay/server
v1.1.0
Published
Server-side TypeScript SDK for Havenpay payment, project, webhook, and account APIs
Maintainers
Readme
@havenpay/server
Server-side TypeScript SDK for Havenpay.
@havenpay/server is the backend SDK for secret-key operations: PaymentIntent creation, project management, API key lifecycle, account lifecycle, provider account credential-reference management, refund requests, ledger-backed balance transaction reads, webhook endpoint management, event replay, and raw-body webhook signature verification.
Use it only from trusted backend infrastructure. Never bundle it into browsers, React Native apps, edge pages that expose client code, or public UI packages.
Install
bun add @havenpay/servernpm install @havenpay/serverUse it for
- Creating PaymentIntents from a merchant server.
- Returning a short-lived payment
clientSecretto browser or mobile clients. - Managing projects, API keys, connected accounts, webhook endpoints, and events.
- Verifying Havenpay webhook signatures against the exact raw request body.
- Managing provider account encrypted credential references.
- Reading balance transactions and creating refund requests through server-only APIs.
Quickstart
import { Havenpay } from '@havenpay/server'
const havenpay = new Havenpay({
secretKey: process.env.HAVENPAY_SECRET_KEY,
apiVersion: '2026-05-24.haven',
retry: {
maxAttempts: 3,
initialDelayMs: 100,
maxDelayMs: 2000,
},
timeoutMs: 30000,
})
// Persist this non-secret key with the merchant order and reuse it whenever the
// same checkout is reopened.
const checkoutKey = 'checkout_order_ORD_123'
const intent = await havenpay.paymentIntents.prepare({
projectId: 'proj_...',
attemptGeneration: 1,
checkoutKey,
amount: 1000,
currency: 'usd',
customer: {
phone: '+263771234567',
},
mobileMoney: {
provider: 'omari',
phone: '+263771234567',
},
mobilePresentation: {
allowedMethodFamilies: ['omari'],
appIdentifier: 'com.example.app',
beneficiaryReference: 'customer_123',
bindingId: 'psb_order_123',
buildId: 'android-production-42',
deviceKeySha256: 'a'.repeat(64),
distribution: 'direct',
orderSummary: {
amountNote: 'Billed once',
lineItems: [{ amountMinor: 1000, label: 'Order ORD-123' }],
subtitle: 'Your Example Store purchase',
title: 'Example Store',
},
payerReference: 'customer_123',
quoteReference: 'quote_123',
},
metadata: {
orderId: 'order_123',
},
})
return Response.json({
paymentIntentId: intent.id,
clientSecret: intent.clientSecret,
mobilePresentation: {
buildId: 'android-production-42',
deviceKeySha256: 'a'.repeat(64),
distribution: 'direct',
packageName: 'com.example.app',
quoteReference: 'quote_123',
},
})Send clientSecret only to the client that is completing this one payment. Keep secretKey on the server.
prepare() derives a deterministic idempotency key from the checkout identity
and immutable payment authority. Exact reopen/lost-response calls return the
same generation. When authenticated retrieval confirms failed, canceled, or
expired, handle Try Again with
havenpay.paymentIntents.retry(intent.id); never retry processing, unknown,
or succeeded.
orderSummary is backend-authored display data bound to the same immutable
order as the amount. Its title is required; subtitle and amount note are
optional; and it may contain one to twelve { label, quantity?, amountMinor }
lines. Each amountMinor is the complete line total, and the line totals must
exactly equal the PaymentIntent amount. Use the summary for subscriptions,
retail baskets, utility references, or other order types without moving order
authority into the mobile app.
The client mobilePresentation values must exactly match the authenticated
server binding. Map server appIdentifier to Android packageName or iOS
bundleId; copy buildId, deviceKeySha256, distribution, and
quoteReference unchanged. A missing or mismatched value fails closed. These
values are binding context, not merchant credentials, and must not be reused
across orders.
allowedMethodFamilies must be non-empty, unique, include the initial
mobileMoney.provider, and contain only adapters whose mobile-money capability
is implemented. Creation rejects the entire binding when any entry is
unavailable; the server does not silently filter it. The current public IDs are:
| Provider ID | Current candidate status |
| --- | --- |
| omari | Implemented adapter; use test/staging until live readiness gates are complete. |
| fake | Repository test provider only. |
| ecocash, onemoney, innbuck | Not implemented for payable presentation. |
The server/API provider ID is innbuck; checkout policy uses innbucks. Neither
name means that InnBucks is currently available.
Webhook verification
Webhook verification requires the exact raw body bytes sent by Havenpay. Do not parse JSON before signature verification.
import { Havenpay } from '@havenpay/server'
const havenpay = new Havenpay({
secretKey: process.env.HAVENPAY_SECRET_KEY,
})
const event = havenpay.webhooks.constructEvent({
rawBody,
signature: request.headers.get('Havenpay-Signature') ?? '',
secret: process.env.HAVENPAY_WEBHOOK_SECRET,
})
switch (event.type) {
case 'payment_intent.succeeded':
// Fulfil the order in an idempotent server-side transaction.
break
case 'payment_intent.failed':
// Mark the order as failed only after validating the event payload.
break
}Main resources
havenpay.accounts.create(...)
havenpay.accounts.list(...)
havenpay.accounts.update(...)
havenpay.projects.create(...)
havenpay.projects.list(...)
havenpay.projects.update(...)
havenpay.projects.archive(...)
havenpay.projects.getConfig(...)
havenpay.apiKeys.create(...)
havenpay.apiKeys.list(...)
havenpay.apiKeys.revoke(...)
havenpay.paymentIntents.create(...)
havenpay.paymentIntents.prepare(...)
havenpay.paymentIntents.retrieve(...)
havenpay.paymentIntents.retry(...)
havenpay.refunds.create(...)
havenpay.refunds.retrieve(...)
havenpay.refunds.list(...)
havenpay.providerAccounts.create(...)
havenpay.providerAccounts.list(...)
havenpay.providerAccounts.rotate(...)
havenpay.providerAccounts.revoke(...)
havenpay.balanceTransactions.list(...)
havenpay.webhookEndpoints.create(...)
havenpay.webhookEndpoints.list(...)
havenpay.webhookEndpoints.update(...)
havenpay.webhookEndpoints.rotateSecret(...)
havenpay.webhookEndpoints.delete(...)
havenpay.events.list(...)
havenpay.events.replay(...)
havenpay.webhooks.constructEvent(...)Project administrators can change the ordered mobile method catalogue without shipping client code when a method uses an existing audited flow:
await havenpay.projects.update('proj_...', {
paymentMethodPolicy: [
{
allowedCurrencies: ['usd'],
branding: { accentColor: '#E95F3D', icon: 'phone' },
displayName: 'OMARI',
enabled: true,
id: 'omari',
minimumAmountMinor: 10,
order: 1,
provider: 'omari',
},
],
})HavenPay still intersects this policy with active credentials, implemented adapter capability, environment, provider health/kill switch, currency, amount, and the checkout allowlist. Disabling a method removes it from new attempts and blocks a new confirmation from a stale snapshot.
Provider account credential references
Provider account APIs accept encrypted credential references and key fingerprints. They must not accept or return raw provider secret material.
await havenpay.providerAccounts.create({
projectId: 'proj_...',
provider: 'omari',
encryptedCredentialRef: 'kms://havenpay/provider-credentials/...',
keyFingerprint: 'fp_...',
metadata: {
label: 'Primary test credential',
},
})Live provider capability should be enabled only when the provider adapter, credentials, contract tests, and operational reconciliation path prove that lane.
Error handling
Non-2xx API responses throw HavenpayApiError.
import type { Havenpay } from '@havenpay/server'
import { HavenpayApiError } from '@havenpay/server'
async function _retrievePaymentIntent(havenpay: Havenpay) {
try {
return await havenpay.paymentIntents.retrieve('pi_...')
}
catch (error) {
if (error instanceof HavenpayApiError) {
console.error(error.status, error.code, error.requestId)
}
throw error
}
}Errors always expose status, raw body, and response-header request ID. Typed code
metadata is populated only when an endpoint returns the nested SDK error
envelope; current top-level PaymentError bodies remain available through the
raw body. Do not log request bodies, API keys, idempotency keys, webhook secrets,
provider credentials, or client secrets.
Retry and timeout policy
Retries are disabled unless retry is configured.
- Network failures and HTTP 5xx responses may retry.
- Safe reads may retry.
- Mutating requests retry only when they include an
Idempotency-Key. - Validation, authentication, authorization, tenant-scope, and not-found errors should not be retried without changing the request or credentials.
timeoutMsapplies per request attempt.
Observability
onRequestEvent emits redacted server-side request lifecycle events.
const _havenpay = new Havenpay({
secretKey: process.env.HAVENPAY_SECRET_KEY,
onRequestEvent: (event) => {
console.info({
type: event.type,
operationName: event.operationName,
requestId: event.requestId,
status: event.status,
})
},
})Events include method, path, operation name, attempt number, status, error kind, and API response request ID only. They do not include headers, request bodies, API keys, idempotency keys, provider credentials, webhook secrets, or client secrets.
Security boundary
This is a server-only package. It may use Node/Bun server capabilities and secret-key authentication. It must never be imported by:
@havenpay/web@havenpay/web-elements@havenpay/react-native- browser bundles
- mobile client bundles
Keep these values server-side:
HAVENPAY_SECRET_KEY- webhook signing secrets
- provider credential references and fingerprints
- account admin tokens
- raw webhook request bodies
- raw provider response payloads
Package boundaries
Use:
@havenpay/serverfor trusted backend operations.@havenpay/webfor browser payment-sheet flows.@havenpay/web-elementsfor optional browser custom elements.@havenpay/react-nativefor Expo and bare React Native payment flows.@havenpay/corefor shared public types.
Client authentication uses only a public projectId and the short-lived payment
clientSecret required for one payment flow. A native presentation also receives
the non-secret, exact mobile binding context shown above; it must never receive a
merchant API key, provider credential, or webhook secret.
API-version pinning
Pass apiVersion to send X-Haven-API-Version with SDK requests:
const _havenpay = new Havenpay({
secretKey: process.env.HAVENPAY_SECRET_KEY,
apiVersion: '2026-05-24.haven',
})Pinning keeps backend integrations explicit as Havenpay evolves.
