API reference

Send us everything your own system holds: payments, the people behind them, their accounts and wallets, Travel Rule transfers, screening you did yourself, and the documents. Ask us about a payment you have not released and the answer is allow, hold or block. Read back what the rules raised and what is waiting on a person.

Authentication

Every request carries a bearer key, issued in the workspace under Integrations, API access. Keys are stored hashed and can be revoked at any time.

A key writes to one book. A key beginning prv_test_ writes to your sandbox book instead: same endpoints, same rules, none of it in your real records.

Three hundred requests a minute by default. Every response says how many you have left.

GET/api/v1/ping

Check the key and the book it writes to

key is the name you gave the key. book is the reporting entity it writes to. If the book is wrong, the key is wrong.

Request

curl https://app.pruvyo.com/api/v1/ping \
  -H "Authorization: Bearer prv_live_…"

Response

{"ok":true,"key":"Core banking","book":"Acme Payments UAB"}
POST/api/v1/transactions

Record what already happened

Up to 500 rows a request. They land on the same ledger, through the same engine, into the same alert book as a file import.

The answer names every alert the rules raised and points each one at the row that caused it. A rejected row never stops the good ones in the same request.

Fiat and crypto go through this one endpoint.

FieldTypeMeaning
occurred_atrequiredstringISO 8601. Also accepts date or timestamp. Refused if more than a day ahead.
amountrequirednumber | stringMajor units. 1.250,50 and 1,250.50 both parse. Or amount_minor as an integer.
referencestringYour id. Send it and a retry answers duplicate instead of doubling your ledger. Also accepts external_ref or id.
currencystringThree letters. EUR by default. A crypto ticker here is read as a quantity of that asset, not as money.
directionin | outOmit it and the sign of the amount decides.
customerstringThe person or company on your side of the payment.
counterpartystringWho was on the other side.
counterparty_countrystringTwo-letter country of the counterparty.
assetstringFor crypto. The ticker moved.
tx_hashstringFor crypto. Marks the row on-chain.
counterparty_addressstringFor crypto. Sanctions screening needs it: without it the sanctions rules report themselves not evaluated for that payment rather than passing it.
payment_type, railstringHow it moved, where your own vocabulary has one.
accountstringWhich of your accounts the payment came from. Also accepts account_ref or account_id. Send it and a customer identified weeks later attributes these rows backwards.

Request

curl -X POST https://app.pruvyo.com/api/v1/transactions \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"transactions":[{
        "reference": "your-id-0001",
        "occurred_at": "2026-08-01T09:30:00Z",
        "amount": 88000.50,
        "currency": "EUR",
        "direction": "in",
        "customer": "Yuki Tanaka",
        "counterparty": "Nordbank AG",
        "counterparty_country": "DE"
      }]}'

Response

{"batch_id":"b_3f9a…",
 "received":1,
 "imported":1,
 "duplicates":[],
 "rejected":[],
 "unmatched_customers":[],
 "ambiguous_customers":[],
 "alerts":[{"index":0,"ref":"AL-2291","rule":"Large inbound from a new counterparty","severity":"high"}],
 "engine":{"rules_evaluated":34,"unmeasured":[]}}
POST/api/v1/customers

The people and companies behind the payments

A customer is the identity everything else joins on, so the reference is what makes a payment and a wallet belong to the same person.

FieldTypeMeaning
referencerequiredstringYour id for them. Everything else joins on it.
namerequiredstringWhat they are called.
typeperson | companyPerson by default. individual, retail and natural also read as person; business, corporate, legal and entity read as company.
riskrequiredstringYour own rating. An unknown word is refused rather than rounded to something.
countrystringTwo letters.
kyc_statestringWhere their verification stands.
onboarded_atstringISO 8601.
expected_monthly_eurnumberWhat you expect them to move, so the rules can notice when they do not.

Request

curl -X POST https://app.pruvyo.com/api/v1/customers \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"customers":[{
        "reference": "cus-4471",
        "name": "Yuki Tanaka",
        "type": "person",
        "country": "JP",
        "risk": "medium"
      }]}'
