Documentation / API v1

Everything an agent needs to spend safely.

Paygente decides whether a payment is permitted and records why. This page is the whole contract: the objects, the lifecycle, the errors and the boundary of what this release does. Nothing here is behind a login.


Quickstart

Propose a payment. The response tells you the outcome and every reason behind it, in the same request — an agent should never have to poll to find out whether it is allowed to act.

Request
curl -sX POST "$PAYGENTE_API/api/v1/mandates" \
  -H "content-type: application/json" \
  -H "idempotency-key: run-2026-04-01-0031" \
  -H "x-paygente-user: elena" \
  -d '{
    "agentId": "agt_01JQ…",
    "recipientId": "rcp_01JQ…",
    "asset": "EURC",
    "amount": "75.00",
    "purpose": "Extended dataset licence"
  }'
Response — 201 Created
{
  "mandate": {
    "id": "mnd_01JQ…",
    "status": "PENDING_APPROVAL",
    "statusLabel": "Waiting for approval",
    "requiresApproval": true,
    "simulated": true
  },
  "decision": {
    "outcome": "REQUIRE_APPROVAL",
    "engineVersion": "policy-engine@1.0.0",
    "reasons": [{
      "code": "AMOUNT_REQUIRES_APPROVAL",
      "message": "€75.00 EURC is at or above the €40.00 EURC threshold, so a person has to approve it."
    }]
  },
  "url": "https://paygente.com/app/mandates/mnd_01JQ…"
}

The full machine-readable contract is at /openapi.json. The identity header is a sandbox development shim, not a credential — see the boundary.


Concepts

Five objects. The mandate is the one that matters; the rest exist to bound it.

Mandate

A bounded permission to pay: one agent, one recipient, one amount, one asset, one stated purpose, one expiry. The primary object in Paygente — not a wallet and not a card.

Mandate fields
FieldTypeNotes
idstringPrefixed ULID, e.g. mnd_01JQ8F7Z9K3M4N5P6Q7R8S9T0V
statusenumDRAFT · PROPOSED · EVALUATING · BLOCKED · PENDING_APPROVAL · APPROVED · REJECTED · EXECUTING · COMPLETED · FAILED · REVOKED · EXPIRED
moneyMoneyAmount in integer minor units, plus exact and display forms
purposestringThe business reason, recorded verbatim
expiresAtdate-timeWhen the authority lapses
requiresApprovalbooleanWhether the policy escalated this to a person
simulatedtrueAlways true in this release

Agent

A named principal that may propose payments. Registering an agent grants no authority by itself and returns no credential; authority comes from the policy attached to it.

Agent fields
FieldTypeNotes
idstringagt_…
externalIdstringYour identifier for the agent, unique per organization
statusenumACTIVE · PAUSED · DISABLED
defaultPolicyIdstring | nullNull means the agent cannot spend at all

Policy

The rules that bound an agent. A closed set: there is no expression language, so a policy is data an evaluator interprets and can never become code that executes.

Policy fields
FieldTypeNotes
maxTransactionAmountMoneyHard ceiling for a single payment
dailyLimit / monthlyLimitMoney | nullRolling UTC-day and calendar-month ceilings
approvalThresholdMoney | nullAt or above this amount, a person must approve
allowedAssetsAssetCode[]EURC, USDC
approvedRecipientIdsstring[] | nullNull means any recipient passing the checks below
maxMandateDurationSecondsintegerLongest authorisation window a mandate may request
requireVerifiedRecipientbooleanDeny unverified recipients
maxRecipientRiskLevelenumLOW · MEDIUM · HIGH
allowFirstTimeRecipientsbooleanWhether a never-before-paid recipient is permitted
allowRecurringPaymentsbooleanWhether recurring payments are permitted

Approval

A person’s decision on a mandate the policy escalated. Exactly one decision per mandate, enforced by a database unique index — a second approval cannot be recorded even under a race.

Approval fields
FieldTypeNotes
decisionenumAPPROVED · REJECTED
decidedByUserIdstringWho decided
commentstring | nullRecorded in the audit trail

