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.
- Gravv receives the wire. Gravv does not convert a USD wire into a USD account, so it sends no
transfer.inflow.deposit_received. - Gravv credits 2,500 USD to your account, sends
transfer.inflow.completed, and writes an incomingaccount_fundingrow. - If your pricing has a fee for incoming wires, Gravv bills it to your business on a separate
fee_chargerow. The account keeps the full 2,500 USD.
Before you start
- You have an
activeaccount with deposit details. To set one up, see Share account details and receive a deposit. - You have registered your webhook URL in the Dashboard.
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_receivedwhen Gravv converts the deposit, ortransfer.inflow.awaiting_approvalon accounts that hold deposits for approval.- Then
transfer.inflow.completedortransfer.inflow.deposit_settled. - On a failure,
transfer.inflow.payout_failedortransfer.inflow.compensated. - For on-chain wallet deposits,
inflow.onchain.completedorinflow.onchain.failed, withstatusin 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_namewasSandbox Test Sender.narrationwassandbox simulated pay-in.session_idwas equal to the simulator'sreference.transaction_idwas equal to the row'sid.
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>.