Confirmation Callback
After processing a transfer webhook, your platform must send a confirmation callback to Bankroll indicating whether the transfer was accepted or refused.
Endpoint
POST {BANKROLL_API_BASE_URL}/api/webhooks/partner-transfer-confirmations
Request
The request body contains a confirmation object and its HMAC signature:
{
"confirmation": {
"partnerTransferId": 42,
"status": "accepted",
"metadata": {
"userId": 123,
"transferId": 456
}
},
"signature": "base64-encoded-hmac-signature"
}
Confirmation Fields
| Field | Type | Required | Description |
|---|
partnerTransferId | integer | Yes | The transfer ID from the original webhook |
status | string | Yes | "accepted" or "refused" |
reason | string | If refused | Explanation for why the transfer was refused |
metadata | object | No | Arbitrary JSON metadata for your records |
The signature field is an HMAC-SHA256 of the confirmation object, signed with the shared secret key. See Signature Verification.
Accepting a Transfer
When your platform successfully processes the transfer (e.g., credits the user’s account):
{
"confirmation": {
"partnerTransferId": 42,
"status": "accepted",
"metadata": {
"userId": 123,
"transferId": 456
}
},
"signature": "..."
}
On acceptance, Bankroll marks the transfer as complete and initiates payout to your partner wallet.
Refusing a Transfer
If your platform cannot process the transfer, refuse it with a reason. The user’s points will be refunded:
{
"confirmation": {
"partnerTransferId": 42,
"status": "refused",
"reason": "restricted_account",
"metadata": {
"userId": 123
}
},
"signature": "..."
}
Common refusal reasons:
restricted_account — the user’s account is restricted or suspended
user_not_found — the external ID does not match a user on your platform
invalid_amount — the transfer amount is invalid or outside allowed limits
The reason field is required when status is "refused". Requests without a reason will be rejected with a 400 error.
Response
Success
{
"success": true,
"status": "ACCEPTED"
}
Errors
| Status Code | Description |
|---|
400 | Invalid request body (missing fields, invalid types) |
401 | Invalid signature |
404 | Transfer ID not found |
409 | Conflict — cannot accept a refused transfer or refuse an accepted transfer |
500 | Internal server error |
Example
class BankrollCallbackService
CALLBACK_PATH = "/api/webhooks/partner-transfer-confirmations"
def self.deliver(partner_transfer_id:, status:, reason: nil, metadata: nil)
confirmation = {
partnerTransferId: partner_transfer_id,
status: status,
metadata: metadata
}
confirmation[:reason] = reason if status == "refused"
response = HTTParty.post(
"#{ENV.fetch("BANKROLL_API_BASE_URL")}#{CALLBACK_PATH}",
headers: { "Content-Type" => "application/json" },
body: {
confirmation: confirmation,
signature: sign(confirmation, ENV.fetch("BANKROLL_SECRET_KEY"))
}.to_json
)
raise "Callback failed: #{response.code}" unless response.success?
end
end
Timing
Send the confirmation callback promptly after processing the transfer. Bankroll has a reconciliation job that redispatches stale transfers that remain in the CREATED state for over 30 seconds, so timely confirmation prevents duplicate webhook delivery.