PaymentAttempt

One execution of a mandate against a rail. Records what the rail actually did and whether it matched what was authorised.

PaymentAttempt fields
FieldTypeNotes
statusenumPENDING · SUCCEEDED · FAILED · REVERSED
railNamestringmock-sandbox-rail in this release
externalIdstring | nullRail reference, always prefixed sim_ here
reconciliationMatchedboolean | nullDid the rail move the authorised amount to the authorised recipient
simulatedtrueAlways true in this release

Mandate lifecycle

Transitions are enforced in the domain layer. An interface cannot talk a mandate into a state the state machine refuses, because every interface calls the same function.

Transitions
DRAFT             → PROPOSED, REVOKED, EXPIRED
PROPOSED          → EVALUATING, REVOKED, EXPIRED
EVALUATING        → BLOCKED, PENDING_APPROVAL, APPROVED, EXPIRED
PENDING_APPROVAL  → APPROVED, REJECTED, REVOKED, EXPIRED
APPROVED          → EXECUTING, REVOKED, EXPIRED
EXECUTING         → COMPLETED, FAILED
FAILED            → EXECUTING, REVOKED, EXPIRED

BLOCKED · REJECTED · COMPLETED · REVOKED · EXPIRED   terminal

Guarantees

  • A blocked mandate cannot execute. A policy denial is permanent for that mandate.
  • A rejected mandate can never be approved later.
  • An expired mandate cannot execute, even if it was approved before it expired.
  • A completed mandate cannot be revoked.
  • A revoked mandate cannot execute.
  • Nothing executes while the organization kill switch is active.
  • A mandate executes at most once. The state machine, a row lock, a database partial unique index and the rail’s own idempotency each refuse a second payment independently.
  • An approval cannot be submitted by a role that does not hold the permission.

Budget is reserved at approval

An approved mandate consumes its amount against the daily and monthly limits immediately, before it executes. If only completed payments counted, ten mandates could each individually pass the same daily limit and then all execute — the limit would be checked ten times and honoured zero. Budget is released when a mandate fails, expires or is revoked.


Policy engine

A pure TypeScript function. No I/O, no randomness, no clock of its own, and no language model. The same inputs always produce the same decision, and the inputs are snapshotted with the result — so a decision made in April can still be re-explained in October, even if the policy has changed since.

Decision shape
type PolicyDecision = {
  outcome: "ALLOW" | "REQUIRE_APPROVAL" | "DENY";
  reasons: Array<{ code: string; message: string }>;
  appliedPolicyId: string | null;
  engineVersion: string;
};
Reason codes
NO_POLICY_ASSIGNED            AMOUNT_NOT_POSITIVE
EXECUTION_DISABLED            AMOUNT_EXCEEDS_MAX_TRANSACTION
AGENT_NOT_ACTIVE              AMOUNT_REQUIRES_APPROVAL
ASSET_NOT_ALLOWED             DAILY_LIMIT_EXCEEDED
ASSET_NOT_SUPPORTED_BY_…      MONTHLY_LIMIT_EXCEEDED
RECIPIENT_NOT_VERIFIED        EXPIRY_IN_PAST
RECIPIENT_NOT_APPROVED        MANDATE_DURATION_TOO_LONG
RECIPIENT_INACTIVE            RECURRING_NOT_ALLOWED
RECIPIENT_RISK_TOO_HIGH       RECIPIENT_FIRST_TIME_NOT_ALLOWED
WITHIN_POLICY

Every rule runs on every evaluation. The engine does not stop at the first problem, because a requester deserves the full list rather than discovering it one round trip at a time. The outcome is the most restrictive severity among the reasons produced, so adding a rule can never accidentally loosen a decision.

Policy limits are compared against the mandate amount without currency conversion. Paygente holds no exchange rate and implies none: a policy permitting more than one asset applies the same numeric limit to each.


Money

There is no code path in Paygente where an amount becomes a floating-point number. Amounts are integer minor units — for EURC and USDC that is 1e-6 of one token, matching the on-chain contracts.

