Skip to main content

Mandates API

The Mandate API exposes Invoica’s PACT primitive as a generic REST surface. A mandate is a bilateral cryptographically-signed agreement between two parties — agents or humans — that authorizes specific work for a specific period. Every state transition is anchored on-chain (Base Sepolia in v0.1) and a webhook fires on each change.
Status: v0.1 — testnet anchor on Base Sepolia. Mainnet contract migration scheduled for v0.2. First production integration: Helixa Synagent TG bot.

Base URL

Authentication

All endpoints require an x-api-key header. Request a key at support@invoica.ai.

Signing algorithm

Every mandate signature is HMAC-SHA256 over a deterministic canonical encoding of the mandate fields. The shared secret (PACT_SIGNING_SECRET) is distributed out of band per integration partner.
Why canonical JSON matters: Postgres JSONB doesn’t preserve key insertion order, and TIMESTAMPTZ re-emits timestamps in a normalized format. Without canonicalization, a roundtrip through the DB produces different bytes than what the signer signed and verification fails on every /sign call.
Canonical rules:
  1. Sort object keys alphabetically (recursive — applies to nested objects)
  2. Normalize expires_at via new Date(value).toISOString()
  3. Stringify with JSON.stringify — no whitespace
Reference implementation (Node.js):

State machine

Terminal states: completed, disputed, expired. Atomic completion: POST /complete advances signed_by_both → in_progress → completed in two anchor txs. Every state transition emits a MandateAnchored(bytes32 mandateHash, string state, uint256 ts, address sender) event on Base Sepolia. The contract has no storage; receipts are the (mandateHash, txHash, blockNumber) tuples persisted in the MandateTransition row trail. Anchor contract: 0x55acad606c488057db395e87ac5d57944f31c497

Propose Mandate

Create a new mandate proposal. Returns the mandate id; the proposer must sign next.
proposer
string
required
Opaque agent or human id. Invoica does not validate this — partner controls identity resolution.
counterparty
string
required
Opaque agent or human id.
scope
string
required
Natural-language description of the work being authorized.
terms
object
required
Free-form JSON. Recommended fields: amount, currency, deliverable, deadline. Stored verbatim and included in the signed canonical bytes.
expiry
string
required
ISO 8601 timestamp. After this point the mandate auto-expires (no further signs/transitions allowed).
context
object
Opaque partner-specific metadata. For the Helixa Synagent integration, set context.source = "helixa_synagent_tg_bot" and context.chat_id = "<TG chat ID>".

Request

Response


Sign Mandate

Sign a mandate. The proposer signs first; once both signatures are recorded, the mandate state becomes signed_by_both and is anchored on-chain.
signer
string
required
Either "proposer" or "counterparty". The proposer must sign before the counterparty.
signature
string
required
Hex-encoded HMAC-SHA256 of the canonical mandate bytes (see signing algorithm above).

Request

Response

When the counterparty signs, status becomes signed_by_both and a drs_receipt_id may populate.

Errors


Get Mandate

Fetch full mandate state plus the complete transition trail. Every state change is one row, each with the on-chain anchor tx_hash verifiable on BaseScan.

Response


Complete Mandate

Mark a mandate as completed. If called from signed_by_both, implicitly advances through in_progress → completed (two anchor transactions).
triggered_by
string
Optional agent id of the party triggering completion. Defaults to "system". Recorded in the transition metadata.

Request


Dispute Mandate

Flag a mandate as disputed. Terminal state — no further transitions allowed.
triggered_by
string
Optional agent id triggering the dispute.
reason
string
required
Free-text reason. Recorded on the mandate and in the transition metadata.

Request


Webhook events

Use the existing Webhooks API to subscribe to mandate state changes:
Payload:
Verify the X-Invoica-Signature header against your secret using HMAC-SHA256 over the raw body.

On-chain verification

Every transition with a non-null anchor_tx_hash is independently verifiable:
The MandateAnchored event carries (mandateHash, state, timestamp, sender). In v0.1, sender is always the Invoica anchor signer (0x7433208E00aB3F84119da26e6DEB0596D09B65d0). Mainnet migration in v0.2 will rotate this address.

Quickstart

A complete reference client (~150 lines TypeScript) lives at scripts/test-mandate-cycle.ts in the Invoica repo. Postman collection: contact support@invoica.ai for the JSON file.

What’s out of scope (v0.1)

  • EIP-712 wallet signing (v0.2)
  • Mainnet anchor contract (v0.2)
  • Multi-party mandates beyond proposer + counterparty (v0.2)
  • Variable-cost mandates / streaming escrow (v0.2 — design notes in BOND v0.2 research)
  • OAuth scopes per partner (v0.2)