Technical guide
TrustedPAI is an independent checking service for payments. It answers two questions. Before a payment happens: should this go through, and why. After it happens: do your own systems actually agree that it did.
It is an HTTP API. It never moves money, never settles, never refunds, and never blocks anything by itself. It returns an answer and a record; what you do with them is yours.
Who we are
An independent service, tied to no payment provider and no marketplace. We are not your processor and we do not compete with it: we sit beside whatever you already run, and we are paid by you rather than by a share of what you move.
What that independence buys you
A provider report describes its own records. Comparing those observations with your order and back-office records can expose differences between systems. We document the supplied sources and the scope of each comparison.
Start where your problem is
Most people arrive with one question, not all of them. Pick yours.
Before the payment
An AI agent is about to pay on someone's behalf. You send us the payment authorization it presented. We check the cryptography, apply the policy you configured, and return approve, hold or reject with the reasons written out. Measure end-to-end latency for your deployment and payment provider during the pilot.
After the payment
Your checkout, your payment provider and your back office each keep their own record. Send us what they say and we tell you where they disagree — a payment with no order behind it, a refund with no payment, the same capture claimed twice.
The part that is unusual. Every decision we return can be reproduced by you, on your machine, offline, without asking us for anything. That is not a report you have to believe. See Checking us yourself.
Your first call
Base URL and one header. There is no SDK to install to get started.
# everything lives under this origin https://api.trustedpai.com # every authenticated call carries your key in this header X-API-Key: tpai_live_...
Check you can reach the service and see which rails your account can call:
/v1/capabilitiesno key needed
curl https://api.trustedpai.com/v1/capabilities
{
"service": "TrustedPAI",
"api_version": "v1",
"protocols": [
{ "id": "acp", "status": "primary", "enabled": true,
"screening_endpoint": "/v1/screen-transaction-acp" },
// the other rails, each with its own status
]
}
Your API key is created once and shown once. It is never retrievable afterwards, not from us and not from any console — if it is lost, a new one is issued and the old one is revoked.
Keys, tokens and roles
One owner key per account. Everything your team touches day to day should use a restricted token instead.
The owner key can do everything, including creating and revoking tokens. Each token carries a role, and the role decides which endpoints answer it.
| Role | Can |
|---|---|
operator | Screen payments, read decisions, resolve reviews, report abuse |
reviewer | Read decisions and resolve held ones — no screening |
analyst | Read decisions and metrics, simulate a policy change — no writes |
policy_approver | Decide pending policy change requests, and nothing else |
/v1/access-tokensowner key only
curl -X POST https://api.trustedpai.com/v1/access-tokens \
-H "X-API-Key: $OWNER_KEY" -H "Content-Type: application/json" \
-d '{"label":"risk-ops","role":"reviewer","expires_in_days":90}'
The token comes back once in the response. Copy it then.
The separation that matters. A policy_approver cannot change the policy — it can only approve a change someone else proposed. That split exists so no single credential can both loosen a limit and wave it through.
Screening a payment
One call, before you execute. You keep the final decision.
Every rail has its own endpoint because every rail proves authorization differently, but they all return the same answer shape. This example uses ACP. Confirm deployment availability in the protocol table below.
/v1/screen-transaction-acp
curl -X POST https://api.trustedpai.com/v1/screen-transaction-acp \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"spt_id": "spt_1QeX...",
"merchant_id": "merchant-a",
"agent_id": "agent-77",
"amount": 120.00,
"currency": "EUR"
}'
{
"client": "Acme Payments",
"recommendation": "HOLD",
"risk_score": 55,
"reasons": [
"Amount 120.00 EUR exceeds the per-transaction limit (100.00 EUR)",
"No human user present for a transaction of 120.00 EUR (threshold 50.00 EUR)"
],
"processing_time_ms": 84.2,
"transaction_id": 4471,
"review_case_id": 218
}
| Answer | What it means |
|---|---|
| APPROVE | Nothing in your policy objects. No reason is invented to look thorough. |
| HOLD | A person should look. A review case is opened and its id is returned. |
| REJECT | The authorization failed verification, or the risk is high enough that your policy would not allow it. |
reasons is the whole point. Every line is a rule that fired, written so it can be shown to a customer or an auditor without a translation layer.
Replay is refused, not detected later. The identifiers in the authorization are claimed atomically. If the same authorization arrives twice at the same moment, one of them loses the race and is rejected — rather than both being approved and reconciled afterwards.
Calling it from an agent
Screening exposed as MCP tools, subject to transport-specific protocol coverage.
Everything in this guide is a REST endpoint you call from your own code. If the caller is an AI agent, there is a second way in: TrustedPAI speaks the Model Context Protocol, so the agent sees the screening as a tool it can call directly rather than through an integration somebody built for it first.
https://api.trustedpai.com/mcpstreamable HTTP
Seven tools are published. Five screen a payment, one for each rail:
screen_ap2_transaction screen_acp_transaction screen_ucp_transaction screen_x402_transaction screen_mpp_transaction
And two cover what happens around a decision:
# what is already known about an agent get_agent_reputation # tell us a transaction turned out badly report_transaction_abuse
Three things are worth knowing before you point an agent at it.
It is the same engine. No risk logic is duplicated behind this transport: a decision reached here and the same decision reached over REST come from one implementation, and both are reproducible from the evidence the same way.
Your key travels with each call. Every tool takes api_key as a parameter, exactly as the REST header does. The server holds no credentials of its own, so it never becomes the one place that knows everyone's keys.
The same ceiling applies. Calls are rate limited per key, on the same limits as every other caller, and refused before the key is looked up.
A decision still means what it means everywhere else: HOLD and REJECT mean the payment must not proceed without a human looking at it. An agent that calls the tool and ignores the answer has not been screened.
The six payment rails
Same answer, different proof. Only what your account has enabled will answer.
Availability and qualification are different. The live check below reads /v1/capabilities. If the request fails, availability remains unknown. Confirm account configuration and supported modes before integrating.
| Rail | Endpoint | Qualification / deployment |
|---|---|---|
| ACP — Stripe, OpenAI | /v1/screen-transaction-acp | primary Availability not checked |
| AP2 v0.2 — Google | /v1/screen-transaction-ap2-v02 | experimental Availability not checked |
| x402 — Coinbase | /v1/screen-transaction-x402 | experimental Availability not checked |
| UCP | /v1/screen-transaction-ucp | experimental Availability not checked |
| MPP — Stripe, Tempo | /v1/screen-transaction-mpp | experimental Availability not checked |
| AP2 legacy | /v1/screen-transaction | compatibility Availability not checked |
Deployment availability has not been checked. Consult /v1/capabilities and confirm your account configuration.
Experimental is our own word for a rail that is built and tested but has not met the conditions we set for promoting it: an independent interoperability run against a counterparty, and a latency measurement on a real deployment. Both are outstanding for all four. Asking for one switches it on for your account; it does not change what has been established about it. Compatibility means the rail is kept working for integrations written against it, and is not where a new one should start.
Each rail needs its verification material registered once, so screening never fetches a key from a URL the counterparty controls:
/v1/integrations/acpalso ap2, ucp, evm
One rail is honest about its limits. MPP card mode carries a payload encrypted toward the merchant's own provider. Nobody outside that provider can verify it — the specification says so. We run structural checks, mark the result as not cryptographically verified, and the risk engine scores it accordingly. It is never presented as equivalent to the others.
Your policy
The limits are yours. We apply them; we do not invent them.
/v1/policy
/v1/policyowner key
| Setting | Effect |
|---|---|
per_transaction_limit | Above this, the payment is never waved through unexamined |
daily_limit_per_agent | Counted across the agent's last 24 hours, not per call |
human_hold_threshold | Above this amount, no automatic approval |
require_human_presence_above | Above this, an unattended payment raises the score |
merchant_whitelist / blacklist | Allowed and refused merchants |
currency | Limits are read in this currency only — a payment in another one is held, never converted silently |
Try a change before making it
/v1/policy/simulate
Replays your recent decisions against a proposed policy and reports how many would have come out differently. You see the cost of a change before your customers do.
Four eyes on a change
/v1/policy/change-requests
/v1/policy/change-requests/{id}/decisionpolicy_approver
A proposal records a snapshot of the policy it was made against. If the live policy moved in the meantime, the approval does not silently apply to something else.
/v1/policy/versions
Every change is versioned with who made it and when, so a decision taken last March can be read against the policy that was actually in force then.
Human review
HOLD is not a dead end. It is a queue with a name attached.
/v1/reviews?status=PENDING
/v1/reviews/{id}/resolve
curl -X POST https://api.trustedpai.com/v1/reviews/218/resolve \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"decision": "APPROVE",
"resolution_reason": "Called the customer, purchase confirmed",
"resolved_by": "risk-ops"
}'
A resolution without a reason is refused. Months later the question is never what was decided — it is why, and by whom.
Decision evidence
Every decision keeps the complete set of inputs it was made from.
/v1/transactions/{id}/evidence
Not a summary of the decision — the inputs themselves:
- the result of the cryptographic verification, and why it passed or failed
- the payment as we normalised it: amount, currency, merchant, whether a human was present
- the policy exactly as it stood at that moment
- the agent's history as it was read, not as it is now
- the instant the decision was taken, and the version of the rules that took it
/v1/transactions/evidence-export?limit=1000add &fingerprints=true when you need them
Fingerprints are off by default on the bulk export. Hashing is cheap for one record and not cheap for a thousand — you should not pay for it while paging through history.
Checking us yourself
You do not have to believe our answer. Reproduce it.
The rules are a pure function of the five inputs above. Given the same inputs, the same answer comes out — so the check runs on your machine, against a file, with no database, no network and no key.
curl "https://api.trustedpai.com/v1/transactions/4471/evidence" \ -H "X-API-Key: $KEY" > evidence.json python scripts/evidence/verify_decision_evidence.py evidence.json
Transaction: 4471
Recorded: HOLD, risk 55
Fingerprint: b6cf7aca0a6a930f...
MATCH — the recorded decision is the decision these inputs produce.
| Exit | Meaning |
|---|---|
0 | Match. The record supports the decision filed against it. |
1 | Mismatch, with the differences printed. This is the case the whole feature exists for. |
2 | Not checkable — recorded under a different rule version, or before we stored everything the engine reads. Reported as unverifiable, never as wrong. |
Why the third case is not swept under the rug. Calling an old record wrong would accuse a decision that may well have been right. The tool says it cannot judge, and says why.
Telling us what happened
You report a fact about the payment. You never grade our decision.
/v1/transactions/{id}/outcome
curl -X PUT https://api.trustedpai.com/v1/transactions/4471/outcome \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"outcome":"fraudulent","notes":"Chargeback received 12 days later"}'
| Outcome | Means |
|---|---|
legitimate | The payment was genuine |
fraudulent | It turned out to be fraud |
disputed | A chargeback or dispute followed |
not_executed | The payment never happened at all |
Whether our recommendation was right is derived by comparing the two. That is deliberate: an account cannot mark its own decision quality. Reporting again for the same transaction corrects the record rather than adding a second one, and the correction is written to the audit trail.
not_executed is excluded from the measurement. A payment we blocked that then never happened proves nothing either way. Counting it as a success would flatter every rejection we ever made.
Decision quality
Rates always come with the number they were computed from.
/v1/decision-quality
{
"outcome_coverage": 0.31,
"approved_and_labelled": 160,
"false_negatives": 23,
"false_negative_rate": 0.1437,
"blocked_and_labelled": 30,
"false_positives": 7,
"false_positive_rate": 0.2333,
"synthetic_outcomes": 0
}
- Read
outcome_coveragefirst. A fine accuracy over 2% coverage means very little. - A rate with no denominator is
null, not0. Zero would read as a perfect score earned on no evidence. synthetic_outcomescounts demonstration data. When it equals the labelled total, none of the figures above rest on a real payment — and you can see that rather than being told a shaped number.
Commerce reconciliation
Send what each of your systems says. We tell you where they disagree.
One observation at a time, each carrying which system it came from, which operation it describes, and the amount in minor units.
/v1/commerce/sandbox/events
curl -X POST https://api.trustedpai.com/v1/commerce/sandbox/events \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"journey_id": "order-4471",
"event_id": "evt_9f3c",
"operation_id": "cap-1",
"kind": "payment_captured",
"currency": "EUR",
"amount_minor": 12000,
"occurred_at": "2026-09-10T14:22:05Z"
}'
/v1/commerce/sandbox/journeys/{journey_id}
{
"journey_id": "order-4471",
"result": {
"status": "anomaly",
"findings": ["REFUND_WITHOUT_CAPTURE"],
"missing": ["payment_captured"]
}
}
The same event arriving twice counts once. Amounts stay exact integers throughout — no floating point ever touches money.
See it running on three orders, no key required.
What a finding means
Three statuses, and a named reason whenever something is wrong.
| Status | Means |
|---|---|
| consistent | The records you supplied agree. Not proof of settlement, and not proof your provider's history is complete. |
| incomplete | Something required was absent. This is never a pass. |
| anomaly | A named disagreement, reported alongside anything also missing. |
| Finding | In plain terms |
|---|---|
REFUND_WITHOUT_CAPTURE | A refund is recorded, but no capture was supplied. |
REFUND_EXCEEDS_CAPTURE | More was refunded than was ever collected. |
PAYMENT_TOTAL_MISMATCH | The amount taken is not the amount agreed. |
ORDER_TOTAL_MISMATCH | The order was written for a different amount. |
DUPLICATE_OPERATION_ACROSS_SOURCES | Two systems claim the same payment. We refuse to add them together. |
CONFLICTING_EVENT | The same event arrived twice carrying different values. |
CURRENCY_MISMATCH | Amounts in different currencies. We do not convert them behind your back. |
CONFLICTING_OPERATION | The same payment or refund was reported twice, for different amounts. Nothing here can decide which report is the true one. |
AMBIGUOUS_CHECKOUT_TOTAL | Two basket totals for one order — so no comparison against it is safe. |
AMBIGUOUS_ORDER_TOTAL | Two order totals for one order. The same refusal, on the other side. |
An amended basket is the usual cause of the two ambiguous findings. A journey identifies one frozen basket. If the customer changes it and the new total is recorded under the same journey, we are holding two baskets and no rule for choosing between them — so comparison stops rather than picking one. That is a refusal to guess, not a discrepancy in your data: every other check on the journey still runs, and refund coverage is still evaluated against what was actually captured.
Record each revision under its own journey identifier. The two are then compared separately, and each one answers for itself.
Webhooks
Signed, retried, and inspectable when they fail.
/v1/webhookHTTPS only
The secret is returned once. Every delivery is signed with it, so you can tell our call from anyone else's.
/v1/webhook-deliveries
The outbox: what was sent, how many attempts, what your endpoint answered. When a delivery fails you can see why instead of guessing.
/v1/webhook/test-receiver
A disposable endpoint of ours, so you can watch a full signed delivery arrive before pointing us at your own systems.
Usage
Counted when the decision is taken, never used to refuse one.
/v1/usage?months=12
{
"current_period": "2026-09",
"current_period_events": 2500,
"monthly_event_allowance": 10000,
"allowance_remaining": 7500,
"allowance_exceeded": false
}
The count is written in the same database transaction as the decision, so it survives the deletion of the decisions themselves under your retention setting. Going over an agreed volume is a conversation, not an outage: nothing is ever refused because of this figure.
Your data
You set how long we keep it, and you can take it out at any time.
/v1/data-retention1 to 3650 days
/v1/data-export
/v1/data-purge
Retention runs automatically at the interval you choose. A hold pins specific records against it — a dispute under investigation should not be deleted by a schedule.
/v1/retention-holds
/v1/audit-events
Every configuration change, policy edit, review resolution and purge is recorded with the actor who did it.
Screening receives the protocol-specific authorization material needed for the supported checks. Stored decision evidence is separate from the incoming request. Confirm the fields, credential handling and retention for your integration before sending production data.
Errors and limits
| Status | What happened |
|---|---|
401 | Key missing, invalid or deactivated. The response carries a WWW-Authenticate challenge naming the header we expect. |
403 | The key is valid but the role cannot reach this endpoint. |
404 | Not found — including records that belong to another account, which are never distinguished from records that do not exist. |
422 | The body did not match the schema. The response names the field. |
429 | 60 requests per minute per key, on a sliding window shared across our servers. |
503 | A dependency we need is unavailable. We fail closed: you get no answer rather than a cheerful one. |
We would rather return nothing than return an approval we cannot stand behind. Where a control cannot run, the answer is an error or a hold, never a pass.
Service status
/v1/healthno key needed
/v1/readychecks the database and the shared store
/v1/health answers if the process is up. /v1/ready answers only if the things it needs to give a real answer are reachable — use that one for your monitor.
A public page lives at api.trustedpai.com/status, and the full OpenAPI document at /docs.