# Ultraner with Express

The default JSON body parser is the single most common cause of webhook signatures that never validate.

- Language: JavaScript
- Client: @ultraner/sdk (https://github.com/Ultraner/ultraner-node)
- Human version of this page: https://ultraner.com/frameworks/express
- Live in 14 markets: Benin, Cameroon, DR Congo, Gabon, Ivory Coast, Kenya, Mozambique, Republic of Congo, Rwanda, Senegal, Sierra Leone, Tanzania, Uganda and Zambia

## The mistake this framework invites

`express.json()` replaces the raw body before your handler sees it. Mount `express.raw()` on the webhook route only, leaving JSON parsing everywhere else.

## Install

```bash
npm install @ultraner/sdk
```

## Charge a mobile money wallet

```
const { Ultraner } = require('@ultraner/sdk');
const { randomUUID } = require('node:crypto');

const ultraner = new Ultraner(process.env.ULTRANER_API_KEY);

app.post('/pay', async (req, res) => {
  const payment = await ultraner.payments.createMnoCharge({
    amount: req.body.amount,       // whole shillings, TZS has no minor unit
    currency: 'TZS',
    provider: 'Vodacom',           // provider code, not brand name
    account_number: req.body.phone,
  }, { idempotencyKey: randomUUID() });

  res.json({ reference: payment.reference, status: payment.status });
});
```

## Verify a webhook

Where framework differences actually bite: the signature is computed over
the raw body, so any middleware that parses JSON before you read it will
break verification.

```
const crypto = require('node:crypto');

// Raw body for this route only; JSON everywhere else.
app.post('/webhooks/ultraner',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-Ultraner-Signature');
    const expected = crypto
      .createHmac('sha256', process.env.ULTRANER_WEBHOOK_SECRET)
      .update(req.body)            // the Buffer, not JSON.stringify(...)
      .digest('hex');

    const valid = crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expected, 'hex'),
    );
    if (!valid) return res.status(400).end();

    const event = JSON.parse(req.body.toString());
    res.status(200).end();         // acknowledge first
    void handle(event);            // then do the work
  });
```

## Things that are true regardless of framework

- **Amounts are whole units in most African currencies.** 5000 TZS is five
  thousand shillings, not fifty. Read `decimals` from
  https://ultraner.com/ai/context/currencies.json rather than assuming cents.
- **A charge is asynchronous.** A 200 means the prompt reached the payer's
  handset, not that they paid. Poll
  `GET /v1/payments/express/status/{transaction_id}` or wait for the webhook.
- **A charge takes `provider`; a payout takes `network`.** Both take
  `account_number`, never `accountNumber`. Unknown keys are stripped
  rather than rejected, so a wrong name does not error: the recipient
  disappears and the call fails as a missing required field.
- **Paying money out needs `X-Signature-Key` and `Idempotency-Key`.**

## More

- Every framework: https://ultraner.com/frameworks
- API reference: https://ultraner.com/docs
- The whole reference as Markdown: https://ultraner.com/docs.md
- OpenAPI: https://ultraner.com/openapi.json
- Agent skills: https://github.com/Ultraner/ultraner-skills
- Machine-readable index of all of this: https://ultraner.com/ai/context/frameworks.json
