Skip to content

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.

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.

Terminal window
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.

Terminal window
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.

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."
}
}
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.

Terminal window
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.

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.

Terminal window
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, compliance pending), accept without a txHash accepts it.
  • With a txHash, or on any outgoing transfer, accept confirms the transfer's blockchain transaction. An outgoing transfer needs the txHash.
  • txHash is a 32-byte hexadecimal transaction hash, with or without 0x. One that is not is 400 VALIDATION_ERROR. A txHash that is not a non-empty string is ignored.
Terminal window
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.

Terminal window
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.

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.

Terminal window
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.