RDC Scanning Webhooks
GoodFunds Gateway uses batched webhooks to notify your system about Remote Deposit Capture (RDC) check processing events. Webhooks are sent in batches to improve efficiency and reduce HTTP overhead. Check images are delivered via secure, time-limited URLs rather than base64-encoded data.
Overview
The RDC Scanning system automatically processes check images through Azure Document Intelligence, extracts all relevant data, and delivers it to your webhook endpoint in batches. Each batch contains multiple transactions with pagination metadata. Check images are stored securely on our servers and accessed via signed, expiring URLs.
Authentication
All webhooks are signed using HMAC-SHA256 to verify they originated from GoodFunds Gateway. Each request includes the following headers:
| Header | Description |
|---|---|
X-Webhook-Signature |
HMAC-SHA256 signature of the raw request body using your webhook token |
X-Webhook-Timestamp |
UNIX timestamp when the webhook was sent (use to prevent replay attacks) |
X-Client-ID |
Your unique client identifier |
X-Webhook-Version |
Webhook version (currently "1.0") |
Verifying Webhook Signatures
To verify a webhook's authenticity:
- Get the raw request body (do not modify or pretty-print it)
- Retrieve your client's secret webhook token from your dashboard
- Generate an HMAC-SHA256 hash of the request body using your token
- Compare your generated signature with the
X-Webhook-Signatureheader using a constant-time comparison function - Check that the timestamp is within 5 minutes of your server's current time to prevent replay attacks
Webhook Payload Format
Webhooks are sent in batches with pagination. Each transaction includes BOTH a base64-encoded image (for immediate display) AND a secure URL (for on-demand retrieval).
{
"meta": {
"batch_id": "20260610-01",
"page": 1,
"total_pages": 5,
"total_count": 50,
"per_page": 10,
"image_url_expiry_hours": 24
},
"transactions": [
{
"batch_id": "20260610-01",
"image_id": "33295785",
"internal_id": "9287",
"image": "base64_encoded_image_data...",
"image_url": "https://api.goodfundsgateway.com/secure_image/view/33295785/1138/1769654400/abc123...",
"image_expires": "2026-06-11T15:30:00+00:00",
"check_date": "2026-06-08",
"check_number": "1491",
"bank_name": "Wells Fargo",
"payee": "Example Payee",
"payor": "Example Payor",
"amount": "500.00"
}
]
}
- Base64 Image (image field) - Use for immediate display, no additional API call needed
- Secure URL (image_url field) - Use for on-demand retrieval, valid for 24 hours
- Recommended approach - Use base64 for immediate display, then transition to URL-based retrieval for subsequent views
Webhook Authentication (Dual-Token Support)
We support dual-token authentication for zero-downtime token rotation. During rotation, both old and new tokens are accepted.
Client Receiver Implementation with Dual-Token Support
<?php
function verify_webhook_signature($payload, $signature, $client_id) {
// Get both primary and secondary tokens from your storage
$primary_token = get_webhook_token($client_id);
$secondary_token = get_webhook_token_secondary($client_id);
// Check primary token
$expected = hash_hmac('sha256', $payload, $primary_token);
if (hash_equals($expected, $signature)) {
return true;
}
// During rotation, check secondary token
if ($secondary_token) {
$expected_secondary = hash_hmac('sha256', $payload, $secondary_token);
if (hash_equals($expected_secondary, $signature)) {
log_message('info', "Secondary token used for client {$client_id}");
return true;
}
}
return false;
}
?>
Meta Fields
| Field | Type | Description |
|---|---|---|
batch_id |
string | Unique batch identifier in format YYYYMMDD-XX (auto-increments daily) |
page |
integer | Current page number (1-indexed) |
total_pages |
integer | Total number of pages available |
total_count |
integer | Total number of transactions across all pages |
per_page |
integer | Number of transactions per page (configurable, default: 10) |
image_url_expiry_hours |
integer | Number of hours until the image URL expires (default: 24) |
Transaction Fields
| Field | Type | Description |
|---|---|---|
batch_id |
string | Batch identifier for this transaction (same as meta.batch_id) |
image_id |
string | Unique identifier for the check image |
internal_id |
string | The store ID from your RDC gateway account. Use this field to route the transaction to the correct store/location in your system. |
image_url |
string | Secure, signed URL to download the check image (expires after 24 hours) |
image_expires |
string | ISO 8601 timestamp when the image URL expires |
check_date |
date | Date written on the check (YYYY-MM-DD format) |
check_number |
string | Check number as printed on the check |
bank_name |
string | Name of the financial institution |
payee |
string | Payee name (who the check is written to) |
payor |
string | Payor name (who wrote the check) |
payor_street_address |
string | Payor's street address (including apartment/unit if present) |
payor_city |
string | Payor's city |
payor_state |
string | Payor's state (2-letter code) |
payor_zip |
string | Payor's ZIP code (first 5 digits only, +4 suffix removed) |
payor_country |
string | Payor's country (defaults to "US") |
routing |
string | Bank routing number (9 digits) |
account |
string | Bank account number (masked, showing only last 4 digits) |
amount |
string | Check amount (formatted as decimal with 2 decimal places) |
memo |
string | Memo line text from the check |
confidence_score |
string | Azure Document Intelligence confidence score (0-100). Higher values indicate better extraction accuracy. |
deposit_date |
string | Date the check was deposited/processed (MM-DD-YYYY format) |
- This is the store_id configured in your RDC gateway account
- Each RDC gateway account you have will have a unique internal_id
- Use this field to identify which store/location the check belongs to
Secure Image Access
Check images are delivered via secure, signed URLs that provide:
- Time-limited access - URLs expire after 24 hours
- Authenticated requests - Each URL contains a cryptographic signature
- Client-specific access - Images can only be accessed by the owning client
The image_url field contains a complete URL. Simply make a GET request to this URL to download the check image. The URL includes an authentication signature and expiration timestamp.
// PHP Example
$image_data = file_get_contents($transaction['image_url']);
file_put_contents('check_image.tiff', $image_data);
- Image URLs expire after 24 hours. Always download and store images promptly.
- Do not share or expose image URLs publicly.
- If an image URL expires, contact support to request a new one.
Image Handling
Beyond the check image itself, each transaction includes text fields (payee, payor, memo) extracted from the handwritten check via OCR. A few things worth knowing about how that text is produced:
Handwritten checks made out to or from Jewish organizations and individuals often include Hebrew/Yiddish-derived words and names (e.g. "Congregation," "Yahrzeit," "Rosenberg") that a general-purpose handwriting OCR model can misread, since these terms fall outside its everyday-English language model.
To reduce this, payee, payor, and memo are automatically passed through a dictionary-based correction step after OCR extraction and before the record is saved — so the corrected text is what you receive in the webhook. This runs as an in-memory dictionary lookup with no external calls, adding a negligible amount of time (well under a millisecond per field) to processing. It does not delay batch or webhook delivery.
This correction is conservative by design: it only fires on high-confidence dictionary matches or very close spelling variants, specifically to avoid altering names or text that aren't in its dictionary. If you ever notice a payee/payor/memo value that looks incorrectly "corrected," let support know — the dictionary is tunable on our end.
Batch Processing
Webhooks are sent in batches to improve efficiency. Each batch contains up to per_page transactions (configurable, default 10). Process each page sequentially using the pagination metadata:
// Process current page
$transactions = $payload['transactions'];
foreach ($transactions as $transaction) {
// Download the image using the secure URL
$image_data = file_get_contents($transaction['image_url']);
// Process each transaction
}
// Check if there are more pages
if ($payload['meta']['page'] < $payload['meta']['total_pages']) {
// Wait for next webhook (will be sent automatically)
}
Responding to Webhooks
Your endpoint should respond with a 2xx HTTP status code (e.g., 200 OK) to acknowledge successful receipt. Any other status code (4xx or 5xx) will be considered a failure and trigger a retry.
We recommend responding immediately with a 200 status before processing the data asynchronously to avoid timeouts:
// Accept immediately
http_response_code(200);
echo json_encode(['success' => true]);
// Then process in background (e.g., queue job, background process)
// ... your processing logic ...
Retry Logic
GoodFunds Gateway implements a robust retry system to ensure reliable delivery:
| Attempt | Delay |
|---|---|
| First failure | Immediate retry after 1 minute |
| Second failure | Retry after 5 minutes |
| Third failure | Retry after 15 minutes |
PHP Example
<?php
// Get the raw request body
$payload = file_get_contents('php://input');
// Get headers
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$client_id = $_SERVER['HTTP_X_CLIENT_ID'] ?? '';
// 1. Check timestamp (prevent replay attacks - 5 minute window)
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(401);
die(json_encode(['error' => 'Invalid timestamp']));
}
// 2. Get your webhook token from secure storage
$token = 'your-webhook-token-here';
// 3. Generate expected signature
$expected = hash_hmac('sha256', $payload, $token);
// 4. Compare signatures securely
if (!hash_equals($expected, $signature)) {
http_response_code(401);
die(json_encode(['error' => 'Invalid signature']));
}
// 5. Parse the batched payload
$data = json_decode($payload, true);
$meta = $data['meta'];
$transactions = $data['transactions'];
foreach ($transactions as $transaction) {
// Download the image using the secure URL
$image_data = file_get_contents($transaction['image_url']);
$filename = "/path/to/images/{$transaction['image_id']}.tiff";
file_put_contents($filename, $image_data);
// Process the check data
// ... your business logic ...
}
// 6. Acknowledge receipt
http_response_code(200);
echo json_encode(['success' => true]);
?>
Node.js Example
const crypto = require('crypto');
const fs = require('fs');
const https = require('https');
const express = require('express');
const app = express();
// Middleware to get raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
// Function to download image from URL
function downloadImage(url, filepath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(filepath);
https.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', reject);
});
}
app.post('/webhook-endpoint', async (req, res) => {
const payload = req.rawBody;
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const clientId = req.headers['x-client-id'];
// 1. Check timestamp (prevent replay attacks - 5 minute window)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Invalid timestamp' });
}
// 2. Get your webhook token from environment variable
const token = process.env.WEBHOOK_TOKEN;
// 3. Generate expected signature
const expected = crypto
.createHmac('sha256', token)
.update(payload)
.digest('hex');
// 4. Compare signatures
if (expected !== signature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 5. Process the batch
const data = req.body;
const meta = data.meta;
const transactions = data.transactions;
for (const transaction of transactions) {
// Download the image using the secure URL
const filename = `/path/to/images/${transaction.image_id}.tiff`;
await downloadImage(transaction.image_url, filename);
// Process the check data
// ... your business logic ...
}
// 6. Acknowledge receipt
res.status(200).json({
success: true,
processed: transactions.length,
batch_id: meta.batch_id
});
});
app.listen(3000, () => {
console.log('Webhook receiver listening on port 3000');
});
Python (Flask) Example
import hmac
import hashlib
import base64
import time
import os
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# Get webhook token from environment variable
WEBHOOK_TOKEN = os.environ.get('WEBHOOK_TOKEN', '')
@app.route('/webhook-endpoint', methods=['POST'])
def webhook_receiver():
# Get request data
payload = request.get_data(as_text=True)
signature = request.headers.get('X-Webhook-Signature', '')
timestamp = request.headers.get('X-Webhook-Timestamp', '')
client_id = request.headers.get('X-Client-ID', '')
# 1. Check timestamp (prevent replay attacks - 5 minute window)
if abs(time.time() - int(timestamp)) > 300:
return jsonify({'error': 'Invalid timestamp'}), 401
# 2. Generate expected signature
expected = hmac.new(
WEBHOOK_TOKEN.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 3. Compare signatures
if not hmac.compare_digest(expected, signature):
return jsonify({'error': 'Invalid signature'}), 401
# 4. Process the batch
data = request.json
meta = data['meta']
transactions = data['transactions']
for transaction in transactions:
# Download the image using the secure URL
response = requests.get(transaction['image_url'])
filename = f"/path/to/images/{transaction['image_id']}.tiff"
with open(filename, 'wb') as f:
f.write(response.content)
# Process the check data
# ... your business logic ...
# 5. Acknowledge receipt
return jsonify({
'success': True,
'processed': len(transactions),
'batch_id': meta['batch_id']
}), 200
if __name__ == '__main__':
app.run(port=3000)
Ruby (Sinatra) Example
require 'sinatra'
require 'json'
require 'openssl'
require 'base64'
require 'net/http'
# Get webhook token from environment variable
WEBHOOK_TOKEN = ENV['WEBHOOK_TOKEN']
post '/webhook-endpoint' do
# Get raw payload and headers
payload = request.body.read
signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
timestamp = request.env['HTTP_X_WEBHOOK_TIMESTAMP']
client_id = request.env['HTTP_X_CLIENT_ID']
# 1. Check timestamp (prevent replay attacks - 5 minute window)
current_time = Time.now.to_i
if (current_time - timestamp.to_i).abs > 300
halt 401, { error: 'Invalid timestamp' }.to_json
end
# 2. Generate expected signature
expected = OpenSSL::HMAC.hexdigest('sha256', WEBHOOK_TOKEN, payload)
# 3. Compare signatures
unless expected == signature
halt 401, { error: 'Invalid signature' }.to_json
end
# 4. Process the batch
data = JSON.parse(payload)
meta = data['meta']
transactions = data['transactions']
transactions.each do |transaction|
# Download the image using the secure URL
uri = URI(transaction['image_url'])
image_data = Net::HTTP.get(uri)
filename = "/path/to/images/#{transaction['image_id']}.tiff"
File.write(filename, image_data)
# Process the check data
# ... your business logic ...
end
# 5. Acknowledge receipt
content_type :json
status 200
{
success: true,
processed: transactions.length,
batch_id: meta['batch_id']
}.to_json
end
Testing Your Webhook
Use our test page to validate your webhook endpoint:
The test page allows you to:
- Configure your webhook endpoint URL and token
- Select a sample transaction type or provide custom JSON
- Set the batch size (number of transactions per webhook)
- Send batched test webhooks to your endpoint
- View the response and verify signature validation
Troubleshooting Common Issues
| Issue | Solution |
|---|---|
| Signature verification fails | Ensure you're using the raw request body (not pretty-printed JSON). Check that your token is correct. |
| Webhook not receiving requests | Verify your endpoint is publicly accessible and firewall isn't blocking requests. |
| Image URL returns 401 | The URL has expired (24 hour window). Contact support for a new URL if needed. |
| Image URL returns 403 | Your client ID doesn't match the image owner. Verify you're using the correct client ID. |
Frequently Asked Questions
How long are image URLs valid?
Image URLs are valid for 24 hours from the time the webhook is sent. We recommend downloading and storing images immediately upon receipt.
Can I request a longer expiration time?
The default expiration is 24 hours. If you need longer access, please contact support to discuss your requirements.
What image format is used?
Images are typically in TIFF format. Your image processing library should support TIFF files.
How are account numbers masked?
Account numbers show only the last 4 digits (e.g., "******1234"). The full number is stored securely but never exposed in webhooks.
Can I change the batch size?
Yes, batch size is configurable per client. Contact support to adjust the default of 10 transactions per batch.
Support
Need help with webhook integration? Contact our support team:
- Email: support@goodfundsgateway.com
- Documentation: Main Documentation
- Changelog: RDC Webhooks Changelog