> ## Documentation Index
> Fetch the complete documentation index at: https://docs.float.co.za/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Diagnose the most common integration problems — signature mismatches, missing callbacks, rate limits, and validation errors.

Work through the section matching your symptom. If you're still stuck, [contact support](#getting-help) with the details listed at the bottom — it turns a back-and-forth into a one-shot answer.

## `400` — "Request signature does not match"

The `X-Signature` you sent doesn't match what Float computed over the body it received. In practice this is almost always one of:

<AccordionGroup>
  <Accordion title="You pretty-printed the body">
    Float verifies against a **compact re-serialisation** of your payload (key order preserved, whitespace stripped), so a pretty-printed body never matches. Serialise compactly, sign that exact string, and send that same string.

    The `400` response's `detail` field includes the signature Float expected — compare it to yours: if they differ, your body bytes or key are wrong; if they match, you're mangling the header itself.

    ```javascript theme={null}
    // ❌ signs one serialisation, sends another
    const sig = sign(JSON.stringify(payload));
    await axios.post(url, payload, { headers: { "X-Signature": sig } });

    // ✅ sign and send the same string
    const body = JSON.stringify(payload);
    const sig = sign(body);
    await axios.post(url, body, {
      headers: { "X-Signature": sig, "Content-Type": "application/json" },
    });
    ```
  </Accordion>

  <Accordion title="Wrong algorithm or encoding">
    It must be **HMAC-SHA512**, output **Base64-encoded** (not hex, not URL-safe Base64). A hex digest is 128 characters; the correct Base64 digest is 88 characters ending in `=`.
  </Accordion>

  <Accordion title="Wrong key">
    Sign with the **signing key**, not the client secret — and check you're using the sandbox key against sandbox and the production key against production.
  </Accordion>

  <Accordion title="Signing is sticky and you stopped signing">
    Once Float has accepted one correctly signed request from your integration, unsigned mutating requests are rejected from then on. If you signed in a test script but your app doesn't sign yet, the app's requests will 400. Roll signing out everywhere at once. See [Authentication](/authentication#request-signing).
  </Accordion>
</AccordionGroup>

## Callback never arrives

<Steps>
  <Step title="Confirm the payment actually finished">
    Check the checkout with [`GET /api/checkouts/:id`](/api-reference/get-checkout). If it's still `received`, the shopper hasn't completed payment — there's nothing to deliver yet.
  </Step>

  <Step title="Check your endpoint is publicly reachable over HTTPS">
    `curl -X POST https://your-site.com/float/notify -d '{}' -H "Content-Type: application/json"` from outside your network. Localhost URLs, VPN-only hosts, and self-signed certificates all fail — TLS errors are a common silent killer, and Float alerts internally on SSL failures to your `notify_url`.
  </Step>

  <Step title="Check your endpoint responds 2xx fast">
    Non-2xx responses and timeouts are retried up to 18 times over \~3 hours, then delivery stops. WAFs and bot protection (Cloudflare challenges) returning 403 to server-to-server POSTs are a frequent cause.
  </Step>

  <Step title="Reconcile via the API">
    If the retry window has passed, the state is still one `GET` away — fetch the checkout and process it as you would the callback. Build this reconciliation as a scheduled job, not a manual step; see [Accepting payments → Reconcile](/guides/accepting-payments#6-reconcile).
  </Step>
</Steps>

## Shopper wasn't redirected back to my site

* The redirect uses the checkout's `success_url`/`cancel_url` — or the `redirect_url` your callback response returned. An **empty or malformed** `redirect_url` in your callback response is ignored; a wrong one wins over your configured URLs. Log what your handler returns.
* If your callback endpoint was down at completion time, Float still redirects the shopper using the URLs from the checkout — but your backend won't know about the payment until a retry lands or you reconcile. Never gate the "thank you" page on your own database alone; treat `success_url` hits as a hint and confirm via callback or API.

## `422` validation errors

| `detail` contains                                | Cause                                          | Fix                                                                                                  |
| ------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `Amount has to be between ...`                   | Amount outside your configured bands           | Check [`GET /api/config`](/api-reference/get-config) `rules`; hide Float at cart values outside them |
| `must match merchant territory currency`         | The wrong currency for your merchant territory | Send `ZAR`                                                                                           |
| `Orders can only be refunded within 100 days...` | Refund too late                                | Purchase date is day 0; day 100 still works, day 101 doesn't                                         |
| `Order can't be refunded on the day of purchase` | Same-day refund on an auto-refund integration  | Resubmit after 10:00 (local time) the next day                                                       |
| Refund amount errors                             | Amount exceeds the refundable remainder        | Sum your previous refunds; refund at most `amount - already_refunded`                                |
| Phone number errors                              | Format rejected                                | Must match `^[+0-9][0-9.()/-]{7,25}$` — strip spaces                                                 |

The same-day restriction only applies to integrations with **automatic refund approval** — manual-review integrations can submit immediately.

## `429` rate limits

The limits are listed per endpoint in the [API overview](/api-reference/overview#rate-limits). If you're hitting them:

* **`GET /api/config` (5/hour)** — cache it. It changes rarely; refresh hourly or on deploy.
* **`GET /api/checkouts/:id` (5/minute)** — you're polling instead of using callbacks. Poll only the specific orders you're reconciling.
* **`POST /api/refunds` (1/second)** — queue bulk refunds with a 1s+ spacing.

Honour the `Try again in N seconds` hint in the 429 body; don't hammer.

## Checkout link expired

The hosted payment session is valid for roughly **30 minutes**. If a shopper returns to a stale `payment_url`, create a **new checkout** (a fresh `client_reference_id` per attempt, or dedupe on your side) rather than reusing the old URL.

## Getting help

Email [support@float.co.za](mailto:support@float.co.za) with:

1. The **checkout ID** (`data.id` from create checkout) and your `client_reference_id`
2. The **environment** (sandbox or production) and timestamp (with timezone)
3. The full **request and response** (redact your Bearer token)
4. For callback issues: your `notify_url` and your endpoint's response/logs for the window

<Note>
  Float keeps a full audit trail of every callback attempt to your `notify_url` (request and response), so support can tell you exactly what your endpoint returned and when.
</Note>
