Developer

Rails

Plug an SMS gateway, a bank, or a feed into a business — provider-agnostic.

A rail is a connection a business configures once in Settings → API keys & webhooks; Trabalance never names a vendor. Every connection has one secret (rsec_…, shown once).

RailDirectionWhat it does
messagingTrabalance ↔ your gatewayOn chosen events, renders a template into a message for the customer or vendor the event concerns and POSTs it to the gateway URL. Any SMS / WhatsApp / email gateway with an HTTP API works: headers and the JSON body are templated, so no adapter is needed. It also LISTENS: post the customer's reply back to the same connection and Trabalance reads it.
payoutYour bank adapter → TrabalancePOST a signed `payout.confirmation` to the connection's inbound URL; Trabalance records the Payment against the bills from the configured cash-flow account. Idempotent on the reference.
bank_feedYour bank adapter → TrabalancePOST signed `bank_feed.lines`; lines enter the statement store for reconciliation.

Messaging

Config: url (https), method (POST), headers (values may use placeholders), events (or *), to (phone | email), template, optional body_template (a JSON string with placeholders — the shape your gateway expects).

Placeholders: {{party_name}} {{party_email}} {{party_phone}} {{to}} {{business_name}} {{event}} {{event_label}} {{document_no}} {{document_type}} {{total}} {{currency}} {{date}} {{message}} {{secret}} {{data.<field>}}

json
{
"rail": "messaging",
"name": "SMS gateway",
"config": {
  "url": "https://gateway.example/v1/messages",
  "headers": { "Authorization": "Bearer {{secret}}" },
  "events": ["invoice.created", "receipt.created"],
  "to": "phone",
  "template": "Hello {{party_name}}, {{event_label}} from {{business_name}}: {{document_no}} for {{currency}} {{total}}.",
  "body_template": "{ \"to\": \"{{to}}\", \"text\": \"{{message}}\", \"from\": \"ACME\" }"
}
}

Deliveries are logged like webhooks (status, attempts, response excerpt) and retried with the same backoff. A party with no contact for the chosen channel is logged as skipped, not failed.

Payout and bank feed (inbound)

Inbound URL: POST https://api.trabalance.com/webhook/rails/<connection id> with Trabalance-Rail-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")> — the same scheme Trabalance uses outbound, in reverse.

json
{ "kind": "payout.confirmation", "reference": "BANK-OUT-0042", "vendor_id": "<vendor id>",
"date": "2026-08-26", "allocations": [{ "document_id": "<bill id>", "amount": 1250.00 }], "currency": "USD" }

{ "kind": "bank_feed.lines", "lines": [{ "date": "2026-08-25", "description": "POS 1234", "amount": 40, "direction": "in" }] }

Responses: 201 { status: "success", data: … }; 401 invalid_signature; 400 validation_error naming fields; 404 for an unknown or inactive connection. A repeated confirmation with the same reference returns the same Payment — safe to retry.

Presets — the shape each provider expects

GET /rails/presets returns a recipe per provider: Twilio, WhatsApp Business (Meta Cloud API), Slack, Gmail API, Microsoft Graph, and inbound recipes for Stripe, Paystack and Flutterwave.

A preset is data, not code. It fills the same form you could fill by hand, names the two or three secrets to paste, and leaves the rail exactly as configurable afterwards — so a provider we have never heard of is a first-class citizen, and deleting a preset breaks nothing.

ℹ️Payment gateways are inbound

Trabalance does not take card details and does not hold funds. A gateway tells Trabalance that money moved and Trabalance records it through the ordinary producer — the only arrangement that keeps the books the source of truth. Stripe, Paystack and Flutterwave sign with their own schemes, so put a small forwarder in front that re-signs with the rail secret; the preset gives you the field mapping.

Replies (inbound messaging)

A reply is where the answer usually is — "already paid on Tuesday", "send me my statement". Post it back to the same connection's inbound URL, signed the same way:

json
{ "kind": "message.received",
"from": "+44 7700 900123",            // or an email — whatever your gateway addresses them by
"text": "I paid 1,200 yesterday",
"received_at": "2026-08-27T09:14:00Z", // optional
"message_id": "SM7f3c…" }             // your id; the idempotency key

Trabalance matches the sender against the phone or email already on a customer's record — a phone by its digit tail, so country codes, spacing and leading zeros do not matter — then reads what they want:

What they saidWhat happensResponse
They have paidA **receipt proposal** is created, allocated by settlement order, with the message itself attached as evidence. Nothing is posted — someone approves it under AI → Proposals.`{ matched: true, intent: "payment_claim", action: "proposed", proposal_id }`
They ask what they oweThe answer is composed from the open items the reports use and sent back out on the same rail as a `statement.requested` event.`{ intent: "statement_request", action: "answered", open_count, total }`
Anything elseRecorded, nothing done.`{ intent: "question" | "other", action: "none" }`
The sender is not one of your customersNothing at all — no figures, no confirmation that the number is unknown.`{ matched: false, action: "none" }`

Always 200 for a message that was read, even when the answer was to do nothing — your gateway should not retry a reply that was deliberately left alone. A bad signature is 401, a malformed envelope 400 naming the field, and the same message_id twice is not a second proposal.

ℹ️A claim is not a payment

A customer saying they paid does not move money. It becomes a proposal a person approves, and approval records the receipt through the ordinary producer — applied oldest-first, never beyond what is open. If the amount exceeds their open balance the claim is recorded and no proposal is made.

⚠️Never a status flip

A confirmation does not mark anything paid by itself. It records a Payment document through the same producer a person uses; the bill becomes Paid because the money was applied, and bill.paid fires on the crossing.