# Ultraner with Flutter

A mobile app is the one place where the temptation to ship a secret key is strongest, and the payment is asynchronous, so the UI has to wait for something it cannot see.

- Language: Dart
- Client: ultraner (https://github.com/Ultraner/ultraner-dart)
- Human version of this page: https://ultraner.com/frameworks/flutter
- 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

Never put an API key in the app bundle. A compiled binary is readable, and a key in it is public. Charge from your own server and have the app poll or listen for the result.

## Install

```bash
flutter pub add ultraner
```

## Charge a mobile money wallet

```
// YOUR SERVER initiates the charge. The app never holds the key.
// The app asks your backend, which calls Ultraner:

Future<String> startPayment(String phone, int amount) async {
  final res = await http.post(
    Uri.parse('https://your-api.example.com/pay'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'phone': phone, 'amount': amount}),
  );
  final body = jsonDecode(res.body);

  // A reference, not a result. The payer is about to see a prompt.
  return body['reference'] as String;
}
```

## 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.

```
// The app cannot receive webhooks, so it waits on your backend, which does.
// Poll your own status endpoint, backing off, and stop on a terminal state.

Future<String> awaitResult(String reference) async {
  var delay = const Duration(seconds: 2);

  for (var attempt = 0; attempt < 10; attempt++) {
    await Future.delayed(delay);
    final res = await http.get(
      Uri.parse('https://your-api.example.com/payments/$reference'),
    );
    final status = jsonDecode(res.body)['status'] as String;

    // Terminal states. Anything else means keep waiting.
    if (status == 'success' || status == 'failed') return status;
    delay *= 2;
  }
  return 'pending';    // tell the user honestly, do not claim failure
}
```

## Worth knowing

The Dart client is for talking to your own backend and for reading public data. Charges and payouts are initiated server-side, so this page shows both halves.

## 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
