Skip to content

Transfer SNS guide

Transfer SNS lets Wave initiate a transfer and manage status progression for SNS transactions, with lifecycle updates delivered through webhooks.

Base URL: https://api.gravv.xyz

Endpoints:

  • POST /v1/transfer/sns
  • POST /v1/transactions/sns/{identifier}/status

Transfer requirements (before POST /v1/transfer/sns)

You must satisfy all of the following:

  1. API credentials for the target environment.
  2. Required request headers: Api-Key and Idempotency-Key.
  3. A configured webhook endpoint and signing secret.
  4. A valid source internal account id provisioned for your business.
  5. An External Account (Recipient) created with POST /v1/external-accounts.
  6. External Account (Recipient) status must be active before 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-targetable
  • active — transfer-targetable
  • rejected — 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

  1. Create the External Account (Recipient) destination.
  2. Wait for External Account (Recipient) status to become active.
  3. Call POST /v1/transfer/sns with your source account, active external account recipient, and amount.
  4. Store the returned reference, transaction_id, and your client_reference if supplied.
  5. 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.

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

Update SNS transaction status

Use POST /v1/transactions/sns/{identifier}/status to update an SNS transaction to a terminal state.

You can use any of the following as {identifier}:

  • client_reference
  • reference
  • transaction_id
curl -X POST "https://api.gravv.xyz/v1/transactions/sns/<IDENTIFIER>/status" \
  -H "accept: application/json" \
  -H "Api-Key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "completed",
    "remark": "Wave SNS status update"
  }'

Status update fields

Field Required Description
status Yes Target terminal status. Supported values are completed and failed.
remark No Optional status note attached to the transfer event payload.
Api-Key Yes API credential header.
{identifier} Yes One of client_reference, reference, or transaction_id for an SNS transfer.

Example response:

{
  "success": true,
  "workflow_id": "6e26e001-6666-6666-6666-666666666666",
  "new_status": "completed",
  "message": "SNS workflow 6e26e001-6666-6666-6666-666666666666 updated to completed"
}

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.

When POST /v1/transactions/sns/{identifier}/status is used, Gravv emits the corresponding terminal webhook event (transfer.status.completed or transfer.status.failed).

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:

  1. Verify the webhook signature before processing.
  2. Match events by reference and client_reference.
  3. Update local state only for the received status transition.
  4. Return 2xx after successful processing.

Common failure cases

Common error responses include:

  • 400 for validation errors, missing required fields, unsupported external account recipient types, inactive recipients, an unsupported destination.rail, or source/destination mismatches.
  • 401 for missing or invalid credentials.
  • 403 when 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.