# Ultraner with Go

net/http gives you the raw body by default, which makes verification simpler here than almost anywhere else, as long as you read it before decoding.

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

`io.ReadAll(r.Body)` can only be done once. Read it into a slice, verify that slice, then unmarshal from the same bytes.

## Install

```bash
go get github.com/ultraner/ultraner-go
```

## Charge a mobile money wallet

```
package main

import (
    "os"
    "github.com/google/uuid"
    "github.com/ultraner/ultraner-go"
)

client := ultraner.New(os.Getenv("ULTRANER_API_KEY"))

payment, err := client.Payments.CreateMnoCharge(ctx, ultraner.MnoChargeParams{
    Amount:        5000,          // whole shillings: TZS has no minor unit
    Currency:      "TZS",
    Provider:      "Vodacom",     // provider code, not brand name
    AccountNumber: "255700000000",
}, ultraner.WithIdempotencyKey(uuid.NewString()))
if err != nil {
    return err
}
// Accepted, not paid.
log.Printf("reference=%s status=%s", payment.Reference, 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.

```
func ultranerWebhook(w http.ResponseWriter, r *http.Request) {
    // Read once, keep the bytes: you verify and unmarshal the same slice.
    raw, err := io.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    mac := hmac.New(sha256.New, []byte(os.Getenv("ULTRANER_WEBHOOK_SECRET")))
    mac.Write(raw)
    expected := hex.EncodeToString(mac.Sum(nil))

    // hmac.Equal is constant time; a == comparison is not.
    if !hmac.Equal([]byte(r.Header.Get("X-Ultraner-Signature")), []byte(expected)) {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    var event Event
    _ = json.Unmarshal(raw, &event)
    w.WriteHeader(http.StatusOK)   // acknowledge first
    go handle(event)               // then 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
