Direct Debits
Initiate automated bank debits to collect premiums directly from an insured's bank account. Supports individual debits and batch collections across multiple policies.
Initiate a Direct Debit #
POST /api/v1/ach/debits
| Field | Type | Required | Description |
|---|---|---|---|
employer_id |
string | Yes | Employer ObjectId |
carrier_id |
string | Yes | Carrier ObjectId |
policy_id |
string | Yes | Policy ObjectId |
bank_account_id |
string | Yes | Bank account ID to debit from |
amount_cents |
integer | Yes | Amount in cents |
payment_purpose |
string | Yes | See payment purposes below |
description |
string | No | Human-readable description |
idempotency_key |
string | No | Unique key to prevent duplicate charges |
effective_date |
string | No | Desired effective date (ISO 8601) |
metadata |
object | No | Custom key-value pairs |
Payment Purposes #
| Purpose | Description |
|---|---|
paygo |
Pay-as-you-go premium collection |
down_payment |
Initial policy down payment |
installment |
Scheduled installment payment |
late_payment |
Late payment collection |
extra_payment |
Extra/overpayment |
overdue_payment |
Overdue amount |
curl -X POST /api/v1/ach/debits \
-H "X-Client-ID: $CLIENT_ID" \
-H "X-Client-Secret: $CLIENT_SECRET" \
-d '{
"employer_id": "681xyz789abc123456789012",
"carrier_id": "680abc456def789012345678",
"policy_id": "682def789ghi012345678901",
"bank_account_id": "684abc123def456789012345",
"amount_cents": 85000,
"payment_purpose": "paygo",
"description": "PayGo premium - March 2026",
"idempotency_key": "paygo_march_2026_acme"
}'
✓ Response (201 Created)
{
"ok": true,
"data": {
"success": true,
"transaction_id": "685def456ghi789012345678",
"status": "PENDING",
"amount": 85000
}
}
Idempotency
: Always provide an idempotency_key to prevent duplicate charges. If you retry a request with the same key, the original transaction is returned instead of creating a new one.
Check Debit Status #
GET /api/v1/ach/debits/{transaction_id}/status
Transaction Statuses #
Audit1's ACH ledger (Ach_activity.status) carries two parallel vocabularies depending on the row's origin. New debits initiated through this API land in the premium_calculation vocabulary. Rows you may retrieve via reporting endpoints can also be in the legacy vocabulary (sourced from the legacy Audit1/Cennairus pipelines and the ACH Processing Company portal).
Group your filtering by what the money is doing, not by the literal status string.
| Group | Statuses (premium_calculation) | Statuses (legacy) | Meaning |
|---|---|---|---|
| In flight | pending, Scheduled |
pending, at_sb, file_transmitted |
Created or queued; on its way to the bank |
| Money landed | submitted, processed (and a forward-compat settled) |
sb_remit, at_sb, file_transmitted, legacy_collected |
Funds have reached the bank or service bureau |
| Failed | cancelled, failed, quarantined |
returned, failed, canceled (one l) |
Money did not move; see failure_reason |
Info
The legacy sb_remit status alone covers 94K+ rows in production — filtering by only the new vocabulary will hide ~99% of historical money. Always group by the table above rather than matching individual strings.
After the bank acknowledges the file, the transaction's status flips to submitted or sb_remit (depending on source). Returns later flip it to returned/failed via the return-handler service.
Server-Side Gates #
Audit1 enforces four server-side gates before a debit reaches the bank. A debit that fails a gate is never submitted; instead it lands in one of the failure-status groups above with a machine-readable reason. None of these gates are caller-controllable — they are platform-level invariants.
1. Billing Eligibility Gate #
The policy's billing mode must allow ACH auto-debit at the moment Phase 3 runs.
reason |
When it fires |
|---|---|
non_paygo_blocked |
The policy's billing_type is set and is not PAY_AS_YOU_GO (e.g., the carrier flipped it to DIRECT_BILLING, AUDIT, or TRADITIONAL). The carrier bills the employer directly — Audit1 must never auto-debit. |
policy_inactive_blocked |
The policy is cancelled (hard block) or expired with a file check_date on/after expiration. Files dated before expiration are still collectible. |
This is the last line of defense: it runs at Phase 3 time, so it catches policies that flipped state after the payroll file was ingested but before the debit was created.
2. Legacy Dedup Gate #
Phase 3 checks the legacy Azure SQL Payment table before flipping any debit to submitted. If the same (employer, period, premium) was already collected by the legacy ACH service, the new debit is suppressed with reason = "legacy_collected". This prevents the new pipeline and the legacy pipeline from both pulling the same money during the cutover transition.
3. Forward-Only Cutoff #
Debits whose ingestion timestamp falls before ACH_FORWARD_ONLY_CUTOFF (default 2026-04-27) are treated as belonging to the legacy pipeline and are not collected by the new ACH gateway. Historical files re-ingested after the cutoff are also blocked under this rule. The cutoff is platform-wide; override is operational only (not exposed to API callers).
4. Anomaly Rail #
Each batch run checks every entry against the employer's recent ACH amount average. If a single entry's amount_cents is more than 100% above the recent average for that employer, the entry is quarantined with reason = "amount_anomaly_above_100pct_of_recent_average". Operations resolves the quarantine manually; the caller sees the transaction stay in quarantined until that happens.
None of these gates produce a 4xx response on the original POST.
They run asynchronously inside the batch worker. To detect a gated debit, poll GET /ach/debits/{transaction_id}/status and inspect both the status group and the structured reason field.
Batch Collection #
Collect from a single employer across multiple policies in one batch.
POST /api/v1/ach/collections
| Field | Type | Required | Description |
|---|---|---|---|
employerId |
string | Yes | Employer to debit |
bankAccountId |
string | Yes | Bank account ID |
policyCollections |
array | Yes | One entry per policy |
policyCollections[].policyId |
string | Yes | Policy ObjectId |
policyCollections[].totalInDollars |
number | Yes | Amount in dollars |
policyCollections[].description |
string | Yes | Line-item description |
policyCollections[].idempotencyKey |
string | Yes | Unique per collection |
curl -X POST /api/v1/ach/collections \
-H "X-Client-ID: $CLIENT_ID" \
-H "X-Client-Secret: $CLIENT_SECRET" \
-d '{
"employerId": "681xyz789abc123456789012",
"bankAccountId": "684abc123def456789012345",
"policyCollections": [
{
"policyId": "682def789ghi012345678901",
"totalInDollars": 1500.00,
"description": "WC Premium - Q1 2026",
"idempotencyKey": "wc_q1_2026_acme"
},
{
"policyId": "682ghi012jkl345678901234",
"totalInDollars": 850.00,
"description": "GL Premium - Q1 2026",
"idempotencyKey": "gl_q1_2026_acme"
}
]
}'
✓ Response (202 Accepted) — batch processing is asynchronous.
List Transactions #
GET /api/v1/ach/transactions?employer_id=...&status=COMPLETED&from_date=2026-01-01&page=1&limit=50
| Parameter | Type | Description |
|---|---|---|
carrier_id |
string | Filter by carrier |
employer_id |
string | Filter by employer |
policy_id |
string | Filter by policy |
status |
string | PENDING, COMPLETED, RETURNED, etc. |
from_date |
string | Start date (ISO 8601) |
to_date |
string | End date |
sort_by |
string | Field to sort by |
sort_order |
string | asc or desc |
Transaction History #
GET /api/v1/ach/transactions/{id}/history
Returns the audit trail for a specific transaction.
Transaction Fees #
GET /api/v1/ach/transactions/{id}/fees
Returns fee breakdown for a transaction.
Bank debit settlement typically takes 2-5 business days.
The COMPLETED status means funds have been received. Use webhooks to get notified in real time.