Skip to main content

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

FieldTypeRequiredDescription
partnerTransferIdintegerYesThe transfer ID from the original webhook
statusstringYes"accepted" or "refused"
reasonstringIf refusedExplanation for why the transfer was refused
metadataobjectNoArbitrary 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 CodeDescription
400Invalid request body (missing fields, invalid types)
401Invalid signature
404Transfer ID not found
409Conflict — cannot accept a refused transfer or refuse an accepted transfer
500Internal 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.