TypeScript SDK
@permara/sdk is the typed client for the Permara API. It
handles auth, automatic idempotency, jittered
retries, typed errors,
and local Safe signing.
Install
pnpm add @permara/sdk
# or: npm i @permara/sdk / yarn add @permara/sdk
Requires Node.js 20+ (uses the global fetch). You can inject a custom fetch
for other runtimes.
Create a client
The base URL is inferred from the key prefix (pm_test_ → sandbox), so usually
you pass only the key:
import { Permara } from '@permara/sdk'
const pm = new Permara({ apiKey: process.env.PERMARA_API_KEY! })
Options (PermaraOptions):
| Option | Default | Purpose |
|---|---|---|
apiKey | — (required) | pm_test_… / pm_live_…. |
baseUrl | inferred from key | Override the API base URL. |
maxRetries | 3 | Retry attempts on 429/5xx/network errors. |
timeoutMs | 30000 | Per-request timeout. |
fetch | global fetch | Injectable transport (tests / non-Node runtimes). |
Namespaces
The client mirrors the CLI verb tree. Each namespace maps to a REST resource:
The full list, in the order packages/sdk/src/client.ts declares them:
| Namespace | REST resource |
|---|---|
pm.tenants, pm.members, pm.status | /v1/tenants, /v1/tenants/members, /v1/tenants/me/summary |
pm.keys | /v1/keys |
pm.wallets, pm.faucet | /v1/wallets, /v1/faucet |
pm.payments, pm.offramp | /v1/payments, /v1/offramp/payments |
pm.dids, pm.directory, pm.contacts | /v1/dids, /v1/directory, /v1/contacts |
pm.claims, pm.destinations, pm.eligibility | /v1/claims, /v1/payout-destinations, /v1/eligibility |
pm.cards, pm.agents, pm.treasury, pm.ramps, pm.policies | /v1/cards, /v1/agents, /v1/treasury, /v1/ramps, /v1/policies |
pm.agreements, pm.payables, pm.invoices | /v1/agreements, /v1/payables, /v1/invoices |
pm.lending, pm.trade | /v1/lending, /v1/trade |
pm.twoFactor | /v1/2fa |
pm.verifications, pm.credentials, pm.identities, pm.credentialPresentations | /v1/verification-sessions, /v1/credentials, /v1/identities, /v1/credential-presentations |
pm.authorities, pm.paymentPolicy, pm.approvals | /v1/authorities, /v1/payment-policy, /v1/approvals |
pm.webhooks | /v1/webhooks |
pm.capabilities | /v1/capabilities |
Every method is typed; the REST reference is the full request and response surface.
Plus two convenience pollers on the client itself:
await pm.waitForWalletActive(walletId) // resolves when DEPLOYED (or FAILED / timeout)
await pm.waitForPayment(paymentId) // resolves when COMPLETED / FAILED / CANCELLED
End-to-end example
import { Permara, signSafeTxHash } from '@permara/sdk'
const pm = new Permara({ apiKey: process.env.PERMARA_API_KEY! })
// 1. Create a wallet.
const wallet = await pm.wallets.create({
name: 'Acme Treasury',
owners: [{ address: signerAddress, role: 'ADMIN_OWNER' }],
})
// 2. Fund it (sandbox faucet).
await pm.wallets.fund(wallet.id, { asset: 'usdc', amount: '250' })
// 3. Pay a wallet handle on the legacy directory (a Permara ID on /v1/payments is the current path).
const payment = await pm.payments.create({
sourceWalletId: wallet.id,
destination: { type: 'handle', handle: 'volt-components' },
amount: { value: '25.00' },
})
// 4. Sign the safeTxHash and submit (auto-executes at threshold).
const signature = await signSafeTxHash(payment.safeTxHash!, privateKey)
const settled = await pm.payments.sign(payment.id, signature)
Signing
signSafeTxHash(safeTxHash, privateKey) produces a raw secp256k1 signature
over the 32-byte hash — no EIP-191 prefix. This is the single most
load-bearing detail of the payment flow (see the
signing gotcha):
import { signSafeTxHash, generateSigner, addressForPrivateKey } from '@permara/sdk'
const signer = generateSigner() // { privateKey, address } — sandbox only
const addr = addressForPrivateKey(privateKey) // EIP-55 address for a key
const sig = await signSafeTxHash(safeTxHash, signer.privateKey)
Never sign the safeTxHash with personal_sign / signMessage — the EIP-191
prefix breaks recovery on both the API and Safe. Always use signSafeTxHash.
Error handling
Non-2xx responses throw a typed PermaraApiError carrying the canonical
envelope fields; transport failures throw
PermaraNetworkError:
import { PermaraApiError, PermaraNetworkError } from '@permara/sdk'
try {
await pm.payments.create({ /* … */ })
} catch (err) {
if (err instanceof PermaraApiError) {
console.error(err.code, err.statusCode, err.requestId, err.details)
} else if (err instanceof PermaraNetworkError) {
console.error('network', err.cause)
} else {
throw err
}
}
Retries and idempotency
- Retries —
429,502,503,504, and network errors are retried up tomaxRetrieswith jittered exponential backoff (250ms → 4s), honoringRetry-After.4xx(other than429) is not retried. - Idempotency — every
POSTgets anIdempotency-Keygenerated once and reused across retries, so nothing double-executes. See Idempotency.
See also
- CLI — the
permaracommand line built on this SDK. - Quickstart — the full loop end to end.
- REST reference — the underlying endpoints.