{"service":"abil-api","handlers":[{"handler":"lesaka","name":"Lesaka (EasyPay)","module":"abil:lesaka","flavour":"transactional","implemented":false,"is_beta":true,"default_territories":["ZA"]},{"handler":"intouch","name":"InTouch (eKYC metering)","module":"abil:intouch","flavour":"metered","implemented":true,"is_beta":false,"default_territories":["ZA"]},{"handler":"google_maps","name":"Google Maps Platform","module":"abil:google","flavour":"metered","implemented":true,"is_beta":false,"default_territories":null},{"handler":"riskscape","name":"RiskScape","module":"abil:riskscape","flavour":"metered","implemented":false,"is_beta":true,"default_territories":["ZA"]},{"handler":"firstdmt_acs","name":"FirstDMT (Axiros AXESS.ACS)","module":"abil:firstdmt","flavour":"metered","implemented":false,"is_beta":true,"default_territories":["ZA"]},{"handler":"gti_los","name":"GeoTerraImage Line of Sight","module":"abil:gti","flavour":"metered","implemented":true,"is_beta":false,"default_territories":["ZA"]},{"handler":"interstellio","name":"Interstellio NebularStack (RADIUS)","module":"abil:interstellio","flavour":"metered","implemented":true,"is_beta":true,"default_territories":["ZA"]},{"handler":"kamstrup_ready","name":"Kamstrup READy (water metering)","module":"abil:kamstrup","flavour":"metered","implemented":true,"is_beta":true,"default_territories":null},{"handler":"meteosource","name":"Meteosource weather","module":"abil:weather","flavour":"metered","implemented":true,"is_beta":false,"default_territories":null},{"handler":"imis","name":"IMIS (T-GIS) municipal system of record","module":"abil:imis","flavour":"metered","implemented":true,"is_beta":false,"default_territories":null},{"handler":"amber_panic","name":"Amber Innovations PanicApp (armed response)","module":"abil:panic","flavour":"transactional","implemented":true,"is_beta":false,"default_territories":null},{"handler":"smsportal","name":"SMSPortal (LINK Mobility) WhatsApp & SMS","module":"abil:smsportal","flavour":"metered","implemented":true,"is_beta":true,"default_territories":["ZA"]},{"handler":"xero","name":"Xero (accounting)","module":"abil:xero","flavour":"metered","implemented":true,"is_beta":false,"default_territories":null}],"metrics":[{"key":"lesaka.vp.vend","label":"Virtual product vend","unit":"transaction","aggregation":"sum"},{"key":"lesaka.vp.topup","label":"Pinless top-up","unit":"transaction","aggregation":"sum"},{"key":"lesaka.ppu.vend","label":"Prepaid utility vend","unit":"transaction","aggregation":"sum"},{"key":"lesaka.bps.payment","label":"Bill payment","unit":"transaction","aggregation":"sum"},{"key":"intouch.verification","label":"Verification service call","unit":"call","aggregation":"sum"},{"key":"intouch.workflow","label":"Workflow run","unit":"workflow","aggregation":"sum"},{"key":"intouch.consent","label":"Consent request","unit":"request","aggregation":"sum"},{"key":"intouch.tfs_screening","label":"TFS continuous screening","unit":"client_month","aggregation":"period_snapshot"},{"key":"google_maps.geocoding","label":"Geocoding","unit":"call","aggregation":"sum"},{"key":"google_maps.places_details","label":"Places Details","unit":"call","aggregation":"sum"},{"key":"google_maps.autocomplete_request","label":"Autocomplete (per request)","unit":"call","aggregation":"sum"},{"key":"google_maps.autocomplete_session_unconcluded","label":"Autocomplete session w/o Details","unit":"session","aggregation":"sum"},{"key":"google_maps.dynamic_maps","label":"Dynamic map load","unit":"load","aggregation":"sum"},{"key":"google_maps.static_maps","label":"Static map","unit":"call","aggregation":"sum"},{"key":"riskscape.autocomplete","label":"Autocomplete request","unit":"event","aggregation":"sum"},{"key":"riskscape.address_details","label":"Address details","unit":"event","aggregation":"sum"},{"key":"riskscape.reverse_geocode","label":"Reverse geocode","unit":"event","aggregation":"sum"},{"key":"riskscape.dynamic_maps","label":"Dynamic map transaction","unit":"transaction","aggregation":"sum"},{"key":"riskscape.elevation_3d","label":"3D elevation query","unit":"event","aggregation":"sum"},{"key":"firstdmt_acs.device_licence","label":"ACS device licence","unit":"device_month","aggregation":"period_snapshot"},{"key":"firstdmt_acs.api_call","label":"ACS API call","unit":"call","aggregation":"sum"},{"key":"gti_los.query","label":"Line-of-sight query","unit":"call","aggregation":"sum"},{"key":"interstellio.subscriber_active","label":"Active subscriber month","unit":"subscriber_month","aggregation":"period_snapshot"},{"key":"interstellio.subscriber_suspended","label":"Suspended subscriber month","unit":"subscriber_month","aggregation":"period_snapshot"},{"key":"interstellio.virtual_server","label":"Virtual RADIUS server month","unit":"instance_month","aggregation":"period_snapshot"},{"key":"interstellio.domain","label":"Domain month","unit":"instance_month","aggregation":"period_snapshot"},{"key":"interstellio.api_call","label":"Provisioning/CoA API call","unit":"call","aggregation":"sum"},{"key":"kamstrup_ready.read","label":"Meter reading ingested","unit":"reading","aggregation":"sum"},{"key":"kamstrup_ready.meter_reporting","label":"Meters reporting in period","unit":"meter_month","aggregation":"period_snapshot"},{"key":"kamstrup_ready.api_call","label":"READy API call","unit":"call","aggregation":"sum"},{"key":"smsportal.whatsapp.message","label":"WhatsApp message delivered","unit":"message","aggregation":"sum"},{"key":"smsportal.sms.part","label":"SMS message part sent","unit":"part","aggregation":"sum"},{"key":"smsportal.api_call","label":"SMSPortal API call","unit":"call","aggregation":"sum"},{"key":"imis.get_build_pre_app","label":"IMIS building application read","unit":"call","aggregation":"sum"},{"key":"imis.capture_complaint","label":"IMIS complaint captured","unit":"complaint","aggregation":"sum"},{"key":"imis.get_complaint_status","label":"IMIS complaint status read (PROPOSED)","unit":"call","aggregation":"sum"},{"key":"imis.get_consumers_account_data","label":"IMIS consumer account read","unit":"call","aggregation":"sum"},{"key":"amber_panic.raise_alert","label":"Panic alert raised","unit":"alert","aggregation":"sum"},{"key":"amber_panic.update_location","label":"Panic live-location ping","unit":"call","aggregation":"sum"},{"key":"meteosource.point","label":"Weather point query","unit":"call","aggregation":"sum"},{"key":"xero.document","label":"Document posted to Xero","unit":"document","aggregation":"sum"}],"endpoints":[{"group":"Channels","routes":[["GET","/v1/abil-channels","List channels (secrets masked; secrets_set booleans)"],["POST","/v1/abil-channels","Create channel (configuration encrypted at rest)"],["GET","/v1/abil-channels/:id","Get channel"],["PUT","/v1/abil-channels/:id","Update channel (omitted secrets preserved)"],["DELETE","/v1/abil-channels/:id","Soft-delete channel"],["GET","/v1/abil-channels/available-handlers","Provider catalog for this licence"],["GET","/v1/abil-channels/handlers/:handler/schema","Configuration schema"],["GET","/v1/abil-channels/handlers/:handler/actions","Actions a handler exposes"],["POST","/v1/abil-channels/:id/execute","Execute 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"]]},{"group":"Supplier webhooks (PUBLIC — the channel's webhook_secret is the credential)","routes":[["POST","/v1/abil-webhooks/:channelId/delivery","SMSPortal 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)"]]},{"group":"CDRs (metering spine)","routes":[["GET","/v1/abil-cdrs/metrics","Metric registry"],["POST","/v1/abil-cdrs","Record a CDR (idempotent via client_reference)"],["GET","/v1/abil-cdrs","Query CDRs (period, metric_key, product_sku, handler, disposition…)"],["GET","/v1/abil-cdrs/summary/:period","Per-metric aggregates for a period"]]},{"group":"Cost rates (ABIL holds cost only — the customer price lives on the IP4X Product)","routes":[["GET","/v1/abil-rates","Cost rate cards (negative = commission earned)"],["POST","/v1/abil-rates","Create a cost rate (effective-dated; supersede, never edit)"],["POST","/v1/abil-rates/:id/close","End-date a rate"],["POST","/v1/abil-rates/rate-period/:period","Rating pass (?reRate=true for month-end whole-volume correction)"]]},{"group":"SKU mapping (cost ↔ IP4X Product — see docs/SKU-CONTRACT.md)","routes":[["GET","/v1/abil-sku-map","(metric_key, service_code) → product SKU"],["POST","/v1/abil-sku-map","Add a mapping (effective-dated; supersede, never edit)"],["POST","/v1/abil-sku-map/:id/close","End-date a mapping"],["DELETE","/v1/abil-sku-map/:id","Soft-delete a mapping"],["POST","/v1/abil-sku-map/restamp/:period","Re-resolve SKUs on a period (409 once pushed)"],["GET","/v1/abil-sku-map/coverage/:period","Mapped vs still on the metric-key fallback"]]},{"group":"Consumers & service keys","routes":[["GET","/v1/abil-consumers","Consumer registry (a per-tenant tree)"],["POST","/v1/abil-consumers","Register a consumer (optional parent_id = tree edge)"],["PUT","/v1/abil-consumers/:id","Update a consumer {name?, notes?, is_active?} — is_active: true reactivates one a blocking cap switched off"],["POST","/v1/abil-consumers/:id/keys","Issue 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/:keyId","Revoke a service key"],["GET","/v1/abil-caps","Usage / spend caps (?scope=channel|consumer|tenant&scope_id=)"],["GET","/v1/abil-caps/usage","Caps with the current window's used vs limit"],["POST","/v1/abil-caps","Create 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/:id","Remove a cap"],["GET","/v1/abil-services","Service groups: one service, several providers"],["GET","/v1/abil-services/status","Members in cost order + the margin verdict per service (?code=)"],["POST","/v1/abil-services","Create a service {code, name, product_sku, margin_floor_pct?=10}"],["PUT","/v1/abil-services/:code","Update a service (name, product_sku, margin_floor_pct 0–95 as a margin on price, is_active, notes)"],["DELETE","/v1/abil-services/:code","Remove a service"],["POST","/v1/abil-services/:code/members","Add a provider {abil_channel_id, action, metric_key, service_code?}"],["DELETE","/v1/abil-services/members/:id","Remove a provider"],["POST","/v1/abil-services/:code/execute","Run 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-ingest","Consumer usage ingest — auth by X-ABIL-Service-Key; customer_ref from the key, not the body"]]},{"group":"Periods & the pull read API","routes":[["GET","/v1/abil-periods/:period","Period status (open | rated | sealed)"],["POST","/v1/abil-periods/:period/seal","Seal a period — the read API then serves it"],["POST","/v1/abil-periods/:period/reopen","Audited reopen (reason required) -> rated"],["GET","/v1/usage","PULL: sealed rated usage per consumer per SKU; auth by service key, scoped to the caller subtree"]]},{"group":"Reports (manual recon)","routes":[["GET","/v1/abil-reports/supplier/:period","Supplier usage/cost by metric (+ flat-fee amortisation) — ?format=csv"],["GET","/v1/abil-reports/billing/:period","What the next usage push will claim, per customer per SKU — ?format=csv"],["GET","/v1/abil-reports/tenant/:period","Tenant cost by SKU / customer / metric — ?format=csv"]]},{"group":"Ledger & waterfall (all configuration)","routes":[["GET","/v1/abil-ledger/parties","Party registry"],["POST","/v1/abil-ledger/parties","Create party"],["GET","/v1/abil-ledger/schemes","Split schemes with lines"],["POST","/v1/abil-ledger/schemes","Create effective-dated waterfall scheme"],["POST","/v1/abil-ledger/allocate/:period","Allocate rated CDRs to ledger entries + float draws"],["GET","/v1/abil-ledger/report/:period","Party totals for a period"],["GET","/v1/abil-ledger/floats","Float balances per channel"],["POST","/v1/abil-ledger/float-events","Record top-up/adjustment"]]},{"group":"Xero connection & mapping (docs/XERO-INTEGRATION.md — 503 xero_not_configured until XERO_CLIENT_ID is set)","routes":[["GET","/v1/abil-xero/status","Every Xero channel: organisation, legal name, base currency, status, last sync, last error, call-budget usage"],["POST","/v1/abil-xero/connect","Start a connection {abil_channel_id} → {authorize_url} (one-time, 5 minutes; abil:xero:connect)"],["GET","/v1/abil-xero/authorize","PUBLIC browser leg (?ticket=): sets the HttpOnly nonce cookie and 302s to Xero"],["GET","/v1/abil-xero/callback","PUBLIC: Xero redirects here; 302 to XERO_RETURN_URL?xero=choose|error&xero_state=… (never tokens)"],["GET","/v1/abil-xero/pending/:stateId","Organisations this approval returned (name, legal name, VAT number, currency, demo flag, already bound elsewhere)"],["POST","/v1/abil-xero/confirm","Bind one organisation to the channel {state_id, xero_tenant_id}; the others from that approval are disconnected"],["POST","/v1/abil-xero/:channelId/disconnect","Stop posting: remove the connection at Xero and revoke the grant once nothing uses it (posted documents stay in Xero)"],["GET","/v1/abil-xero/:channelId/mappings","Tax label → TaxType, revenue key → account code, clearing account, sundry contact name"],["PUT","/v1/abil-xero/:channelId/mappings","Upsert mappings {entries:[{kind: tax|account|setting, source_key, xero_value}]}; empty xero_value removes (abil:xero:map)"],["GET","/v1/abil-xero/:channelId/options","Live ACTIVE revenue/payment accounts and sales tax rates from the organisation (spends Xero calls)"],["POST","/v1/abil-xero/:channelId/validate","Check every mapping against the live organisation → {ok, problems, warnings}"]]},{"group":"Accounting documents (canonical format → the tenant's Xero)","routes":[["POST","/v1/abil-accounting/:channelId/documents","Queue 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/preview","Render the Xero payloads these documents would become — nothing queued, nothing sent"],["GET","/v1/abil-accounting/:channelId/documents","Outbox and ID map (?state=queued|synced|exception|blocked&kind=contact|invoice|credit_note|payment|allocation|void)"],["POST","/v1/abil-accounting/documents/:id/retry","Re-queue an exception after fixing its cause"],["POST","/v1/abil-accounting/:channelId/sync","Post this channel's due documents now instead of on the worker's next tick"],["GET","/v1/abil-accounting/:channelId/tie-out/:period","Month-end tie-out (YYYY-MM): what ABIL posted vs what the organisation holds"]]},{"group":"Billing binding (SKU + customer → IP4X account item)","routes":[["GET","/v1/abil-billing/bindings","(product_sku, customer_ref) → IP4X account item"],["POST","/v1/abil-billing/bindings","Create binding"],["DELETE","/v1/abil-billing/bindings/:id","Remove binding"],["POST","/v1/abil-billing/push/:period","Push counts-only usage to IP4X partner ingest"]]}]}