# Ultraner with Laravel

Laravel applies CSRF middleware to every POST route, so a webhook is rejected before it reaches your controller.

- Language: PHP
- Client: ultraner/sdk (https://github.com/Ultraner/ultraner-php)
- Human version of this page: https://ultraner.com/frameworks/laravel
- 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

Add the webhook path to the CSRF exception list, and use `$request->getContent()` for the raw body. `$request->all()` has already parsed it.

## Install

```bash
composer require ultraner/sdk
```

## Charge a mobile money wallet

```
<?php
// app/Http/Controllers/PaymentController.php
use Ultraner\Ultraner;
use Illuminate\Support\Str;

$ultraner = new Ultraner(env('ULTRANER_API_KEY'));

public function pay(Request $request)
{
    $payment = $ultraner->payments->createMnoCharge([
        'amount'         => 5000,          // whole shillings, TZS has no minor unit
        'currency'       => 'TZS',
        'provider'       => 'Vodacom',     // provider code, not brand name
        'account_number' => $request->input('phone'),
    ], ['idempotency_key' => (string) Str::uuid()]);

    // Accepted, not paid. Wait for the webhook.
    return response()->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.

```
<?php
// Exempt the route from CSRF first: a third party cannot hold your token.
// bootstrap/app.php  ->  $middleware->validateCsrfTokens(except: ['webhooks/ultraner']);

public function handle(Request $request)
{
    $raw       = $request->getContent();   // the bytes, not ->all()
    $signature = $request->header('X-Ultraner-Signature', '');

    $expected = hash_hmac('sha256', $raw, env('ULTRANER_WEBHOOK_SECRET'));

    // hash_equals, not ===, so timing does not leak the signature.
    if (! hash_equals($expected, $signature)) {
        return response()->noContent(400);
    }

    $event = json_decode($raw, true);
    HandleUltranerEvent::dispatch($event);  // queue it, acknowledge now
    return response()->noContent(200);
}
```

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