POST/api/v1/wallets

The addresses those customers hold

Control of a wallet is proved by a signed message and nothing else. Send the proof and the wallet lands verified; send it without and it lands declared.

FieldTypeMeaning
customerrequiredstringThe customer reference this address belongs to.
addressrequiredstringThe address itself.
chainrequiredstringBTC, ETH, TRON or SOL. Anything else is refused rather than guessed at.
labelstringWhat your team calls it.
custodialbooleanTrue where a provider holds the keys, so it lands as custody rather than as theirs.
proofobjectmessage and signature. A message naming the address, signed by it.

Request

curl -X POST https://app.pruvyo.com/api/v1/wallets \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"wallets":[{
        "customer": "cus-4471",
        "chain": "ETH",
        "address": "0x9f2c…",
        "proof": {"message": "I control 0x9f2c… for Pruvyo", "signature": "0x…"}
      }]}'
POST/api/v1/accounts

The accounts their money sits in

Declare an account so the payments that name it attribute to somebody. Send the customer first: an account belonging to nobody is a row that never joins, and it is refused by naming the customer rather than the account.

Venue and reference together are the identity, so the same account sent twice updates rather than doubles.

FieldTypeMeaning
customerrequiredstringThe customer reference this account belongs to.
venuerequiredstringWhere it is held, as you call it: the bank, the exchange.
referencerequiredstringIBAN, account number, or the venue's own reference. Also accepts account_ref or iban.
labelstringWhat your team calls it.

Request

curl -X POST https://app.pruvyo.com/api/v1/accounts \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"accounts":[{
        "customer": "cus-4471",
        "venue": "Nordbank AG",
        "reference": "DE89370400440532013000",
        "label": "Treasury"
      }]}'
POST/api/v1/transfers

Travel Rule transfers

A transfer message is data about a payment, not the payment, so these land on the Travel Rule console and never on the ledger. Sending the payment as well is correct: they are two records of one event.

What the regulation wants and your record does not hold is worked out here, not taken from you, and comes back per transfer as incomplete. It is the whole working of the module, so a caller who could declare nothing missing would be switching off their own Travel Rule.

FieldTypeMeaning
referencerequiredstringYour id for the transfer. The console's reference and its de-duplication are both built off it.
directionrequiredin | outAlso accepts incoming, outgoing, sent, received.
occurred_atrequiredstringISO 8601.
amountnumberFiat value. Leave it out for an unpriced crypto transfer: it stays a quantity rather than being read as zero and falling under every threshold.
currencystringThree letters. EUR by default.
asset, asset_amountstringThe ticker and how much of it moved.
originatorobjectname, account, address, country. Who sent it.
beneficiaryobjectThe same four. Who receives it.
counterparty_vaspstringThe exchange or custodian on the other side.
tx_hash, counterparty_addressstringThe chain record, where there is one.

Request

curl -X POST https://app.pruvyo.com/api/v1/transfers \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"transfers":[{
        "reference": "tr-9001",
        "direction": "out",
        "occurred_at": "2026-08-30T09:30:00Z",
        "amount": 12500,
        "currency": "EUR",
        "originator": {"name": "Yuki Tanaka", "account": "cus-4471"},
        "beneficiary": {"name": "Meridian Trading", "account": "0x9f2c"}
      }]}'

Response

{"received":1,"landed":1,"rejected":[],
 "incomplete":[{"index":0,"reference":"tr-9001","missing":["Beneficiary account"]}]}
POST/api/v1/kyc-checks

Identity, PEP and adverse media you ran

The customer record takes a kyc_state as one word. This takes the checks behind it. The risk model reads them, so a customer whose file says verified and whose checks we never received is scoring against nothing.

Anything that did not pass must carry a reason. A match nobody can explain is noise, and noise is what gets ignored.

