Transfers
A transfer is one Travel Rule exchange, outgoing or incoming, in the API key's environment. You create outgoing transfers; incoming ones arrive through your providers. Creating a transfer either completes it at once, when the Travel Rule does not apply, or queues it for screening and provider work.
For the lifecycle states and their transitions, see Transfer Lifecycle.
Endpoints
Section titled “Endpoints”| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /api/v2/transfers |
transfers:read |
List transfers |
| POST | /api/v2/transfers |
transfers:write |
Create an outgoing transfer |
| GET | /api/v2/transfers/:id |
transfers:read |
Get a transfer |
| PATCH | /api/v2/transfers/:id |
transfers:write |
Accept, reject or retry a transfer |
| GET | /api/v2/transfers/:id/information-requests |
transfers:read |
Read a transfer's information requests |
| POST | /api/v2/transfers/:id/information-requests |
transfers:write |
Act on a transfer's information requests |
A transfer of the other environment is not found: 404 TRANSFER_NOT_FOUND.
List Transfers
Section titled “List Transfers”curl "$COVALENT_URL/api/v2/transfers?direction=outgoing&state=held&limit=50" \ -H "x-api-key: $COVALENT_API_KEY"Query parameters:
| Parameter | Description |
|---|---|
direction |
incoming or outgoing. |
state |
A lifecycle state: created, queued, processing, exchanging, awaiting_counterparty, accepted, rejected, cancelled, completed, failed or held. |
status |
A compliance verdict: pending, approved or rejected; all is no filter. Takes precedence over complianceStatus. |
complianceStatus |
A compliance verdict: pending, approved or rejected. |
protocol |
The protocol of the provider that carried the transfer, such as notabene, veriscope, sygna, codevasp or gtr. At most 50 characters. |
search |
Part of the transfer's ID, its external ID, either VASP's ID, or either address, in any case. At most 200 characters. |
limit |
Items per page, 1 to 100, default 50. |
cursor |
meta.cursor of the previous page. |
Any other parameter, or a value a filter does not take, is 400 INVALID_QUERY. Transfers are listed newest first.
{ "data": [ { "id": "transfer_id", "direction": "incoming", "state": "held", "status": "pending", "asset": "ETH", "amount": "1.5", "network": "ethereum", "blockchainType": "Ethereum", "originator": { "name": "Northwind", "vaspId": "did:web:northwind.example", "vaspName": "Northwind", "address": "0x2222222222222222222222222222222222222222" }, "beneficiary": { "name": "Acme Exchange", "vaspId": "did:web:acme.example", "vaspName": "Acme Exchange", "address": "0x1111111111111111111111111111111111111111" }, "travelRule": { "protocol": "notabene", "matchedAt": "2026-10-08T10:05:00.000Z", "status": "pending" }, "screening": { "sanctions": { "status": "clear", "provider": "Elliptic" } }, "compliance": { "checks": [ { "name": "Elliptic", "status": "passed", "response": { "score": 12 } } ] }, "provider": { "id": "provider_id", "name": "notabene", "displayName": "Notabene", "protocol": "notabene" }, "externalId": "ext-1", "createdAt": "2026-10-08T10:00:00.000Z" } ], "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z", "cursor": null, "hasMore": false, "total": 1, "limit": 50 }}| Field | Description |
|---|---|
state |
The lifecycle state. |
status |
The compliance verdict: pending until there is one, then approved or rejected. |
originator, beneficiary |
Each side's VASP and address. name is the VASP's name, else its ID. |
travelRule |
The carrying protocol (pending until one carries it), when the exchange was accepted, and the originator side's status. |
compliance.checks |
Each screening check: passed, failed or pending, and the vendor's raw response. |
provider |
The provider that carried the transfer, or null. |
externalId |
Your Idempotency-Key for a transfer you created, or the external reference it arrived with. |
A key whose owner's role lacks pii:view gets every response as null, and meta.piiMasked: true.
Create Transfer
Section titled “Create Transfer”curl -X POST "$COVALENT_URL/api/v2/transfers" \ -H "x-api-key: $COVALENT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8c6c1d0e-7f43-4c55-9a51-4f2c3b1e2a90" \ -d '{ "asset": "ETH", "amount": "1.25", "network": "ethereum", "originatorCustomerId": "customer_id", "originatorAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "beneficiaryAddress": "0x1111111111111111111111111111111111111111", "originator": { "originatorPersons": [ { "naturalPerson": { "name": { "nameIdentifier": [ { "primaryIdentifier": "Doe", "secondaryIdentifier": "Jane", "nameIdentifierType": "LEGL" } ] }, "geographicAddress": [ { "addressType": "HOME", "country": "US" } ] } } ] }, "beneficiary": { "beneficiaryPersons": [ { "naturalPerson": { "name": { "nameIdentifier": [ { "primaryIdentifier": "Smith", "secondaryIdentifier": "John", "nameIdentifierType": "LEGL" } ] } } } ] }, "isUnhostedWallet": false }'| Field | Required | Description |
|---|---|---|
asset |
Yes | The asset's symbol, at most 20 characters. It must be enabled in the environment (see Assets). |
amount |
Yes | A decimal string, such as "1.25". |
network |
Yes | The network, at most 50 characters. |
originatorCustomerId |
Yes | The sending customer: a customer of the key's environment that is not deactivated. |
originatorAddress |
No | The sending address. When given, it must be a wallet registered to that customer. |
beneficiaryAddress |
Yes | The receiving address, at most 200 characters. A destination tag can follow as ?memo=, ?dt= or ?tag=. |
originator |
Yes | IVMS101 originator: at least one person, with a usable name, and a country in geographicAddress, countryOfResidence or countryOfRegistration. |
beneficiary |
No | IVMS101 beneficiary. Notabene, CodeVASP, Global Travel Rule and Sygna Bridge carry a transfer only with it, and check its persons when they send it: include at least one person. |
preferredProtocol |
No | notabene, veriscope, sygna, codevasp or gtr; Veriscope cannot send transfers yet (see Providers). The create is refused unless this protocol has an enabled provider able to carry the transfer. Covalent then tries that provider first, but another enabled provider may still carry the transfer: when that attempt fails before the provider has taken the transfer, or when the beneficiary VASP was last reached through another provider. Without it, any enabled provider able to carry the transfer is used. |
isUnhostedWallet |
No | true when the beneficiary address is a self-hosted wallet. |
beneficiaryVaspId is refused: Covalent discovers the receiving VASP.
Idempotency
Section titled “Idempotency”Send an Idempotency-Key header with every create. A key already used in the environment answers 200 with the transfer it created, whatever the body, and creates nothing; two requests sent at once with the same key create one transfer. The key is stored as the transfer's externalId.
{ "data": { "id": "transfer_id", "state": "queued", "createdAt": "2026-10-09T11:59:59.000Z" }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z", "idempotent": true, "message": "Transfer already exists for this Idempotency-Key." }}Create Outcomes
Section titled “Create Outcomes”| Status | Outcome |
|---|---|
200 |
The Idempotency-Key was used before: its transfer, with meta.idempotent: true. |
201 |
The amount is below the Travel Rule threshold: the transfer is completed at once. |
202 |
The transfer is queued: screening and the provider exchange follow. |
400 |
VALIDATION_ERROR: the body, the asset, the originator, or routing. See below. |
409 |
CONFLICT: two creates collided, and neither carried an Idempotency-Key to match them by. |
503 |
TIMEOUT: pricing the amount or checking the threshold timed out. Retry-After: 5. |
Below the threshold (201):
{ "data": { "id": "transfer_id", "state": "completed", "direction": "outgoing", "asset": "ETH", "amount": "0.01", "network": "ethereum", "originator": { "name": "Jane Doe", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "vasp": "Acme Exchange" }, "beneficiary": { "address": "0x1111111111111111111111111111111111111111", "verified": false }, "compliance": "approved", "routing": "skipped", "thresholdInfo": { "amount": 0.01, "asset": "ETH", "threshold": 1000, "thresholdCurrency": "GBP", "jurisdiction": "GB", "regulation": "UK MLRs 2017" }, "fxRateId": "fx_rate_id", "createdAt": "2026-10-09T11:59:00.000Z" }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z", "message": "Amount 0.01 ETH is below Travel Rule threshold of 1000 GBP (UK MLRs 2017)" }}Queued (202):
{ "data": { "id": "transfer_id", "state": "queued", "direction": "outgoing", "asset": "ETH", "amount": "1.25", "network": "ethereum", "originator": { "name": "Jane Doe", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "vasp": "Acme Exchange" }, "beneficiary": { "address": "0x1111111111111111111111111111111111111111", "verified": false }, "originatorStatus": "pending", "beneficiaryStatus": null, "compliance": "pending", "routing": "queued", "createdAt": "2026-10-09T11:59:00.000Z", "expiresAt": "2026-10-10T11:59:00.000Z" }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z", "jobId": "transfer:transfer_id:1791547140000", "message": "Transfer queued for processing. Poll GET /api/v2/transfers/{id} for status." }}A party's name is present when the IVMS101 data names one. Poll GET /api/v2/transfers/:id, or subscribe to webhooks, to follow a queued transfer.
A body that fails validation names every problem:
{ "error": "amount: Invalid", "code": "VALIDATION_ERROR", "details": { "validationErrors": ["amount: Invalid"], "schemaName": "create-transfer" }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z" }}The other 400 VALIDATION_ERROR refusals carry details.schemaName only, and say why in error: the asset is disabled in the environment, the originator customer is unknown or deactivated, the originator address is not that customer's wallet, the originator has no country, or no enabled provider can carry the transfer.
Get Transfer
Section titled “Get Transfer”curl "$COVALENT_URL/api/v2/transfers/transfer_id?checksLimit=50&checksOffset=0" \ -H "x-api-key: $COVALENT_API_KEY"| Parameter | Description |
|---|---|
checksLimit |
How many compliance checks to include, 1 to 200, default 50. A value out of range is read as the nearest bound; a value that is not a number, or 0, as the default. |
checksOffset |
How many checks to skip, default 0. |
{ "data": { "id": "transfer_id", "direction": "incoming", "state": "held", "asset": "ETH", "amount": "1.5", "network": "ethereum", "originator": { "address": "0x2222222222222222222222222222222222222222", "vaspId": "did:web:northwind.example", "vaspName": "Northwind" }, "beneficiary": { "address": "0x1111111111111111111111111111111111111111", "vaspId": "did:web:acme.example", "vaspName": "Acme Exchange" }, "status": { "ours": "pending", "theirs": "accepted" }, "rejection": null, "compliance": { "status": "pending", "total": 1, "checks": [ { "id": "check_id", "integration": "elliptic", "displayName": "Elliptic", "type": "sanctions", "status": "success", "response": { "score": 12 }, "durationMs": 800 } ] }, "cascade": { "provider": { "id": "provider_id", "name": "notabene", "displayName": "Notabene", "protocol": "notabene" }, "externalId": "ext-1", "attempts": [ { "id": "attempt_id", "provider": "notabene", "displayName": "Notabene", "protocol": "notabene", "order": 1, "status": "accepted", "externalId": "ext-1", "error": null, "durationMs": 340 } ] }, "txHash": null, "messages": [ { "type": "attestation", "direction": "received", "timestamp": "2026-10-08T10:00:00.000Z" } ], "expiresAt": "2026-10-09T10:00:00.000Z", "createdAt": "2026-10-08T10:00:00.000Z" }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z" }}| Field | Description |
|---|---|
status.ours, status.theirs |
Each side's Travel Rule status: pending, accepted or rejected, or null. |
rejection |
{ reason, details } once the transfer is rejected, else null. |
compliance.status |
The compliance verdict, or null before there is one. |
compliance.total |
How many checks the transfer has; checks is the page checksLimit and checksOffset ask for. |
compliance.checks[].status |
pending, success, error or skipped. response is null for a role without pii:view. |
cascade |
The provider that carried the transfer and each provider attempt, in order. |
expiresAt, acceptedAt, completedAt |
Present only once set. |
Transfer Actions
Section titled “Transfer Actions”PATCH /api/v2/transfers/:id takes one action. Every action answers the transfer's state after it:
{ "data": { "id": "transfer_id", "state": "accepted", "status": { "ours": "accepted", "theirs": "accepted" }, "txHash": null }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z", "action": "accept", "previousState": "held" }}An unknown transfer is 404 TRANSFER_NOT_FOUND, whatever the action. A body without an action, or with another one (cancel among them), is 400 INVALID_REQUEST:
{ "error": "Unknown action: cancel", "code": "INVALID_REQUEST", "details": { "action": "cancel", "allowedActions": ["accept", "reject", "retry"] }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z" }}An action the transfer's current state does not allow is also 400 INVALID_REQUEST, and error says why. A case linked to the transfer does not hold back an API key's accept or reject; the dashboard waits for the case to be resolved.
Accept
Section titled “Accept”curl -X PATCH "$COVALENT_URL/api/v2/transfers/transfer_id" \ -H "x-api-key: $COVALENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "accept", "txHash": "0x1111111111111111111111111111111111111111111111111111111111111111" }'- On an incoming transfer held for review (
state: held, compliancepending),acceptwithout atxHashaccepts it. - With a
txHash, or on any outgoing transfer,acceptconfirms the transfer's blockchain transaction. An outgoing transfer needs thetxHash. txHashis a 32-byte hexadecimal transaction hash, with or without0x. One that is not is400 VALIDATION_ERROR. AtxHashthat is not a non-empty string is ignored.
Reject
Section titled “Reject”curl -X PATCH "$COVALENT_URL/api/v2/transfers/transfer_id" \ -H "x-api-key: $COVALENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "reject", "status": "rejected", "reason": "COMPLIANCE_POLICY", "details": "Internal policy decision" }'| Field | Required | Description |
|---|---|---|
status |
Yes | rejected. |
reason |
Yes | A rejection reason: BENE_NOT_FOUND, BENE_NAME_MISMATCH, SANCTIONS_MATCH, HIGH_RISK, COMPLIANCE_POLICY, INVALID_DATA, EXPIRED or OTHER. |
details |
No | Free text, at most 500 characters. It is kept on the transfer's rejection, and out of the audit log. Only Veriscope passes it to the counterparty: Notabene and CodeVASP receive the reason as they map it, and Global Travel Rule and Sygna Bridge a fixed reason. |
A missing reason is 400 MISSING_REQUIRED_FIELD. A missing status, an unknown reason or details that are too long are 400 VALIDATION_ERROR, with details.validationErrors.
curl -X PATCH "$COVALENT_URL/api/v2/transfers/transfer_id" \ -H "x-api-key: $COVALENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "retry", "reason": "Retry after provider outage" }'A retry queues a failed or held transfer again. A transfer in any other state is 400 INVALID_TRANSFER_STATE, checked before the reason:
{ "error": "Cannot retry transfer in state 'completed'", "code": "INVALID_TRANSFER_STATE", "details": { "currentState": "completed", "action": "retry", "allowedStates": ["failed", "held"] }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z" }}reason is required (400 MISSING_REQUIRED_FIELD without it); it is kept for the record, out of the audit log and the activity feed.
Information Requests
Section titled “Information Requests”A transfer carried by Sygna Bridge can exchange further customer due diligence (CDD) information with the counterparty. Any other transfer answers 404 with the code SYGNA_TRANSFER_NOT_FOUND.
curl "$COVALENT_URL/api/v2/transfers/transfer_id/information-requests" \ -H "x-api-key: $COVALENT_API_KEY"{ "data": { "pending": true, "messages": [ { "id": "message_id", "direction": "inbound", "type": "REQUEST_CDD", "status": "received", "inReplyToId": null, "createdAt": "2026-10-09T11:00:00.000Z", "reviewedAt": null, "content": {} } ] }, "meta": { "apiVersion": 2, "timestamp": "2026-10-09T12:00:00.000Z" }}pending is true while a request or an answer waits for review; the transfer is held until then. A message's type is REQUEST_CDD, RESPOND_CDD, POST_CDD or REVIEW, and its direction inbound, outbound or internal. A request received for your customer carries a draft answer, drawn from the customer's verified Travel Rule identity; nothing is sent until you send it. A role without pii:view gets person data masked, and meta.piiMasked: true.
POST takes one action, and answers 202 with the message's messageId and status (such as queued); a review answers status: "reviewed" alone. The counterparty answers later.
| Action | Body | Effect |
|---|---|---|
request |
{ "action": "request", "idempotencyKey": "<uuid>", "fields": { ... } } |
Ask the counterparty for information. fields names at least one of: geographic_address, national_identification, date_and_place_of_birth (each a list of their fields), customer_identification, country_of_residence (true), other_cdd_info (text). |
reply |
{ "action": "reply", "idempotencyKey": "<uuid>", "requestId": "<message id>", "information": { ... } } |
Answer a request you received. |
post |
{ "action": "post", "idempotencyKey": "<uuid>", "information": { ... } } |
Send information unasked. |
review |
{ "action": "review", "reviewedMessageIds": ["<message id>"], "note": "..." } |
Record your review of messages, and release the hold. |
retry |
{ "action": "retry", "messageId": "<message id>" } |
Send a failed message again. |
information is a JSON object of at most 50 KB. An action that is not valid is 400 INVALID_INFORMATION_REQUEST, with details.issues. A refusal from Sygna Bridge keeps its status when it is 400, 404 or 409, and is 503 otherwise; its code is Sygna's.