Receiving the Deduction File
This is the end-to-end guide for the Deduction file notification — the webhook you use to be told, in real time, that a new deduction file is ready and where to download it.
What is the Deduction file?
After you send us your Census (employee roster) and Receipt (payment records), BMG Money reconciles the loan repayment schedule for your employees and produces a Deduction file: the list of amounts to be deducted in your next payroll run.
When that file is ready, BMG Money pushes a notification to your webhook endpoint containing a time-limited download URL. You never have to poll — subscribe once and you are notified for every deduction file generated for your company.
The Deduction file is produced by BMG Money's payroll reconciliation process. Your integration does not upload or trigger it directly — you send Census/Receipt data, and the Deduction file is generated and announced to you via this webhook.
The complete flow
Step 1 — Subscribe to the file event
Register a subscription so we know where to notify you. Use the event name file.
POST https://api.bmgmoney.com/law/api/v1/subscriptions/
Authorization: Bearer <access_token>
Content-Type: application/json
{
"targetUrl": "https://your-system.example.com/webhooks/bmg",
"secret": "a-long-random-string-min-8-chars",
"events": ["file"],
"description": "BMG file notifications"
}
Choose your granularity
Subscribing to the parent topic
filecovers the entire file lifecycle — validation, processing errors and deduction-ready — and you tell events apart by reading theeventfield in each delivered payload (see Step 3).If you only care about the deduction file, subscribe to
file.deduction.readyinstead and you will receive exclusively that event. Parent and child subscriptions can coexist (on differenttargetUrls); when both match the same event, each URL still receives a single delivery.
The secret you provide here is used to sign every delivery (see Step 4). Store it securely — you need it to validate incoming webhooks.
Step 2 — Send Census and Receipt
Provide the data BMG needs to reconcile deductions. You can use either import method:
- Direct JSON —
POST /law/api/v1/censusandPOST /law/api/v1/receipt(see Import via JSON). - File handshake — pre-signed upload for large CSVs (see Import via URL).
Once reconciliation completes on our side, the deduction webhook fires automatically.
Step 3 — Receive the notification
We send an HTTP POST to your targetUrl. The body has exactly two top-level fields: event and data.
{
"event": "file.deduction.ready",
"data": {
"fileName": "deduction_2026_05.csv",
"message": "The deduction file is ready for download.",
"url": "https://storage.googleapis.com/.../deduction_2026_05.csv?X-Goog-Signature=..."
}
}
data fields
| Field | Type | Description |
|---|---|---|
fileName |
string | Name of the deduction file. |
message |
string | Human-readable status message. |
url |
string | Pre-signed download URL for the file (see Step 5). Valid for 15 minutes. |
errors |
array | Present only on validation-failure events; a list of { propertyName, errorMessage }. Omitted here. |
Events you may receive on the file subscription
Because you subscribe to the whole file topic, inspect the event field and act accordingly:
event value |
Meaning | Typical action |
|---|---|---|
file.deduction.ready |
A deduction file was generated and is ready. | Download data.url and apply the deductions. |
file.validation.success |
An uploaded Census/Receipt passed validation and is being processed. | Informational. |
file.validation.failed |
An uploaded file failed validation. | Inspect data.errors and re-submit. |
file.processing.error |
An unexpected error occurred while processing a file. | Retry / contact support with the fileName. |
Respond with a
2xxstatus quickly (within the 5s timeout). Failed or slow deliveries are retried up to 5 times. Do your heavy processing (like downloading the file) asynchronously after acknowledging.
Step 4 — Verify the signature
Every signed delivery includes these headers:
| Header | Description |
|---|---|
X-Signature |
The HMAC of the raw request body, as lowercase hexadecimal. |
X-Signature-Algorithm |
Identifies the signing algorithm. Deliveries are signed with HMAC-SHA256. |
To validate, compute HMAC-SHA256 over the exact raw body bytes you received (do not re-serialize the JSON) using your subscription secret as the key, hex-encode it in lowercase, and compare — using a constant-time comparison — against X-Signature.
X-Signature == hex_lower( HMAC_SHA256( secret, raw_request_body ) )
C#
using System.Security.Cryptography;
using System.Text;
bool IsValid(string rawBody, string signatureHeader, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader));
}
Node.js
const crypto = require('crypto');
function isValid(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader));
}
Capture the raw body before any JSON middleware reparses it — re-serialized JSON can differ byte-for-byte and break the signature. In Express, use
express.raw()(orverifyto stashreq.rawBody); in ASP.NET Core, read the request body stream directly.
Step 5 — Download the deduction file
data.url is a Google Cloud Storage V4 pre-signed URL for an HTTP GET, valid for 15 minutes. Download it with a plain GET — do not add an Authorization header (the signature is embedded in the URL).
curl -o deduction.csv "<data.url>"
If the URL has expired (HTTP 403/400 from storage), you must wait for the next notification — a new deduction file always arrives with a fresh URL. Download promptly on receipt.
Production
| Base URL | https://api.bmgmoney.com |
| Auth | OAuth 2.0 client credentials (see Authentication). The gateway derives your company context from the token — you do not send an employer id header. |
| Subscriptions | POST https://api.bmgmoney.com/law/api/v1/subscriptions/ |
In production the deduction webhook fires automatically as part of your normal payroll cycle, once Census/Receipt are processed and reconciliation completes.
Testing in Sandbox
The Sandbox lets you validate your webhook receiver end to end — subscription, signature verification, and download — without touching production data.
| Base URL | https://sandbox.bmgmoney.com |
| Auth | Same OAuth client-credentials flow, against https://sandbox.bmgmoney.com/oauth/v1/access-token. |
| Prerequisite | Your outbound IP must be whitelisted (Sandbox is IP-restricted) and your company must be onboarded. Contact your Partner Success contact. |
1. Get a token
curl --request POST \
--url 'https://sandbox.bmgmoney.com/oauth/v1/access-token' \
--header "Authorization: Basic $(printf '%s' 'CLIENT_ID:CLIENT_SECRET' | base64)" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials'
2. Point your subscription at a test endpoint
For quick inspection you can use a throwaway endpoint such as webhook.site — copy your unique URL and use it as targetUrl. It shows you the exact body and headers we send, so you can confirm your signature logic against real deliveries.
curl --request POST \
--url 'https://sandbox.bmgmoney.com/law/api/v1/subscriptions/' \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{
"targetUrl": "https://webhook.site/your-unique-id",
"secret": "sandbox-test-secret",
"events": ["file"],
"description": "Sandbox deduction test"
}'
Verify it was registered:
curl --url 'https://sandbox.bmgmoney.com/law/api/v1/subscriptions/' \
--header 'Authorization: Bearer <access_token>'
3. Trigger a test event yourself
In Sandbox you can fire a webhook on demand — no need to wait for a real reconciliation — using the test trigger endpoint. You upload a CSV and choose the event; the platform stores the file, generates a download URL, and delivers the event to your subscriptions exactly as production would.
curl --request POST \
--url 'https://sandbox.bmgmoney.com/law/api/v1/webhook-trigger' \
--header 'Authorization: Bearer <access_token>' \
--form 'file=@./sample_deduction.csv' \
--form 'event=file.deduction.ready' \
--form 'message=Sample deduction for integration testing'
Form fields
| Field | Required | Description |
|---|---|---|
file |
Yes | The CSV to deliver as a download (max 10 MB). It becomes the data.url in the event. |
event |
No | Event to publish. Defaults to file.deduction.ready. Validated by its root segment (the parent before the first dot — e.g. the root of file.publisher is file), which must be a registered topic — see Events. An unknown root returns 400. |
message |
No | Text included in the event's data.message. |
The response (202 Accepted) echoes what was published:
{
"event": "file.deduction.ready",
"fileName": "sample_deduction.csv",
"downloadUrl": "https://storage.googleapis.com/.../sample_deduction.csv?X-Goog-Signature=..."
}
Moments later your targetUrl receives the delivery — same body and signing as production.
Test-only. This endpoint exists solely for integration testing and is disabled in production (it returns
403). In production, deduction events fire automatically from BMG's payroll reconciliation.
4. Confirm the delivery
- On webhook.site: you will see the
POST, the{"event":"file.deduction.ready","data":{...}}body, and theX-Signature/X-Signature-Algorithmheaders. - On your own endpoint: run your signature validation (Step 4) against the raw body and download
data.url(Step 5).
Once your receiver validates the signature and downloads the file in Sandbox, it will behave identically in Production.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Subscription created but no deliveries ever arrive | You subscribed to a child event and are waiting for a different one (children don't receive parent or sibling events) — subscribe to the parent "file" to receive the whole lifecycle. |
| Deliveries arrive but signature never matches | Body was re-serialized before verifying. Sign the raw received bytes. |
Download URL returns 403 |
The pre-signed URL expired (15-min window). Download immediately on receipt. |
| No delivery in Sandbox after subscribing | No event has been fired yet — trigger one with the test endpoint; or your IP is not whitelisted. |
Test trigger returns 403 |
You called it in production — it is Sandbox/test-only by design. |
Test trigger returns 400 "Invalid event" |
The event's root is not a registered topic — use an event under a valid topic (e.g. any file.*, see Events). |
Subscription creation returns 409 |
You already have an active subscription with that targetUrl — update (PUT) or delete the existing one instead of creating another. |
| Deliveries stop after repeated failures | Your endpoint returned non-2xx or timed out (>5s) on all 5 retries. Check the Deliveries log. |