Skip to main content

Approvals & authority

Authority is what a member may do with the organization's money — which actions, within which limits, over which assets/chains/destinations, and with what approval weight. Approvals are how payments that exceed someone's authority get authorized by others. Two rules run through everything:

  1. Money authority is attested by your Safe, never by Permara. An authority grant becomes cryptographic (AUTHORITY_DELEGATION credential) only when your org's deployed Safe signs it, verified on-chain via EIP-1271. Permara holds no key that can mint spending authority.
  2. Segregation of duties. The member who initiated a payment can never approve it — the vote is refused with SELF_APPROVAL_FORBIDDEN, not silently discounted.

Granting authority

POST /v1/members/{memberId}/authorities (members:manage). Only ACTIVE members can hold authority.

curl -X POST https://api.sandbox.permara.com/v1/members/mem_1a2b3c/authorities \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowedActions": ["payment.create", "payment.approve"],
"perTxLimitUsd": "5000",
"dailyLimitUsd": "20000",
"monthlyLimitUsd": "100000",
"allowedChains": ["base"],
"approvalWeight": 1,
"countersignAboveUsd": "1000"
}'

The response contains the created authority plus attestation instructions — the exact EIP-712 CredentialAttestation message your Safe must sign:

{
"authority": {
"id": "auth_7g8h9i",
"memberId": "mem_1a2b3c",
"status": "ACTIVE",
"allowedActions": ["payment.approve", "payment.create"],
"perTxLimitUsd": "5000",
"approvalWeight": 1,
"requiresCountersignature": false,
"countersignAboveUsd": "1000",
"version": 1,
"credentialId": null
},
"attestation": {
"primaryType": "CredentialAttestation",
"message": {
"subjectDid": "did:safebank:c1a2b…",
"orgDid": "did:safebank:c9z8y…",
"kind": "AUTHORITY_DELEGATION",
"schemaVersion": "safebank/credential/AUTHORITY_DELEGATION/v1",
"docHash": "0x…",
"issuedAt": "1787997900",
"expiresAt": "0",
"nonce": "241531…"
},
"note": "Safe-sign the EIP-712 hash of this message (chainId = the tenant Safe chain), then POST the signature blob to /attest."
}
}

Semantics worth knowing:

  • Limits: perTxLimitUsd caps a single payment; dailyLimitUsd / monthlyLimitUsd are rolling member spend windows. Hitting any of them holds the payment with AUTHORITY_LIMIT_EXCEEDED.
  • Allowlists (allowedCurrencies / allowedAssets / allowedChains / allowedDestinationIds): empty = unrestricted; non-empty = exclusive.
  • Countersignature: requiresCountersignature: true (always) or countersignAboveUsd (above a threshold) forces the approval lane even within the per-tx limit.
  • Versioning: grants are versioned per member; the most-recent ACTIVE grant covering the action wins deterministically. A REVOKED authority is immutable — grant a new one (AUTHORITY_REVOKED on update attempts).

Attesting with the org Safe (EIP-1271)