FieldTypeMeaning
customerrequiredstringThe customer reference the check is about.
providerrequiredstringWho ran it.
kindrequiredstringidentity, liveness, address, sanctions, pep or adverse_media.
resultrequiredstringpass, fail, match or not_run.
detailstringRequired whenever the result is not pass. Plain language.
checked_atstringISO 8601. Now, if you leave it out.
expires_onstringWhen it goes stale. A passport expires on its own date; screening ages.

Request

curl -X POST https://app.pruvyo.com/api/v1/kyc-checks \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"checks":[{
        "customer": "cus-4471",
        "provider": "ComplyAdvantage",
        "kind": "pep",
        "result": "match",
        "detail": "Named on the UK consolidated list, 2019",
        "expires_on": "2027-01-31"
      }]}'
POST/api/v1/screening

Screening you did yourself

If you screen an address with a provider you already pay for, send us the answer. Without it Pruvyo either screens it again, which you pay for twice, or reports the control as not evaluated on that payment.

The verdict is a boolean because a control is binary. The categories sit beside it so why survives the boolean, and raw keeps whatever your provider actually said, so a disputed verdict can be re-read years later rather than argued from a summary.

FieldTypeMeaning
addressrequiredstringWhat was screened.
providerrequiredstringWho answered. A verdict nobody is named for is one nobody can stand behind at an inspection.
sanctionedrequiredbooleanTrue or false. A missing verdict is refused rather than read as clean.
categoriesstring[]Every category they named.
checked_atstringISO 8601. Now, if you leave it out.
rawobjectTheir answer, kept whole.

Request

curl -X POST https://app.pruvyo.com/api/v1/screening \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"results":[{
        "address": "0x9f2c…",
        "provider": "Chainalysis",
        "sanctioned": false,
        "categories": ["exchange"]
      }]}'
POST/api/v1/documents

Evidence your own system holds

Multipart, because this one carries a file. Everything else here is JSON. Up to 25 MB.

The bytes are read and the checks are run before anything is said about the file, the same as a document dragged onto the screen. A file nobody has examined is never recorded as clean, and an API door is not an excuse to skip a check the screen cannot skip. The answer says what the checks found, or that they did not run and why.

The same bytes arriving twice is a re-send, not a second document: you get the id of the one already on file and nothing is added.

FieldTypeMeaning
filerequiredfileThe document itself.
customerrequiredstringThe customer reference it belongs to.
kindrequiredstringkyc_id, proof_address, source_of_funds, bank_statement, incorporation, screening, filing_pack or other.

Request

curl -X POST https://app.pruvyo.com/api/v1/documents \
  -H "Authorization: Bearer prv_live_…" \
  -F "file=@statement.pdf" \
  -F "customer=cus-4471" \
  -F "kind=bank_statement"

Response

{"document_id":"doc_5b1e…",
 "name":"statement.pdf",
 "kind":"bank_statement",
 "sha256":"9f2c…",
 "size_bytes":184320,
 "checks":{"ran":true,"verdict":"passed"}}
POST/api/v1/decisions

Ask before money moves

The same shape as a transaction, plus a required reference. This asks about a payment you have not released.

The answer is allow, hold or block, with the rules that decided it named. Where the rules can answer it comes back immediately; where they cannot, it is held for a person.

One reference gets one verdict, for ever. A retry replays the stored answer rather than evaluating again, so the same payment cannot be allowed at nine and blocked a minute later. The answer says replayed so you can tell.

not_evaluated names the controls that exist but could not be measured on this payment: what an allow was not checked against.

If Pruvyo is unreachable you get an error, never a verdict. Whether that means hold or release is your policy to set.

Request

curl -X POST https://app.pruvyo.com/api/v1/decisions \
  -H "Authorization: Bearer prv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"reference": "payment-4471",
       "occurred_at": "2026-08-01T09:30:00Z",
       "amount": 250000,
       "currency": "EUR",
       "direction": "out",
       "customer": "cus-4471",
       "counterparty": "Meridian Trading",
       "counterparty_country": "AE"}'

Response