In a response
"money": {
  "asset": "EURC",
  "amountMinor": "75000000",
  "amount": "75.000000",
  "amountDisplay": "€75.00 EURC"
}
In a request
{ "asset": "EURC", "amount": "75.00" }

// Accepted:  "40"  "40.5"  "0.000001"
// Rejected:  "1,000.00"   thousands separator
//            "1e3"        exponent notation
//            "€40"        currency symbol
//            "1.0000001"  more precision than the asset has

Parse amountMinor as a big integer — it is the authoritative value. Display values truncate rather than round, so a rendered amount can never read as more than what was actually authorised.


Idempotency

Send an Idempotency-Key header on every mutating request. A timeout is indistinguishable from a failure, so clients retry; without a key, a retried execute would be a second payment.

  • Same key, same body → the original result is replayed.
  • Same key, different body → 409 idempotency_key_reused. Silently accepting it would lose one of the two requests without anyone noticing.
  • Uniqueness is a database constraint, not a read-then-write, so two concurrent retries cannot both pass the check.

Error codes

Every error has the same shape. No stack trace, no SQL and no driver message ever crosses the boundary; the correlation id locates the server log entry and the audit records that share it.

Error shape
{
  "error": {
    "code": "invalid_state_transition",
    "message": "This payment was blocked by the spending policy and cannot be approved or executed.",
    "details": [],
    "correlationId": "cor_01JQ8F7Z9K3M4N5P6Q7R8S9T0V"
  }
}
Paygente API error codes
CodeHTTPMeaning
validation_failed400The request did not satisfy the schema. `details` names each field.
unauthenticated401No identity was resolved.
forbidden403The caller’s role does not permit this action.
not_found404No such resource — or it belongs to another organization. The two are deliberately indistinguishable.
conflict409The request conflicts with current state.
invalid_state_transition409The mandate cannot move from its current status to the requested one.
idempotency_key_reused409The same key was used for a different request body.
execution_disabled409The organization kill switch is active.
policy_denied422The spending policy refused the payment.
rate_limited429Too many requests. `Retry-After` says when.
rail_failure502The rail could not complete the request.
internal_error500Unexpected. No detail is disclosed; the correlation id locates the log entry.

Regulatory boundary

Paygente Phase 1 is software only. Being precise about this is not a disclaimer; it is a description of what the code does.

What Paygente does

  • Records a company’s intent as a mandate
  • Evaluates that mandate against a policy the company set
  • Routes it to a person when the policy says a person must decide
  • Instructs a rail exactly once, and reconciles what came back
  • Keeps an append-only record of all of it

What Paygente does not do

  • Hold customer money or stablecoins
  • Custody private keys
  • Exchange fiat and crypto
  • Execute regulated transfers
  • Issue payment cards
  • Perform KYC, KYB or final sanctions screening
  • Promise yield, or provide consumer financial services

This release contacts no blockchain and no payment provider. Every transaction is executed by a deterministic mock rail and every mandate and payment attempt is returned with "simulated": true. Paygente is not licensed, regulated or approved, and makes no claim to be. Future production execution is intended to be delegated to licensed CASP, EMI, card-issuing or payment partners through adapters that sit behind the same interface the mock implements today.


Glossary

Mandate
A bounded permission to make one payment. The primary product object.
Principal
The organization on whose behalf an agent acts.
Policy
The closed rule set that bounds an agent’s authority.
Rail
The system that actually moves value. Paygente authorises; a rail executes.
Kill switch
An organization-wide stop on all new executions.
Correlation id
One identifier following a request from the HTTP boundary through the domain into every audit event it produced.
Reconciliation
Comparing what a rail actually did against what the mandate authorised.
Minor units
The smallest indivisible unit of an asset. 1e-6 of one token for EURC and USDC.
CASP
Crypto-Asset Service Provider — a licence category under the EU MiCA regulation.
EMI
Electronic Money Institution — a licence category for issuing e-money and holding client funds.

MCP tools and authorization boundaries