Quickstart
This is the fast path: from zero to a settled USDC payment in under 30 minutes, entirely in sandbox (no real funds). Every step shows what you should get back.
You will:
- Get a sandbox API key and log in with the CLI.
- Create a wallet.
- Fund it from the faucet.
- Find your Permara ID and send a payment.
Node.js 20+. For the SDK: pnpm add @permara/sdk. For the CLI, build it once
with pnpm --filter @permara/cli build (see the CLI guide).
1. Get a key
API keys are minted per tenant. Grab a sandbox
key (pm_test_…) from the Permara developer dashboard, then save it to a
local CLI profile:
permara login pm_test_...
# validates the key by resolving your tenant, then writes ~/.permara/config.json
Expected output confirms the tenant and masks the key:
Logged in · sandbox
tenant Acme Robotics (acme-robotics)
key pm_test_Ab12Cd34Ef56_…
profile sandbox → ~/.permara/config.json
For the SDK and curl, keep the key in an env var instead:
export PERMARA_API_KEY=pm_test_...
The base URL is inferred from the key prefix — pm_test_ →
https://api.sandbox.permara.com. See Environments.
2. Create a wallet
A wallet is a Safe multisig. Create one owned by a signer address you control. In sandbox the CLI generates and stores a demo signer for you on first use.
- curl
- TypeScript
- CLI
curl -X POST https://api.sandbox.permara.com/v1/wallets \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Treasury",
"owners": [{ "address": "0xYourSignerAddress", "role": "ADMIN_OWNER" }],
"threshold": 1,
"chainId": 84532
}'
import { Permara } from '@permara/sdk'
const pm = new Permara({ apiKey: process.env.PERMARA_API_KEY! })
const wallet = await pm.wallets.create({
name: 'Acme Treasury',
owners: [{ address: '0xYourSignerAddress', role: 'ADMIN_OWNER' }],
threshold: 1,
})
permara wallets create --name "Acme Treasury" --wait
# --wait polls until the Safe is DEPLOYED on-chain
You get back a Wallet. The address is a predicted Safe address at
first; status moves PENDING_DEPLOYMENT → DEPLOYING → DEPLOYED.
{
"id": "w_1a2b3c",
"address": "0x7Fb2…c41A",
"status": "PENDING_DEPLOYMENT",
"chainId": 84532,
"threshold": 1,
"predictedAddress": true,
"name": "Acme Treasury",
"owners": [{ "address": "0xYourSignerAddress", "role": "ADMIN_OWNER", "label": null }],
"createdAt": "2026-08-09T12:00:00.000Z"
}
Keep the id — you need it for the next steps.
3. Fund it from the faucet
Sandbox only. The faucet drips test USDC (default
250, capped at 1000 per drip).
- curl
- TypeScript
- CLI
curl -X POST https://api.sandbox.permara.com/v1/wallets/w_1a2b3c/fund \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "asset": "usdc", "amount": "250" }'
const drip = await pm.wallets.fund('w_1a2b3c', { asset: 'usdc', amount: '250' })
permara wallets fund w_1a2b3c --usdc 250
You get back a Drip. status is SUBMITTED and becomes CONFIRMED once the
transfer lands on-chain:
{
"id": "d_9z8y",
"walletId": "w_1a2b3c",
"asset": "usdc",
"amount": "250",
"chainId": 84532,
"status": "SUBMITTED",
"txHash": null,
"failureReason": null,
"createdAt": "2026-08-09T12:00:05.000Z"
}
4. Your Permara ID
Every business on Permara has a generated payment identity — a
Permara ID of the form
did:permara:<64 lowercase hex>. It is created with the business and becomes
payable once the business is verified; nobody claims or chooses one. Payers
address it directly, by the business's verified name, or by an email that
invites the business if it is not on Permara yet.
- curl
- TypeScript
- CLI
# Your identities
curl https://api.sandbox.permara.com/v1/dids/me \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY"
# Does a Permara ID resolve to a payable business, and whose?
curl "https://api.sandbox.permara.com/v1/dids/resolve?did=did:permara:7f5d…9e8d" \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY"
const { dids } = await pm.dids.me()
const who = await pm.dids.resolve('did:permara:7f5d…9e8d')
// { did, resolvable: true, displayName: 'Harbor Goods' }
permara did me
permara did resolve did:permara:7f5d…9e8d
The public DID document is served at GET /v1/dids/<did>/did.json, and the
W3C DID Resolution binding at GET /1.0/identifiers/<did>; neither carries a
payment endpoint or any personal data. To pay someone who is not on Permara
yet, reserve an identity for their email or mobile number
(POST /v1/dids/reservations) and sign against it — the same contact always
returns the same identity.
PUT /v1/directory/handle (a @handle pointing at a Safe wallet on the
legacy rail) still answers for existing integrations and is being removed. New
integrations address a Permara ID, an email, or an address.
5. Send a payment
A payment is proposed → signed → executed. The API proposes it (screening
- calldata), you sign the returned
safeTxHashlocally with a raw secp256k1 signature, and submitting the signature auto-executes once the wallet's threshold is met.
- curl
- TypeScript
- CLI
# 1. Propose — returns a payment with a safeTxHash to sign.
curl -X POST https://api.sandbox.permara.com/v1/payments \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceWalletId": "w_1a2b3c",
"destination": { "type": "handle", "handle": "volt-components" },
"amount": { "value": "25.00" }
}'
# 2. Sign the safeTxHash locally (raw secp256k1 — NOT personal_sign), then:
curl -X POST https://api.sandbox.permara.com/v1/payments/p_abc/sign \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "signature": "0x<signature-over-safeTxHash>" }'
import { Permara, signSafeTxHash } from '@permara/sdk'
const payment = await pm.payments.create({
sourceWalletId: 'w_1a2b3c',
destination: { type: 'handle', handle: 'volt-components' },
amount: { value: '25.00' },
})
// Raw secp256k1 over the 32-byte safeTxHash — never personal_sign.
const signature = await signSafeTxHash(payment.safeTxHash!, privateKey)
const settled = await pm.payments.sign(payment.id, signature) // auto-executes at threshold
permara pay send --from w_1a2b3c --to @volt-components --amount 25.00
# proposes, signs with your demo signer, and executes — all in one command
After signing, the payment settles. status goes to COMPLETED and
executedTxHash is populated:
{
"id": "p_abc",
"sourceWalletId": "w_1a2b3c",
"destinationType": "handle",
"destinationHandle": "volt-components",
"destinationAddress": "0x…",
"destinationChainId": 84532,
"asset": "usdc",
"amount": "25.00",
"status": "COMPLETED",
"safeTxHash": "0x…",
"executedTxHash": "0x…",
"memo": null,
"failureReason": null,
"createdAt": "2026-08-09T12:01:00.000Z"
}
That is the whole loop.
Next steps
- Payments — the propose → sign → execute lifecycle and the signing gotcha, in detail.
- TypeScript SDK — namespaces, error handling, retries, idempotency.
- Errors and Idempotency — write resilient integrations.
- REST reference — the full endpoint surface.