01 / Systems / Afriex

Cross-border payments

One app, many payment partners, one clear answer for the user.

Role
Software Engineer, Afriex
Years
2024 to 2025
Stack
Node.js, TypeScript
Focus
Integrations, webhooks
Requests from the app and business API go through the API to a layer of partner adapters, one per partner. Partners send webhooks back, which pass a signature check on the raw body, get mapped to internal statuses, and update the transaction once. REQUEST WEBHOOK STATUS APP mobile and web BUSINESS API clear error codes API routes, interactors PARTNER ADAPTERS one client per partner PARTNERS banks, mobile money SIGNATURE CHECK on the raw body STATUS MAPPING partner to internal TRANSACTIONS updated once REPLAYABLE IF IT FAILS NAME MISMATCH GOES TO REVIEW
Fig. 1 · Requests out, webhooks back

Afriex moves money across borders through partner payment companies. Each partner has its own API, its own status codes and its own idea of what a webhook looks like. The user just wants to know if their money arrived.

  • A wrong status costs real money.
  • Partners retry webhooks, go down, and change things with little warning.
  • Business customers shouldn't see a partner's raw error message.

I put each partner behind its own client with the same shape. The rest of the app asks "what's the status of this transfer?" and doesn't care who answers.

Webhooks all come in through one path: check the signature, translate the partner's status into ours, then update the transaction. If one fails, we can replay it. A webhook that arrives twice only counts once.

Deposits where the account name doesn't match the sender go to manual review instead of straight through. Partner errors map onto a short list of our own codes, so customers get something they can act on.

In 2025 one partner's webhook signatures stopped validating. I switched that endpoint off in production rather than accept events we couldn't verify.

We were checking the signature against the parsed request body, but the partner signed the raw bytes. I added middleware to keep the raw body, fixed the check, and wrote tests for missing signature fields before switching it back on.

A client per partner is more code than one generic one. When a partner changes something, though, the fix stays in one place.

Adding a partner means writing one adapter, not touching the core flow.