API v1

The OneGate Recon API

Every gateway, batch and fee OneGate Recon checks against your bank statement and your own records, readable by your treasury system, ERP or data warehouse. Expected receipts for your cash forecast, what arrived and what paid it, what needs chasing, and the fees for your ledger.

Your first call

  1. An owner of your organization makes a key under Settings › Developers, chooses what it may read, and copies it. It is shown once.
  2. Send it as a bearer token. /me says which organization the key reads and what it may do:
curl https://onegaterecon.com/api/v1/me \
  -H "Authorization: Bearer $ONEGATE_RECON_KEY"
{
  "organization": { "id": "5f0c2a9e-…", "name": "Example Trading (Pty) Ltd" },
  "key": { "prefix": "ogr_live_a1B2c3", "scopes": ["reconciliations:read", "exceptions:read", "cash:read"] },
  "api_version": "v1"
}

Then list your reconciliations, newest first:

curl "https://onegaterecon.com/api/v1/reconciliations?type=gateway&limit=3" -H "Authorization: Bearer $ONEGATE_RECON_KEY"

The sandbox

Build against made-up data first. A test key (ogr_test_…, made the same way under Settings › Developers) reads the sandbox: one organization with one gateway, one month, and every kind of answer the API gives, settlements that arrived, one short, one overdue, one still in transit; payments on one side only; bank credits nothing explains. It holds nobody's real figures and reads the same for everyone, so a test you write today gives the same answer next month.

  • Every endpoint works the same with a test key. /me says "mode": "test", and every answer carries the header OneGate-Mode: test, so a sandbox figure is never mistaken for a live one.
  • The sandbox month is September 2026: GET /cash?from=2026-09-01&to=2026-10-05 shows money due and received, with one batch overdue and one in transit.
  • When your integration is ready, make a live key with the scopes it needs and change only the key.

Feeding a treasury system

Most treasury systems want three things from reconciliation: what money is coming and when, what arrived and what it was for, and what to chase. A nightly job with three calls covers all of them.

In your treasury systemCallScope
Expected receipts for the cash forecast (the next two weeks)GET /cash?from=2026-09-01&to=2026-09-14cash:read
Actual receipts, with the bank line that paid each settlementGET /reconciliations/{id}/settlements?status=arrivedsettlements:read
What to chase: overdue and short settlements, payments on one side onlyGET /exceptions?status=openexceptions:read
Gateway fees for the general ledgerGET /reconciliations/{id}/feesfees:read
The month report for the auditorsGET /reconciliations/{id}/report?format=pdfreports:read

/cash gives, for every day, the settlements due that day and the money that reached the bank, and lists what has not arrived yet with its due day, in transit while it is not due, overdue once it is. Amounts are decimal strings in the organization's currency, so they can be posted as they are.

curl "https://onegaterecon.com/api/v1/cash?from=2026-09-01&to=2026-09-14" -H "Authorization: Bearer $ONEGATE_RECON_KEY"

{
  "from": "2026-09-01", "to": "2026-09-14", "currency": "ZAR",
  "days": [ { "date": "2026-09-01", "due": "182340.15", "received": "179880.15" }, … ],
  "outstanding": [
    { "gateway": { "id": "…", "name": "Ozow" }, "method": { "id": "…", "name": "Instant EFT" },
      "label": "Batch 8841", "status": "in_transit", "due_by": "2026-09-03",
      "expected": "24811.40", "received": null, "currency": "ZAR", … }
  ]
}

Every kind of reconciliation

One API covers every kind. A reconciliation has the same summary whatever its kind, and its id says which it is. What only one kind has is under details. Exceptions have one shape everywhere.

KindIdWhat it reconcilesExceptions
gatewaygw_…Each gateway's settlements against your bank statement, and your own records against the gateway's filesYes
servicesr_…An aggregator's collections and settlements, per merchantNo
matchingmt_…Two or three files against each other, on the columns you chooseYes
merchantmr_…A merchant's figures, fees and adjustmentsNo

Keys and scopes

  • A key reads one organization. It is shown once; we keep only a hash of it, so a lost key is revoked and replaced, never recovered.
  • Give each system its own key with only the scopes it needs. A key may expire, and may be limited to your servers' IP addresses.
  • Every call is written to your organization's change log, with the key that made it.
  • Keep keys on your servers. Never put one in a web page or a mobile app.
ScopeReads
reconciliations:readReconciliations of every kind: their period, status and totals
exceptions:readWhat needs a look: overdue and short settlements, payments on one side only, credits nobody explains
cash:readMoney due and received, day by day: the cash-forecast feed
settlements:readEach batch, day or week a gateway owed, and the bank line that paid it
fees:readCommission taken and invoiced, per gateway and month
reports:readThe month report, as an Excel workbook or a PDF
setup:readGateways and sources, to label things the way the organization does
files:writeSend files of your sources in, and bank statements as camt.053 or MT940
checks:writeStart a reconciliation and run its check
decisions:writeAccept, expect next month, or note an exception

