Transfer SNS guide
Transfer SNS lets Wave initiate a transfer and receive asynchronous status updates through webhooks.
Base URL: https://api.gravv.xyz
Endpoint: POST /v1/transfer/sns
Transfer requirements (before POST /v1/transfer/sns)
You must satisfy all of the following:
- API credentials for the target environment.
- Required request headers:
Api-KeyandIdempotency-Key. - A configured webhook endpoint and signing secret.
- A valid source internal account id provisioned for your business.
- An External Account (Recipient) created with
POST /v1/external-accounts. - External Account (Recipient) status must be
activebefore transfer initiation.
Supported External Account (Recipient) types are bank_account and mobile_money. If destination.rail is supplied, the supported value is global_bank.
External Account (Recipient) setup
Create External Account (Recipient):
curl -X POST "https://api.gravv.xyz/v1/external-accounts" \
-H "Api-Key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"payee_type": "mobile_money",
"account_name": "Ada Lovelace",
"phone_number": "+254700000000",
"currency": "KES",
"institution_country_iso_code": "KE",
"customer_id": "<CUSTOMER_ID>"
}'
Expected creation response includes External Account (Recipient) status:
{
"id": "9a2e5d10-1111-1111-1111-111111111111",
"payee_type": "mobile_money",
"account_name": "Ada Lovelace",
"status": "waiting_approval",
"currency": "KES",
"customer_id": "c0ffee00-2222-2222-2222-222222222222"
}
Use this External Account (Recipient) as transfer destination only after status becomes active.
waiting_approval status (important)
The transfer service enforces an active-only destination rule. If the External Account (Recipient) is still waiting_approval, transfer initiation is blocked until approval is completed.
Status behavior:
waiting_approval— parked for maker-checker review; not transfer-targetableactive— transfer-targetablerejected— terminal non-transferable
In Gravv services, approval decisions are handled by approver roles (admin/owner) and waiting_approval is resolved by an explicit approve/reject action. For this integration, always check recipient status and proceed only when it is active.
Transfer flow
- Create the External Account (Recipient) destination.
- Wait for External Account (Recipient) status to become
active. - Call
POST /v1/transfer/snswith your source account, active external account recipient, and amount. - Store the returned
reference,transaction_id, and yourclient_referenceif supplied. - Process
transfer.status.*webhook events until the transfer reaches a terminal status.
Request
curl -X POST "https://api.gravv.xyz/v1/transfer/sns" \
-H "accept: application/json" \
-H "Api-Key: <YOUR_API_KEY>" \
-H "Idempotency-Key: <YOUR_IDEMPOTENCY_KEY>" \
-H "Content-Type: application/json" \
-d '{
"source": { "source_type": "internal_account", "id": "<SOURCE_ACCOUNT_ID>" },
"destination": { "destination_type": "external_account", "id": "<RECIPIENT_ID>" },
"amount": "100.00",
"additional_information": {
"remitter_full_name": "John Doe",
"remitter_country": "US",
"remitter_document_number": "A123456789",
"remitter_document_issue_date": "2020-01-15",
"remitter_document_expiry_date": "2030-01-15",
"remitter_date_of_birth": "1990-06-25",
"remitter_address": {
"address_line1": "123 Main Street",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"remitter_nationality": "US"
},
"description": "Wave payout",
"customer_id": "<CUSTOMER_ID>",
"client_reference": "wave-2026-06-18-001"
}'
| Field | Required | Description |
|---|---|---|
Api-Key |
Yes | API credential header. |
Idempotency-Key |
Yes | Unique key to safely deduplicate retries for the same transfer request. |
source.source_type |
Yes | Must be internal_account. |
source.id |
Yes | Source internal account id provisioned for your business. |
destination.destination_type |
Yes | Must be external_account. |
destination.id |
Yes | Id of an active External Account (Recipient) in your tenant. |
destination.rail |
No | Payment rail for the recipient payout. Use global_bank when supplied. |
amount |
Yes | Decimal amount as a string. |
additional_information |
No | Supplemental remitter information when required by payout rail/compliance. |
description |
No | Description for the transfer. |
customer_id |
No | Customer identifier when required by your integration. You can find it in Dashboard Manage → API Keys. |
client_reference |
No | Your correlation reference. Echoed back on webhook events when supplied. |
Response
A successful request returns the accepted transfer record. Final status is delivered asynchronously by webhook.
{
"reference": "5f8d0a11-4444-4444-4444-444444444444",
"transaction_id": "8d6a8c2b-5555-5555-5555-555555555555",
"transfer_status": "pending",
"amount": "100.00",
"local_amount": null,
"local_currency": "USD",
"date_created": "2026-06-18T10:15:30Z"
}
Lifecycle and webhook handling
Transfer SNS uses these status events:
pending ──▶ completed
└───▶ failed
transfer.status.pending— the transfer was accepted and is processing.transfer.status.completed— terminal success.transfer.status.failed— terminal failure.
Use event_group_id, reference, and client_reference to correlate webhook events with the transfer request.
Example status webhook:
{
"event_category": "transfer",
"event_type": "transfer.status.completed",
"event_group_id": "7c9e6679-3333-3333-3333-333333333333",
"timestamp": "2026-06-18T10:15:30Z",
"event_data": {
"reference": "5f8d0a11-4444-4444-4444-444444444444",
"status": "completed",
"amount": "100.00",
"client_reference": "wave-2026-06-18-001",
"remark": null,
"tx_hash": null,
"client_metadata": null
}
}
Webhook handlers should:
- Verify the webhook signature before processing.
- Match events by
referenceandclient_reference. - Update local state only for the received status transition.
- Return
2xxafter successful processing.
Common failure cases
Common error responses include:
400for validation errors, missing required fields, unsupported external account recipient types, inactive recipients, an unsupporteddestination.rail, or source/destination mismatches.401for missing or invalid credentials.403when the caller lacks the required transfer permission.
External Account (Recipient) setup can also fail validation if a bank_account recipient is missing account_number, a mobile_money recipient is missing phone_number, or required fields such as payee_type, account_name, currency, or customer_id are absent.
See the API reference for complete schemas and examples.