{"decision_id":"dec_8f21…",
 "reference":"payment-4471",
 "verdict":"hold",
 "reasons":["Sanctions screening"],
 "not_evaluated":[],
 "review":{"ref":"HD-118","status":"pending","decided_at":null,"note":null},
 "replayed":false,
 "latency_ms":41,
 "decided_at":"2026-08-30T09:30:01Z"}
GET/api/v1/decisions/{id}

Read the answer back

Poll it until the review answers, or register a callback below and stop polling.

Response

{"decision_id":"dec_8f21…",
 "reference":"payment-4471",
 "verdict":"allow",
 "reasons":[],
 "not_evaluated":[],
 "review":null,
 "replayed":true,
 "decided_at":"2026-08-30T09:30:01Z"}

Callbacks

Register a callback address against your key and Pruvyo posts to it the moment an analyst decides. It fires for a decision you asked for and for a hold a rule placed on a payment you reported, which are told apart by the event name.

The post carries the decision id and your own reference. No verdict, no customer, no amount: you read the result back with your key. A forged or replayed callback is therefore harmless, and there is no signature scheme for either side to get wrong.

Answer with any 2xx within five seconds. Anything else is retried until it lands, up to eight attempts, and polling keeps working the whole time.

Request

POST https://your-system.example/pruvyo/decisions
Content-Type: application/json

{"event":"decision.decided","decision_id":"dec_8f21…","reference":"payment-4471"}

{"event":"hold.decided","hold_ref":"HD-118","reference":"HD-118"}
GET/api/v1/holds

What is waiting on a person

Everything held in your book, newest first, whether it was held through a decision you asked for or by a rule on a payment you reported. Before this existed, a payment you pushed could be held and there was nothing to poll.

Send the last placed_at you saw as since and a loop never re-reads what it has handled. next_since in the answer is the stamp to send next time. Up to 500 a page.

Read only. A hold is released by a person with the standing to release it, under four eyes; a key deciding one would move that authority to whoever holds the key.

Request

curl "https://app.pruvyo.com/api/v1/holds?since=2026-08-30T09:00:00Z" \
  -H "Authorization: Bearer prv_live_…"

Response

{"holds":[{
   "ref":"HD-118",
   "status":"pending",
   "verdict":null,
   "reason":"Sanctions screening",
   "rule":"Sanctions screening",
   "customer":"cus-4471",
   "amount_minor":25000000,
   "currency":"EUR",
   "placed_at":"2026-08-30T09:00:00Z",
   "decide_by":"2026-08-30T13:00:00Z"
 }],
 "next_since":"2026-08-30T09:00:00Z"}
GET/api/v1/alerts

What the rules raised

The push response names the alerts your rows raised in that request. This is everything else: a rule that counts a week of payments, an alert from a connection's own sync, a screening result that arrived after the payment did.

reference is your own id for the payment that caused it, so an alert lands against the row you sent.

Request

curl "https://app.pruvyo.com/api/v1/alerts?since=2026-08-30T09:00:00Z&limit=100" \
  -H "Authorization: Bearer prv_live_…"

Response

{"alerts":[{
   "ref":"AL-2291",
   "title":"Large inbound from a new counterparty",
   "severity":"high",
   "status":"open",
   "rule":"Large inbound from a new counterparty",
   "customer":"cus-4471",
   "reference":"your-id-0001",
   "raised_at":"2026-08-30T09:05:00Z"
 }],
 "next_since":"2026-08-30T09:05:00Z"}

Errors

401Key missing, unknown or revoked.
413More than 500 rows in one request.
422Nothing readable. The answer lists each rejected row with its field and the reason.
429Over this key's rate ceiling. Retry-After says when to come back.
503No API configured on this deployment.

Response

{"rejected":[
   {"index":1,"field":"occurred_at","reason":"not a date we can read: \"nonsense\""},
   {"index":2,"field":"currency","reason":"expected a 3-letter code, got \"EURO\""}
]}

Keys are issued in the workspace under Integrations, API access. Create account, then talk to us to have it switched on.