Sending data in

Your systems can also put data in, so nothing is uploaded by hand. Each call does exactly what the screen does: the file is recognised, read, filed in the month its days fall in, and the month is checked again by itself. Send an Idempotency-Key header with every write (any unique string): the same request sent twice within a day gets the first answer again and is never done twice.

WhatCallScope
A gateway's file, your records export, a commission invoicePOST /filesfiles:write
Your bank statement as camt.053 (ISO 20022) or MT940, straight from the bank or your treasury systemPOST /bank-statementsfiles:write
Start the monthPOST /reconciliationschecks:write
Check it now (up to a minute on a full month)POST /reconciliations/{id}/checkchecks:write
Accept, expect next month or note an exceptionPOST /exceptions/{id}/decisionsdecisions:write
curl -X POST "https://onegaterecon.com/api/v1/bank-statements" \
  -H "Authorization: Bearer $ONEGATE_RECON_KEY" \
  -H "Idempotency-Key: stmt-2026-09-30-62000000001" \
  -F "file=@camt053-2026-09-30.xml"

The answer says what was read (rows_stored, first_day, last_day), which month it was filed in, and "check": "queued" when the month is being checked again, the check.completed webhook tells you when that is done. A statement or file sent twice answers already_added: true and changes nothing.

Webhooks

Instead of asking all day, be told. Under Settings › Developers add an https address your system listens at and choose the events; you get a signing secret, shown once. Each event is posted as JSON, at once, and tried again with a growing wait for a day if your system does not answer 2xx. After a day of nothing but failures the endpoint is switched off and you are told; fix it and switch it on again. Every delivery can be read and sent again from the same screen.

EventWhen
check.completedA reconciliation was checked: its totals and what needs a look
settlement.overdueA settlement did not reach the bank by its due day
settlement.shortA settlement arrived, but short of what was expected
exception.resolvedSomeone accepted, linked or otherwise resolved an exception
reconciliation.closedA month was closed: its figures are final
reconciliation.reopenedA closed month was reopened, with the reason
file.processedA file was added to a reconciliation and read

A post looks like this. id is the event's: the same event sent twice (a retry) has the same id, so your handler can ignore a repeat.

POST /your/endpoint HTTP/1.1
Content-Type: application/json
OneGate-Event: settlement.overdue
OneGate-Delivery: 6e1f…
OneGate-Mode: live
OneGate-Signature: t=1760981400,v1=5257a869e7…

{ "id": "evt_01c2b4e6a8d0f2a4c6e8b0d2",
  "type": "settlement.overdue", "created_at": "2026-10-20T18:10:00.000Z", "mode": "live",
  "data": { "reconciliation_id": "gw_…", "gateway": { "id": "…", "name": "Ozow" }, "method": { "id": "…", "name": "Instant EFT" },
            "label": "Batch 8841", "due_by": "2026-09-24", "expected": "21500.00", "received": null, "currency": "ZAR" } }

Check the signature before trusting a post: it is HMAC-SHA256 with your secret over <timestamp>.<raw body>. Refuse a timestamp more than five minutes old, and compare in constant time.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Answer 2xx quickly and do the work afterwards: a post that takes more than ten seconds counts as failed and is sent again.

Conventions

TopicHow it works
MoneyA decimal string with its currency: "amount": "10308.00", "currency": "ZAR", never a number, so nothing is rounded on the way.
DaysISO 8601: 2026-08-06. Moments carry their offset.
Lists{ data: [...], next_cursor }. Pass ?cursor= for the next page until it is null; limit is 1-200.
Unknown parametersRefused with a 400, not ignored, so a typo never silently returns everything.
VersionsThis is v1. Fields may be added at any time; nothing is removed or changes meaning within v1. Read the changelog in the reference.

Errors and limits

Errors are RFC 9457 problem details, in plain words. Every answer carries a Request-Id; RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset say where you stand.

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{ "type": "/developers#errors-missing_scope", "title": "This key may not read this",
  "status": 403, "detail": "It needs the \"cash:read\" scope.", "code": "missing_scope" }
StatusCodeWhat it means
400invalid_parameterA parameter is not valid, or is not one the endpoint knows. The detail says which.
401no_key / invalid_keyNo key was sent, or it was revoked or has expired.
403missing_scope / ip_not_allowedThe key may not read this, or not from this address.
404not_foundNo such reconciliation in this organization.
429rate_limitedMore than 600 requests a minute with this key. Wait for Retry-After seconds.
500internalSomething went wrong on our side. Quote the Request-Id header to support.

Coming next

    Tell us what your system needs first: it decides the order.