Building Maintainable APIs: Lessons from 3 Systems
Production API habits from POS, SACCO, and CRM systems - versioning, errors, auth, and docs juniors can still ship against.
KisoByte Solutions · 5 min read

APIs do not usually fail on day one. They fail in month nine, when a second mobile client arrives, a partner wants “just one more field,” and nobody remembers why status means three different things. This post is about building maintainable APIs from three production systems we have shipped and supported - a retail POS bridge, a SACCO membership core, and a CRM/lead intake layer - without pretending every SME needs microservices.
If you are still deciding whether to build at all, start with the complete guide to custom software for SMEs in Kenya. If the fork is buy vs build, pair that with custom software vs off-the-shelf. Here we stay in the engineering habits that keep integrations alive.
What “maintainable” meant on each system
We judged maintainability by how painful change felt after launch:
| System | Change that tested the API | What held up | What we fixed later |
|---|---|---|---|
| POS bridge | New payment channel + offline retry | Idempotent write endpoints | Error codes were too free-form |
| SACCO core | New member grade + dividend report | Clear resource boundaries | Over-eager nested payloads |
| CRM / leads | Second channel (WhatsApp + web) | Stable lead schema + events | Auth tokens lived too long |
None of these needed a platform rewrite. They needed boring contracts: predictable URLs, stable fields, documented errors, and a change process humans actually follow.
Lesson 1 - Design around resources, not screens
Early POS integrations looked like the UI: POST /saveSaleScreen with a blob matching whatever the cashier saw that week. When the UI changed, every partner broke.
We moved to resources partners already understand: sale, payment, stock-adjustment, member, lead. Screens compose resources; APIs do not mirror pixel layouts.
Practical rule: if you cannot explain the endpoint to a non-frontend teammate in one sentence, it is probably a screen dump. Rename it before people depend on it.
Lesson 2 - Version at the edge, freeze in production
We keep a major version in the path (/v1/...) and treat additive fields as compatible when clients ignore unknowns. Breaking renames wait for /v2 or a negotiated sunset window.
| Change type | How we ship it | Client impact |
|---|---|---|
| Add optional field | Same version | None if ignored |
| Widen validation | Same version + warn in notes | Only bad payloads fail |
| Rename / remove field | New version | Migration needed |
| Change auth meaning | New version | Hard cutoff date |
For East African partners - banks, SMS gateways, warehouse apps on flaky links - surprise renames cost more than an extra version folder. Publish a sunset date [VERIFY: e.g. 90 days] and keep dual-run if money moves through the call.
Lesson 3 - Errors must be machine-readable and human-honest
The POS bridge originally returned { "error": "Something went wrong" }. Support could not triage. We standardised:
{
"error": {
"code": "PAYMENT_ALREADY_RECORDED",
"message": "This payment reference was already posted.",
"retryable": false,
"requestId": "..."
}
}
Checklist we refuse to ship without:
- Stable
codestring (not only HTTP status) - Safe
message(no stack traces, no raw SQL) -
retryableflag for timeouts vs business rejects -
requestIdin logs and response - Documented mapping for the top 15 codes
SACCO money flows especially need this. Ambiguous failures create double posts and angry treasurers.
Lesson 4 - Idempotency is not optional when money or membership is involved
Retail and SACCO clients retry when the network drops mid-response. Without idempotency keys, you invent duplicate sales or duplicate receipts.
Pattern that worked across systems:
- Client sends
Idempotency-Key(or a business unique reference: M-Pesa receipt, invoice number). - Server stores the first successful result for that key.
- Replays return the same result, not a second write.
Budget a small store for keys (DB table or Redis) and a retention window [VERIFY: 24–72 hours is common]. This is dull engineering. It prevents weekend emergencies.
Lesson 5 - Auth that operations can revoke
Long-lived shared API keys “for the partner” aged badly. CRM lead intake taught us to prefer:
- Short-lived tokens where possible
- Per-client credentials (not one key for “all mobile apps”)
- Ability to revoke one channel without killing the others
- Scope strings (
leads:write,members:read) instead of god-mode keys
When a contractor laptop goes missing, you want a revoke button - not a full production restart. How you hire and brief people who will own these systems belongs in your custom software delivery conversation as much as in the code review.
Lesson 6 - Docs that match production, not the pitch deck
We maintain an OpenAPI (or equivalent) file in the same repo as the API, generated or hand-checked before release. README screenshots go stale; the contract file is the source of truth.
Minimum doc set for an SME-facing API:
- Auth how-to with a copy-paste example
- Happy-path for the three main flows
- Error code list
- Rate limits / pagination rules
- Changelog dated with breaking vs additive notes
If a new hire cannot call the sandbox in one afternoon, maintainability is already lost.
Lesson 7 - Observe before you optimise
All three systems eventually needed the same operational basics:
- Structured logs with
requestId - Latency and 4xx/5xx dashboards
- Alerts on error-code spikes (not only CPU)
- A weekly glance at slow endpoints
We wasted time micro-optimising endpoints that were fine while silent 409 spikes hid a client bug. Measure, then fix.
A maintainability checklist before you call v1 “done”
Use this before the first external consumer goes live:
| Item | Done? |
|---|---|
| Resource-shaped endpoints (not screen dumps) | |
| Version strategy written down | |
| Idempotency on money / membership writes | |
| Stable error codes + request IDs | |
| Per-client credentials + revoke path | |
| Contract file matches staging | |
| Staging sandbox with fake data | |
| Owner named for API changes |
If more than two boxes are empty, you are still in prototype territory - fine for internal experiments, risky for partners.
What we would tell a founder before funding an API layer
You do not need a “platform team.” You need a clear scope, a small surface area, and the discipline to treat the contract as a product - the same discipline as the written-scope habit applied to integrations. Over-building early (extra services, clever abstractions) ages worse than a simple monolith with a disciplined public API.
When the business process itself is still fuzzy, fix that first - see again the SME custom software guide. When the process is clear and systems must talk, invest in the boring habits above. They are the difference between an integration that still works after the third client - and one that only works for the person who wrote it.
Ready to scope a build that fits your budget?
Tell us what you sell, who uses the system, and what “done” looks like. We’ll come back with a clear scope, timeline bands, and whether a product or custom build is the smarter path.
