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

# Callbacks

> Float's signed server-to-server payment notifications: payload, verification, retries, and the dynamic redirect.

When a checkout reaches a terminal state, Float POSTs a JSON callback to the `notify_url` you supplied when creating the checkout. **This callback is your source of truth for order state.**

## The payload

```json theme={null}
{
  "status": "successful",
  "amount": 249900,
  "currency": "GBP",
  "client_reference_id": "order-1001",
  "number_of_payments": 3
}
```

| Field                 | Type    | Description                                                                                                 |
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `status`              | string  | `successful` or `cancelled`. Nothing else — a declined card lets the shopper retry, so there's no `failed`. |
| `amount`              | integer | The checkout amount in pence, as originally submitted.                                                      |
| `currency`            | string  | ISO 4217 code (`GBP`).                                                                                      |
| `client_reference_id` | string  | Echoed verbatim from your checkout request. Use it to find the order.                                       |
| `number_of_payments`  | integer | The instalment term the shopper selected.                                                                   |

Headers:

```
Content-Type: application/json
X-Signature: <Base64 HMAC-SHA512 of the raw body>
```

## Verifying the signature

Float signs the raw JSON body with **your signing key** (the same one you use for [request signing](/authentication#request-signing)). Verify every callback before trusting it:

<CodeGroup>
  ```ruby Ruby (Rails) theme={null}
  class FloatCallbacksController < ApplicationController
    skip_before_action :verify_authenticity_token

    def create
      raw_body  = request.body.read
      expected  = OpenSSL::HMAC.base64digest("SHA512", ENV.fetch("FLOAT_SIGNING_KEY"), raw_body)
      signature = request.headers["X-Signature"].to_s

      return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)

      payload = JSON.parse(raw_body)
      order = Order.find_by!(reference: payload["client_reference_id"])
      order.mark_paid!(instalments: payload["number_of_payments"]) if payload["status"] == "successful"
      order.mark_cancelled! if payload["status"] == "cancelled"

      render json: { redirect_url: order_confirmation_url(order) }
    end
  end
  ```

  ```javascript Node.js (Express) theme={null}
  const crypto = require("crypto");
  const express = require("express");
  const app = express();

  app.post("/float/notify", express.raw({ type: "application/json" }), (req, res) => {
    const expected = crypto
      .createHmac("sha512", process.env.FLOAT_SIGNING_KEY)
      .update(req.body)
      .digest("base64");
    const signature = req.get("X-Signature") || "";

    const valid =
      expected.length === signature.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
    if (!valid) return res.sendStatus(401);

    const payload = JSON.parse(req.body);
    // ...update the order by payload.client_reference_id...

    res.json({ redirect_url: `https://example.com/orders/${payload.client_reference_id}/confirmed` });
  });
  ```

  ```php PHP theme={null}
  $rawBody   = file_get_contents('php://input');
  $expected  = base64_encode(hash_hmac('sha512', $rawBody, getenv('FLOAT_SIGNING_KEY'), true));
  $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

  if (!hash_equals($expected, $signature)) {
      http_response_code(401);
      exit;
  }

  $payload = json_decode($rawBody, true);
  // ...update the order by $payload['client_reference_id']...

  header('Content-Type: application/json');
  echo json_encode(['redirect_url' => 'https://example.com/orders/confirmed']);
  ```
</CodeGroup>

<Warning>
  Verify against the **raw request body bytes**, not a re-serialised parse of them, and use a constant-time comparison. Reject anything that doesn't verify.
</Warning>

## Responding

* Return any **`2xx`** status to acknowledge. Anything else counts as a failed delivery and triggers retries.
* Respond **fast** (well under 10 seconds). Do heavy work — emails, ERP syncs — asynchronously after acknowledging.

### Dynamic redirect (optional)

If your `2xx` response body is JSON containing a `redirect_url`, Float sends the shopper's browser **there** instead of your static `success_url`/`cancel_url`:

```json theme={null}
{ "redirect_url": "https://example.com/orders/1001/confirmed?token=abc123" }
```

This lets you land the shopper on an order-specific, authenticated confirmation page. Notes:

* Only honoured on the **initial synchronous delivery** (the one made while the shopper is waiting on Float's page). Retried deliveries can't affect a redirect that already happened.
* Omit the field (or return an empty body) to use your static URLs — that's perfectly fine.

## Delivery and retries

* The first delivery happens synchronously while the shopper is still on Float's payment page.
* If it fails (timeout, non-2xx, network error), Float retries in the background — **up to 18 attempts at \~10-minute intervals (\~3 hours)**.
* The shopper is still redirected using your static URLs even when the callback fails, so your `success_url` page must tolerate the order not being marked paid *yet*.
* After retries are exhausted, Float's team is alerted and can re-trigger the notification manually. Your safety net is [reconciliation via `GET /api/checkouts/:id`](/guides/accepting-payments#6-reconcile).

## Idempotency

Duplicate deliveries are possible (retry races, manual re-triggers). Key your handler on `client_reference_id` + `status`: if the order is already in the target state, return `2xx` without side effects.

## Endpoint requirements

* Public HTTPS with a **valid certificate** — SSL errors are a common cause of failed deliveries and will page Float's support team.
* No authentication challenge (signature verification replaces it).
* If your infrastructure requires IP allowlisting, ask Float support about static egress IPs.

## Testing your handler locally

Simulate a callback with curl (signature computed with your sandbox signing key):

```bash theme={null}
BODY='{"status":"successful","amount":249900,"currency":"GBP","client_reference_id":"order-1001","number_of_payments":3}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha512 -hmac "$FLOAT_SIGNING_KEY" -binary | base64)

curl -X POST https://localhost:3000/float/notify \
  -H "Content-Type: application/json" \
  -H "X-Signature: $SIG" \
  -d "$BODY"
```

For end-to-end sandbox tests, expose your local server with a tunnel (ngrok, Cloudflare Tunnel) and use the tunnel URL as `notify_url`.
