Receiving Webhooks
When a user initiates a transfer to your platform, Bankroll sends a transfer.created webhook to your configured endpoint.
Webhook Payload
{
"type": "transfer.created",
"transfer": {
"id": 42,
"usdAmountCents": 500,
"externalId": "user-123",
"externalName": "jane_doe",
"timestamp": 1741723200
},
"signature": "base64-encoded-hmac-signature"
}
Fields
| Field | Type | Description |
|---|
type | string | Always "transfer.created" |
transfer.id | integer | Unique transfer ID. Use this in your confirmation callback. |
transfer.usdAmountCents | integer | Transfer amount in USD cents (e.g., 500 = $5.00) |
transfer.externalId | string | null | The user’s ID on your platform |
transfer.externalName | string | null | The user’s display name on your platform |
transfer.timestamp | integer | Unix epoch seconds when the transfer was created |
signature | string | Base64-encoded HMAC-SHA256 of the transfer object |
Delivery
Webhooks are delivered with automatic retries and exponential backoff if your endpoint is unavailable. Your endpoint should return a 2xx status code to acknowledge receipt.
Handling the Webhook
Your endpoint should:
- Parse the JSON body
- Verify the HMAC signature using the
transfer object and the signature field
- Check that the
externalId maps to a valid user on your platform
- Decide whether to accept or refuse the transfer
- Send a confirmation callback to Bankroll
Always verify the signature before processing the transfer. Reject any request with an invalid or missing signature.
Idempotency
The same webhook may be delivered more than once. Use transfer.id to deduplicate — if you’ve already processed a transfer with that ID, return 200 OK without reprocessing.
Example
class TransferWebhooksController < ApplicationController
def create
payload = JSON.parse(request.raw_post)
return head :bad_request unless payload
# Verify signature
transfer = payload["transfer"]
signature = payload["signature"]
return head :unauthorized unless valid_signature?(transfer, signature)
# Check for duplicates
return head :ok if Transfer.exists?(partner_transfer_id: transfer["id"])
# Look up the user
user = User.find_by(id: transfer["externalId"])
return head :unprocessable_entity unless user
amount_cents = transfer["usdAmountCents"].to_i
return head :unprocessable_entity unless amount_cents.positive?
# Process and send confirmation callback
process_transfer(transfer["id"], user, amount_cents)
head :ok
end
end