Skip to main content

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

FieldTypeDescription
typestringAlways "transfer.created"
transfer.idintegerUnique transfer ID. Use this in your confirmation callback.
transfer.usdAmountCentsintegerTransfer amount in USD cents (e.g., 500 = $5.00)
transfer.externalIdstring | nullThe user’s ID on your platform
transfer.externalNamestring | nullThe user’s display name on your platform
transfer.timestampintegerUnix epoch seconds when the transfer was created
signaturestringBase64-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:
  1. Parse the JSON body
  2. Verify the HMAC signature using the transfer object and the signature field
  3. Check that the externalId maps to a valid user on your platform
  4. Decide whether to accept or refuse the transfer
  5. 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