Skip to main content

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):

OptionDefaultPurpose
apiKey— (required)pm_test_… / pm_live_….
baseUrlinferred from keyOverride the API base URL.
maxRetries3Retry attempts on 429/5xx/network errors.
timeoutMs30000Per-request timeout.
fetchglobal fetchInjectable 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:

NamespaceREST 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)
danger

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

  • Retries429, 502, 503, 504, and network errors are retried up to maxRetries with jittered exponential backoff (250ms → 4s), honoring Retry-After. 4xx (other than 429) is not retried.
  • Idempotency — every POST gets an Idempotency-Key generated once and reused across retries, so nothing double-executes. See Idempotency.

See also

  • CLI — the permara command line built on this SDK.
  • Quickstart — the full loop end to end.
  • REST reference — the underlying endpoints.