ABIL — A Better Integration Layer

Configuration pages: /v1/console — the built-in ops console (channels, rates, waterfall, bindings, period operations). All endpoints (except this page, /v1/health, the two Xero browser routes /v1/abil-xero/authorize and /callback, and the supplier webhook /v1/abil-webhooks/:channelId/delivery, which takes the channel's webhook secret) require Authorization: Bearer <IP4X token> + X-Tenant-ID, verified live against the IP4X stack. Machine-readable manifest: /v1/api-index.json.

Provider handlers

handlernamelicence keyflavourstatus
lesakaLesaka (EasyPay)abil:lesakatransactional⏳ pending
intouchInTouch (eKYC metering)abil:intouchmetered✅ implemented
google_mapsGoogle Maps Platformabil:googlemetered✅ implemented
riskscapeRiskScapeabil:riskscapemetered⏳ pending
firstdmt_acsFirstDMT (Axiros AXESS.ACS)abil:firstdmtmetered⏳ pending
gti_losGeoTerraImage Line of Sightabil:gtimetered✅ implemented
interstellioInterstellio NebularStack (RADIUS)abil:interstelliometered✅ implemented
kamstrup_readyKamstrup READy (water metering)abil:kamstrupmetered✅ implemented
meteosourceMeteosource weatherabil:weathermetered✅ implemented
imisIMIS (T-GIS) municipal system of recordabil:imismetered✅ implemented
amber_panicAmber Innovations PanicApp (armed response)abil:panictransactional✅ implemented
smsportalSMSPortal (LINK Mobility) WhatsApp & SMSabil:smsportalmetered✅ implemented
xeroXero (accounting)abil:xerometered✅ implemented

Channels

GET/v1/abil-channelsList channels (secrets masked; secrets_set booleans)
POST/v1/abil-channelsCreate channel (configuration encrypted at rest)
GET/v1/abil-channels/:idGet channel
PUT/v1/abil-channels/:idUpdate channel (omitted secrets preserved)
DELETE/v1/abil-channels/:idSoft-delete channel
GET/v1/abil-channels/available-handlersProvider catalog for this licence
GET/v1/abil-channels/handlers/:handler/schemaConfiguration schema
GET/v1/abil-channels/handlers/:handler/actionsActions a handler exposes
POST/v1/abil-channels/:id/executeExecute an action {action, params, disposition?} — records CDRs. Auth: IP4X user with abil:channels:execute, OR X-ABIL-Service-Key carrying channels:execute for this channel (disposition is always billable; customer_ref must be the key's consumer or beneath it). Idempotency-Key header replays the stored response; 429 cap_exceeded/float_exhausted; 504 outcome_unknown after a vendor timeout

Supplier webhooks (PUBLIC — the channel's webhook_secret is the credential)

POST/v1/abil-webhooks/:channelId/deliverySMSPortal WhatsApp status receipts: SMSPortal's native bare array [{MessageId, Status, Timestamp…}] or {events:[…]} / {statuses:[…]} (1–500) → ingest_delivery through the executor; billable CDRs only for receipts that correlate to this channel's own send, priced at the category that send recorded. Auth: X-ABIL-Webhook-Secret (a custom header on the SMSPortal webhook) OR Authorization: Basic base64(<any user>:<webhook_secret>) — never path, query or body. 200 {data:{received, delivered, failed, ignored, malformed, cdr_uncorrelated?}}; every refusal (no/wrong secret, unknown, deleted, inactive or non-smsportal channel) the same 401 + WWW-Authenticate: Basic; 400 validation for a malformed batch; 429 per source IP (ABIL_WEBHOOK_RATE_PER_MIN)

CDRs (metering spine)

GET/v1/abil-cdrs/metricsMetric registry
POST/v1/abil-cdrsRecord a CDR (idempotent via client_reference)
GET/v1/abil-cdrsQuery CDRs (period, metric_key, product_sku, handler, disposition…)
GET/v1/abil-cdrs/summary/:periodPer-metric aggregates for a period

Cost rates (ABIL holds cost only — the customer price lives on the IP4X Product)

GET/v1/abil-ratesCost rate cards (negative = commission earned)
POST/v1/abil-ratesCreate a cost rate (effective-dated; supersede, never edit)
POST/v1/abil-rates/:id/closeEnd-date a rate
POST/v1/abil-rates/rate-period/:periodRating pass (?reRate=true for month-end whole-volume correction)

SKU mapping (cost ↔ IP4X Product — see docs/SKU-CONTRACT.md)

GET/v1/abil-sku-map(metric_key, service_code) → product SKU
POST/v1/abil-sku-mapAdd a mapping (effective-dated; supersede, never edit)
POST/v1/abil-sku-map/:id/closeEnd-date a mapping
DELETE/v1/abil-sku-map/:idSoft-delete a mapping
POST/v1/abil-sku-map/restamp/:periodRe-resolve SKUs on a period (409 once pushed)
GET/v1/abil-sku-map/coverage/:periodMapped vs still on the metric-key fallback

Consumers & service keys

GET/v1/abil-consumersConsumer registry (a per-tenant tree)
POST/v1/abil-consumersRegister a consumer (optional parent_id = tree edge)
PUT/v1/abil-consumers/:idUpdate a consumer {name?, notes?, is_active?} — is_active: true reactivates one a blocking cap switched off
POST/v1/abil-consumers/:id/keysIssue a service key {name?, scopes?: [ingest|usage|channels:execute|accounting], allowed_channel_ids? (required with channels:execute or accounting)} — secret shown once
DELETE/v1/abil-consumers/keys/:keyIdRevoke a service key
GET/v1/abil-capsUsage / spend caps (?scope=channel|consumer|tenant&scope_id=)
GET/v1/abil-caps/usageCaps with the current window's used vs limit
POST/v1/abil-capsCreate a cap {scope, scope_id?, metric_key?, kind: quantity|cost, window: day|period, limit_value, currency?, soft_pct?, action: block|warn}
DELETE/v1/abil-caps/:idRemove a cap
GET/v1/abil-servicesService groups: one service, several providers
GET/v1/abil-services/statusMembers in cost order + the margin verdict per service (?code=)
POST/v1/abil-servicesCreate a service {code, name, product_sku, margin_floor_pct?=10}
PUT/v1/abil-services/:codeUpdate a service (name, product_sku, margin_floor_pct 0–95 as a margin on price, is_active, notes)
DELETE/v1/abil-services/:codeRemove a service
POST/v1/abil-services/:code/membersAdd a provider {abil_channel_id, action, metric_key, service_code?}
DELETE/v1/abil-services/members/:idRemove a provider
POST/v1/abil-services/:code/executeRun the service: margin floor (409 margin_breach; 503 margin_unverified), then cheapest live provider first, failover on supplier failure; 429 on the caller's own cap; 502 when all fail. Same auth, Idempotency-Key and service-key rules as channel execute; no supplier cost in the response
POST/v1/abil-ingestConsumer usage ingest — auth by X-ABIL-Service-Key; customer_ref from the key, not the body

Periods & the pull read API

GET/v1/abil-periods/:periodPeriod status (open | rated | sealed)
POST/v1/abil-periods/:period/sealSeal a period — the read API then serves it
POST/v1/abil-periods/:period/reopenAudited reopen (reason required) -> rated
GET/v1/usagePULL: sealed rated usage per consumer per SKU; auth by service key, scoped to the caller subtree

Reports (manual recon)

GET/v1/abil-reports/supplier/:periodSupplier usage/cost by metric (+ flat-fee amortisation) — ?format=csv
GET/v1/abil-reports/billing/:periodWhat the next usage push will claim, per customer per SKU — ?format=csv
GET/v1/abil-reports/tenant/:periodTenant cost by SKU / customer / metric — ?format=csv

Ledger & waterfall (all configuration)

GET/v1/abil-ledger/partiesParty registry
POST/v1/abil-ledger/partiesCreate party
GET/v1/abil-ledger/schemesSplit schemes with lines
POST/v1/abil-ledger/schemesCreate effective-dated waterfall scheme
POST/v1/abil-ledger/allocate/:periodAllocate rated CDRs to ledger entries + float draws
GET/v1/abil-ledger/report/:periodParty totals for a period
GET/v1/abil-ledger/floatsFloat balances per channel
POST/v1/abil-ledger/float-eventsRecord top-up/adjustment

Xero connection & mapping (docs/XERO-INTEGRATION.md — 503 xero_not_configured until XERO_CLIENT_ID is set)

GET/v1/abil-xero/statusEvery Xero channel: organisation, legal name, base currency, status, last sync, last error, call-budget usage
POST/v1/abil-xero/connectStart a connection {abil_channel_id} → {authorize_url} (one-time, 5 minutes; abil:xero:connect)
GET/v1/abil-xero/authorizePUBLIC browser leg (?ticket=): sets the HttpOnly nonce cookie and 302s to Xero
GET/v1/abil-xero/callbackPUBLIC: Xero redirects here; 302 to XERO_RETURN_URL?xero=choose|error&xero_state=… (never tokens)
GET/v1/abil-xero/pending/:stateIdOrganisations this approval returned (name, legal name, VAT number, currency, demo flag, already bound elsewhere)
POST/v1/abil-xero/confirmBind one organisation to the channel {state_id, xero_tenant_id}; the others from that approval are disconnected
POST/v1/abil-xero/:channelId/disconnectStop posting: remove the connection at Xero and revoke the grant once nothing uses it (posted documents stay in Xero)
GET/v1/abil-xero/:channelId/mappingsTax label → TaxType, revenue key → account code, clearing account, sundry contact name
PUT/v1/abil-xero/:channelId/mappingsUpsert mappings {entries:[{kind: tax|account|setting, source_key, xero_value}]}; empty xero_value removes (abil:xero:map)
GET/v1/abil-xero/:channelId/optionsLive ACTIVE revenue/payment accounts and sales tax rates from the organisation (spends Xero calls)
POST/v1/abil-xero/:channelId/validateCheck every mapping against the live organisation → {ok, problems, warnings}

Accounting documents (canonical format → the tenant's Xero)

POST/v1/abil-accounting/:channelId/documentsQueue up to 500 canonical documents {source, documents[]} → 202. Auth: X-ABIL-Service-Key with the accounting scope for this channel, OR an IP4X user with abil:xero:map
POST/v1/abil-accounting/:channelId/previewRender the Xero payloads these documents would become — nothing queued, nothing sent
GET/v1/abil-accounting/:channelId/documentsOutbox and ID map (?state=queued|synced|exception|blocked&kind=contact|invoice|credit_note|payment|allocation|void)
POST/v1/abil-accounting/documents/:id/retryRe-queue an exception after fixing its cause
POST/v1/abil-accounting/:channelId/syncPost this channel's due documents now instead of on the worker's next tick
GET/v1/abil-accounting/:channelId/tie-out/:periodMonth-end tie-out (YYYY-MM): what ABIL posted vs what the organisation holds

Billing binding (SKU + customer → IP4X account item)

GET/v1/abil-billing/bindings(product_sku, customer_ref) → IP4X account item
POST/v1/abil-billing/bindingsCreate binding
DELETE/v1/abil-billing/bindings/:idRemove binding
POST/v1/abil-billing/push/:periodPush counts-only usage to IP4X partner ingest

Metrics (41)

lesaka.vp.vend lesaka.vp.topup lesaka.ppu.vend lesaka.bps.payment intouch.verification intouch.workflow intouch.consent intouch.tfs_screening google_maps.geocoding google_maps.places_details google_maps.autocomplete_request google_maps.autocomplete_session_unconcluded google_maps.dynamic_maps google_maps.static_maps riskscape.autocomplete riskscape.address_details riskscape.reverse_geocode riskscape.dynamic_maps riskscape.elevation_3d firstdmt_acs.device_licence firstdmt_acs.api_call gti_los.query interstellio.subscriber_active interstellio.subscriber_suspended interstellio.virtual_server interstellio.domain interstellio.api_call kamstrup_ready.read kamstrup_ready.meter_reporting kamstrup_ready.api_call smsportal.whatsapp.message smsportal.sms.part smsportal.api_call imis.get_build_pre_app imis.capture_complaint imis.get_complaint_status imis.get_consumers_account_data amber_panic.raise_alert amber_panic.update_location meteosource.point xero.document