The grant is effective immediately for gating; attestation makes it a signed, portable credential. The document and nonce derive deterministically from the stored row, so there is no pending-signature state — you can recompute and sign at any time:

  1. Compute the EIP-712 hash of attestation.message (types/domain in @permara/attestationscredentialAttestationHash; domain { name: 'Permara', version: '1', chainId: <your Safe's chain>, verifyingContract: '0x5afeba…0002' }).
  2. Collect owner signatures through your normal Safe signing flow until the Safe's threshold is met (Safe UI, Safe Transaction Service, or signMessage on the Safe).
  3. Submit the combined signature blob:
curl -X POST https://api.sandbox.permara.com/v1/members/mem_1a2b3c/authorities/auth_7g8h9i/attest \
-H "X-SafeBank-Api-Key: $PERMARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "signature": "0x…combined-safe-signature…" }'

The API verifies the signature on-chain (isValidSignature via eth_call against your Safe) and, if the threshold is met, creates the AUTHORITY_DELEGATION credential (issuer = your Safe's address) in the same transaction. Failure modes:

CodeStatusMeaning
ORG_SAFE_NOT_DEPLOYED409Your tenant Safe is predicted-address only — deploy it first. There is no fallback signer.
ORG_ATTESTATION_INVALID400The signature does not validate on-chain (threshold not met, or signed the wrong hash).
AUTHORITY_ALREADY_ATTESTED409The authority already carries a credential.
AUTHORITY_NOT_ACTIVE409Suspended/revoked authorities can't be attested.

Revoking an authority (DELETE …/authorities/{id}) revokes its backing credential atomically.

The approval lane

When the credential gate decides a payment needs approval (flat countersignature, countersignAboveUsd, or an org payment-policy rule), the payment parks in APPROVAL_REQUIRED with a PENDING approval request (at most one open request per payment — concurrent holds converge on one).

create payment ──gate──▶ APPROVAL_REQUIRED ──votes──▶ APPROVAL_PENDING ──Σweight ≥ required──▶ payment re-enters POLICY_CHECK → ROUTE_QUOTED …
└─ any REJECT vote ──▶ request REJECTED

Vote from a member session (approvals come from members, not applications — an API key gets APPROVAL_REQUIRES_MEMBER):

# List open requests
curl https://api.sandbox.permara.com/v1/approvals \
-H "Authorization: Bearer $PRIVY_TOKEN"

# Vote (optionally with an EIP-191 wallet signature for non-repudiation)
curl -X POST https://api.sandbox.permara.com/v1/approvals/apr_123/sign \
-H "Authorization: Bearer $PRIVY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "decision": "APPROVE", "signature": "0x…" }'

# Or the payment-scoped shortcut:
curl -X POST https://api.sandbox.permara.com/v1/payments/pay_456/approve \
-H "Authorization: Bearer $PRIVY_TOKEN"

The optional signature is personal_sign over the canonical vote text:

Permara approval vote
request: <approvalRequestId>
payment: <paymentId>
decision: APPROVE
member: <memberId>

Voting rules (each refused with a stable code):

RuleCode
Only PENDING requests accept votesAPPROVAL_NOT_PENDING (409)
Voter must be a member, not an API keyAPPROVAL_REQUIRES_MEMBER (403)
Initiator can never approve their own paymentSELF_APPROVAL_FORBIDDEN (403)
Voter must be an ACTIVE memberMEMBER_NOT_ACTIVE (403)
Voter needs an ACTIVE authority granting payment.approveAUTHORITY_MISSING (403)
Group-scoped requests need the matching approval groupAPPROVAL_GROUP_MISMATCH (403)
One vote per memberALREADY_VOTED (409)
Bad wallet signatureAPPROVAL_SIGNATURE_INVALID (400)

Each APPROVE vote contributes the voter's approvalWeight captured at signing — later authority edits never rewrite past votes. When Σ weights ≥ requiredWeight the request flips APPROVED and the payment automatically re-enters policy evaluation (the gate now sees the approval and quoting proceeds). Any REJECT vote rejects the request.

On-chain Safe-signature equivalence

Safe confirmations are approvals. For payments that execute through your Safe, Permara sums the approval weights of ACTIVE members whose registered signerAddress confirmed the Safe transaction and credits that weight against the approval requirement — signing the Safe tx is the vote; there is never a second signature collection on top of the Safe's M-of-N. (Off-chain payouts are exactly why the approval lane exists: a provider payout is not a contract call, so without this lane it would bypass your approval requirements entirely.)

Endpoint summary

MethodPathScope
GET/v1/members/{memberId}/authoritiesmembers:read
POST/v1/members/{memberId}/authoritiesmembers:manage
PATCH / DELETE/v1/members/{memberId}/authorities/{authorityId}members:manage
POST/v1/members/{memberId}/authorities/{authorityId}/attestmembers:manage
GET/v1/payment-policy (+ /history)policies:read
PUT/v1/payment-policy, POST /v1/payment-policy/attestpolicies:write
GET/v1/approvalspayments:read
POST/v1/approvals/{id}/sign / /{id}/rejectpayments:execute
POST/v1/payments/{id}/approvepayments:execute