See a deposit arrive

Follow a bank deposit from its webhook to its transaction row and its fee row, and match it to the sender.

This recipe follows a bank deposit from its webhook to its transaction row and its fee row, and matches it to the sender. It is for a US staffing platform whose employer clients wire USD into the platform's Gravv account.

Status. Available.

Money example

Acme Staffing LLC wires 2,500 USD to your USD account.

  1. Gravv receives the wire. Gravv does not convert a USD wire into a USD account, so it sends no transfer.inflow.deposit_received.
  2. Gravv credits 2,500 USD to your account, sends transfer.inflow.completed, and writes an incoming account_funding row.
  3. If your pricing has a fee for incoming wires, Gravv bills it to your business on a separate fee_charge row. The account keeps the full 2,500 USD.

Before you start

Find out what is required

The deposit webhook carries the transaction_id of the row. The row tells you the rail in channel and the sender in sending_account_name.

Steps

1. Converting accounts only: receive transfer.inflow.deposit_received

Some accounts convert what they receive. For example, an account receives USD and credits USDC. On those accounts, Gravv sends this event when the conversion starts. event_data.amount is the estimated converted amount, and currency is the currency Gravv credits. The event also carries account_id, sender_name, sender_bank and narration. A USD wire into a USD account skips this step.

2. Receive the credit

Gravv sends transfer.inflow.completed. On accounts that settle on chain, it sends transfer.inflow.deposit_settled with tokenized: true instead. Store event_data.transaction_id for the next step.

3. Read the row

Read the transaction row by that transaction_id.

curl --request GET \
 --url https://api.gravv.xyz/v1/transactions/ec1b8ca1-f91e-4d4c-9ff4-9b7889463e85 \
 --header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions/ec1b8ca1-f91e-4d4c-9ff4-9b7889463e85", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os

import requests

response = requests.get(
"https://api.gravv.xyz/v1/transactions/ec1b8ca1-f91e-4d4c-9ff4-9b7889463e85",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
  "data": {
    "id": "ec1b8ca1-f91e-4d4c-9ff4-9b7889463e85",
    "customer_id": "5be6128e-18c2-4797-a142-ea7d17be3111",
    "amount": "2500",
    "date_created": "2026-10-13T08:02:11Z",
    "direction": "in",
    "transaction_type": "account_funding",
    "description": "Wire deposit from ACME STAFFING LLC",
    "sending_account_id": null,
    "receiving_account_id": "43b7a3a6-9a8d-4a03-980d-7b71d8f56413",
    "sending_account_name": "ACME STAFFING LLC",
    "sending_account_number": "000123456789",
    "channel": "wire",
    "status": "completed",
    "currency": "USD"
  },
  "error": null
}

On accounts that do not settle on chain, description reads like Inbound USD deposit settlement and channel is bank_transfer. When the sender is unknown, sending_account_name is External Sender. Do not rely on balance_before and balance_after on deposit rows.

4. Find the fee

curl --request GET \
 --url 'https://api.gravv.xyz/v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=fee_charge' \
 --header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=fee_charge", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os

import requests

response = requests.get(
"https://api.gravv.xyz/v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=fee_charge",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())

Gravv never takes a deposit fee out of the deposit, so Gravv credits the account with the full 2,500 USD. Gravv takes the fee from your business's main account, or puts it on your monthly invoice if your plan bills that way. Look for the fee row by the deposit row's customer_id, not by the receiving account. The fee row has a description that starts with Fee charge: and a date_created shortly after the deposit. A fee on the monthly invoice has no row here. See Add up a month of fees.

5. List all deposits on the account

curl --request GET \
 --url 'https://api.gravv.xyz/v1/transactions?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=in&transaction_type=account_funding,crypto_funding' \
 --header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=in&transaction_type=account_funding,crypto_funding", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os

import requests

response = requests.get(
"https://api.gravv.xyz/v1/transactions?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=in&transaction_type=account_funding,crypto_funding",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())

What can go wrong

Response Why Fix
transfer.inflow.deposit_received, then nothing for a long time The conversion has started and Gravv is waiting for it to settle Nothing to send. If it does not complete, contact support with the transaction_id and the amount
transfer.inflow.awaiting_approval Your account holds deposits for approval Approve it in the Dashboard
transfer.inflow.payout_failed (with error and failed_at) or transfer.inflow.compensated Crediting failed, or was reversed Contact support with the transaction_id
Two webhooks for one on-chain deposit: inflow.onchain.completed and a transfer.inflow.* event Two parts of Gravv each send an event for it Link them by settlement_event_group_id (the on-chain event's event_group_id) or by tx_hash
A row typed crypto_funding becomes account_funding later Gravv relabels the on-chain credit once the bank side identifies it Key on id, never on the type
No fee row for some deposits Some rails carry no fee on your pricing No action needed

Webhooks you will receive

Gravv sends these events:

  • transfer.inflow.deposit_received when Gravv converts the deposit, or transfer.inflow.awaiting_approval on accounts that hold deposits for approval.
  • Then transfer.inflow.completed or transfer.inflow.deposit_settled.
  • On a failure, transfer.inflow.payout_failed or transfer.inflow.compensated.
  • For on-chain wallet deposits, inflow.onchain.completed or inflow.onchain.failed, with status in upper case.

In webhook history, deposit events are in the transfer category. See Deposit events.

Testing in sandbox

Simulate the wire with your sandbox key. Each new Idempotency-Key makes a new deposit.

curl --request POST \
 --url https://api.gravv.xyz/v2/accounts/simulate/deposit \
 --header 'Api-Key: <API_KEY>' \
 --header 'Idempotency-Key: bc940866-b0d8-4b6a-ac1c-fe56c774550c' \
 --header 'Content-Type: application/json' \
 --data '
{
"account_id": "43b7a3a6-9a8d-4a03-980d-7b71d8f56413",
"amount": "2500.00",
"rail": "wire"
}
'
const response = await fetch("https://api.gravv.xyz/v2/accounts/simulate/deposit", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "bc940866-b0d8-4b6a-ac1c-fe56c774550c",
"Content-Type": "application/json",
},
body: JSON.stringify({
"account_id": "43b7a3a6-9a8d-4a03-980d-7b71d8f56413",
"amount": "2500.00",
"rail": "wire"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v2/accounts/simulate/deposit",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Idempotency-Key": "bc940866-b0d8-4b6a-ac1c-fe56c774550c",
    "Content-Type": "application/json",
},
json={
    "account_id": "43b7a3a6-9a8d-4a03-980d-7b71d8f56413",
    "amount": "2500.00",
    "rail": "wire",
},
)
print(response.status_code, response.json())

The response is {"data":{"accepted":true,"reference":"…","state":"pending"},"error":null}. Wait for the webhooks, then run steps 3 to 5.

For example, a simulated 5,000 NGN deposit sent transfer.inflow.completed with these values:

  • sender_name was Sandbox Test Sender.
  • narration was sandbox simulated pay-in.
  • session_id was equal to the simulator's reference.
  • transaction_id was equal to the row's id.

The row had transaction_type account_funding, channel bank_transfer and status completed. The deposit carried no fee, so no fee_charge row appeared. A deposit event's idempotency_key is a long hash, not <reference>:<sequence>.

Reference

Was this page helpful?