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

# Incentives

> Read a customer's promotional-grant ledger and claw back a grant when its source event is voided, refunded, or fraudulent. Two callable operations.

## Overview

The incentives surface exposes a customer's promotional-grant ledger: the bonus credit they hold, the state of each grant, and the operator action to reverse a grant. Two operations are callable.

* `GET /customers/{customerId}/grants` lists a customer's promotional grants and the state of each.
* `POST /grants/{id}/clawback` reverses a grant when the event that funded it is voided, refunded, or fraudulent.

Promotional money is a separate value class from a customer's cash balance. It expires, it is never cashable out, and it is spent before cash under promo-first ordering. For the full money model and the grant state machine, see [Money: actual vs promotional](/concepts/money-classes).

<Info>
  Call `GET /capabilities` and `GET /openapi` at runtime for the authoritative list of what is available. Where this page and the served spec disagree, the spec wins.
</Info>

## List a customer's grants

`GET /customers/{customerId}/grants` returns the customer's promotional grants, each with its source, its state-machine state, its original and remaining amounts, and its expiry. The feed is read-only, tenant-scoped (it returns only grants belonging to the authenticated tenant), and cursor-paginated. Authenticate with the `PosTerminalJWT` scheme.

`customerId` is the customer's wallet identity id, passed as a path parameter.

### Query parameters

<ParamField query="state" param-type="string" required="false">
  Filter by grant state. One of `LOCKED`, `RELEASED`, `CLAWED_BACK`, `EXPIRED`.
</ParamField>

<ParamField query="cursor" param-type="string" required="false">
  Opaque pagination cursor. Pass `next_cursor` from the previous page.
</ParamField>

<ParamField query="limit" param-type="integer" required="false">
  Max grants per page. Default `50`, maximum `100`.
</ParamField>

### Response fields

<ResponseField name="customer_id" field-type="string" required="true">
  The wallet identity id the grants belong to.
</ResponseField>

<ResponseField name="grants" field-type="array" required="true">
  The customer's promotional grants. Each entry is a grant object (fields below).
</ResponseField>

<ResponseField name="next_cursor" field-type="string" required="false">
  The cursor for the next page, or `null` when there are no more pages.
</ResponseField>

<ResponseField name="has_more" field-type="boolean" required="true">
  `true` when more pages follow.
</ResponseField>

Each grant object carries:

<ResponseField name="grant_id" field-type="string" required="true">
  The grant's stable id. Pass this as the path id when clawing back.
</ResponseField>

<ResponseField name="source" field-type="string" required="true">
  What funded the grant. One of `CASHBACK`, `RELOAD_BONUS`, `SKU_TOPUP_BONUS`, `GATEWAY_BONUS`.
</ResponseField>

<ResponseField name="state" field-type="string" required="true">
  The state-machine state. One of `LOCKED`, `RELEASED`, `CLAWED_BACK`, `EXPIRED`. A `LOCKED` grant becomes `RELEASED` on claim; `CLAWED_BACK` and `EXPIRED` are terminal.
</ResponseField>

<ResponseField name="amount" field-type="object" required="true">
  The grant's original value, as `amount_minor` (integer, minor units) and `currency` (ISO-4217).
</ResponseField>

<ResponseField name="remaining" field-type="object" required="false">
  The unspent value, in the same `amount_minor` plus `currency` shape.
</ResponseField>

<ResponseField name="accrued_at" field-type="string" required="false">
  RFC3339 timestamp of when the grant accrued, or `null`.
</ResponseField>

<ResponseField name="expires_at" field-type="string" required="false">
  RFC3339 timestamp of when the grant expires unclaimed or unspent, or `null`.
</ResponseField>

<ResponseField name="source_event_ref" field-type="string" required="false">
  Server-derived reference to the finalized source event (for example `pos:order:5512`), or `null`.
</ResponseField>

<Info>
  An empty `grants` array is a success. It means the customer holds no grants matching the filter, not an error. A `404` (`NOT_FOUND`) means the customer was not found or belongs to another tenant.
</Info>

<CodeGroup>
  ```bash List grants theme={null}
  curl "https://api.feddi.io/v1/partner/customers/wu_4421/grants?state=LOCKED" \
    -H "Authorization: Bearer <terminal-jwt>"
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "customer_id": "wu_4421",
      "grants": [
        {
          "grant_id": "pg_7781",
          "source": "CASHBACK",
          "state": "LOCKED",
          "amount": { "amount_minor": 1000, "currency": "QAR" },
          "remaining": { "amount_minor": 1000, "currency": "QAR" },
          "accrued_at": "2026-06-01T12:00:00Z",
          "expires_at": "2026-09-01T12:00:00Z",
          "source_event_ref": "pos:order:5512"
        }
      ],
      "next_cursor": null,
      "has_more": false
    },
    "error": null,
    "meta": {
      "request_id": "req_bb11cc22",
      "idempotency_replayed": false,
      "api_version": "2026-06-01",
      "data_completeness_score": 80,
      "decision_trace_id": null
    }
  }
  ```

  ```json 404 Not Found theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "NOT_FOUND",
      "message": "Customer not found."
    },
    "meta": {
      "request_id": "req_bb11cc99",
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

### Errors

<ResponseField name="401 INVALID_API_KEY" field-type="error">
  The JWT is missing or invalid. Re-exchange your terminal credential for a fresh JWT and retry.
</ResponseField>

<ResponseField name="404 NOT_FOUND" field-type="error">
  The customer was not found, or resolves to another tenant. Cross-tenant lookups collapse to `NOT_FOUND` so existence is not leaked.
</ResponseField>

## Clawback a grant

`POST /grants/{id}/clawback` reverses a promotional grant when the source event that funded it is voided or refunded, or for fraud. Authenticate with the `ApiKeyAuth` scheme: this is an operator action gated to operator or admin keys, not a terminal JWT.

`id` is the `grant_id` from the grants list, passed as a path parameter.

A `LOCKED` grant claws back its full amount. A `RELEASED` grant claws back only the remaining unspent amount. Any already-spent portion is booked as a merchant loss and is never pulled back from the customer's cash balance, because promotional money never converts to cash. The grant transitions to `CLAWED_BACK`, which is terminal, in a single atomic operation.

<Danger>
  Clawback moves promotional money and lands the grant in a terminal state. It cannot be undone. Confirm the source event is genuinely voided, refunded, or fraudulent before calling.
</Danger>

### Request fields

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A unique key for this clawback. Replaying the same key is a no-op that returns the original result.
</ParamField>

<ParamField body="meta" param-type="object" required="true">
  Request metadata. Requires `partner_request_id` (your correlation UUID) and `api_version`. `occurred_at` and `sent_at` are recommended RFC3339 timestamps. `partner_request_id` is a correlation id only, never the deduplication key; deduplication is keyed on the `Idempotency-Key` header.
</ParamField>

<ParamField body="reason" param-type="string" required="true">
  Why the grant is being clawed back. One of `SOURCE_ORDER_VOIDED`, `SOURCE_REFUNDED`, `FRAUD`, `DUPLICATE`, `OTHER`.
</ParamField>

<ParamField body="note" param-type="string" required="false">
  Free-text note for the audit trail, or `null`.
</ParamField>

### Response fields

<ResponseField name="grant_id" field-type="string" required="true">
  The grant that was clawed back.
</ResponseField>

<ResponseField name="state" field-type="string" required="true">
  Always `CLAWED_BACK` on success.
</ResponseField>

<ResponseField name="clawed_back" field-type="object" required="true">
  The amount reversed, as `amount_minor` and `currency`.
</ResponseField>

<ResponseField name="spent_loss_logged" field-type="object" required="false">
  The already-spent promotional portion booked as a merchant loss, as `amount_minor` and `currency`. Not recovered from the customer.
</ResponseField>

<ResponseField name="clawed_at" field-type="string" required="false">
  RFC3339 timestamp of the clawback.
</ResponseField>

<CodeGroup>
  ```bash Clawback wrap="true" theme={null}
  curl -X POST https://api.feddi.io/v1/partner/grants/pg_7781/clawback \
    -H "x-api-key: <ops-api-key>" \
    -H "Idempotency-Key: 2b3c4d5e-6f70-8192-0314-253647586a7b" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": {
        "partner_request_id": "2b3c4d5e-6f70-8192-0314-253647586a7b",
        "occurred_at": "2026-06-05T11:10:00Z",
        "sent_at": "2026-06-05T11:10:01Z",
        "api_version": "2026-06-01"
      },
      "reason": "SOURCE_ORDER_VOIDED",
      "note": "pos.order 5512 refunded"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "grant_id": "pg_7781",
      "state": "CLAWED_BACK",
      "clawed_back": { "amount_minor": 600, "currency": "QAR" },
      "spent_loss_logged": { "amount_minor": 400, "currency": "QAR" },
      "clawed_at": "2026-06-05T11:10:02Z"
    },
    "error": null,
    "meta": {
      "request_id": "req_cc22dd33",
      "idempotency_replayed": false,
      "api_version": "2026-06-01"
    }
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Grant is already EXPIRED or CLAWED_BACK and cannot be clawed back."
    },
    "meta": {
      "request_id": "req_cc22dd44",
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

### Idempotency

The clawback is mutating and idempotent on the `Idempotency-Key` header. Send a unique key per clawback. A same-key, same-payload retry replays the original result with `meta.idempotency_replayed: true`. Clawing back an already `CLAWED_BACK` grant is a no-op replay. A same-key, different-payload request returns `IDEMPOTENCY_KEY_REUSED` (`422`). See [Idempotency and errors](/concepts/idempotency-and-errors).

### Errors

<ResponseField name="400 VALIDATION_ERROR" field-type="error">
  The grant is not in a clawback-able state (already `EXPIRED` or `CLAWED_BACK`), or the body failed validation. Re-read the grant with the list endpoint to confirm its current state.
</ResponseField>

<ResponseField name="401 INVALID_API_KEY" field-type="error">
  The API key is missing or invalid.
</ResponseField>

<ResponseField name="403 FORBIDDEN" field-type="error">
  The key lacks clawback permission. Clawback requires an operator or admin key.
</ResponseField>

<ResponseField name="404 NOT_FOUND" field-type="error">
  The grant was not found, or belongs to another tenant.
</ResponseField>

<ResponseField name="422 IDEMPOTENCY_KEY_REUSED" field-type="error">
  The same `Idempotency-Key` was presented with a different payload. Use a new key.
</ResponseField>

## Response metadata

Both operations return the standard `meta` envelope. Two fields are informational:

* `data_completeness_score`: an opaque integer Feddi may use internally. No action required.
* `decision_trace_id`: an opaque correlation id. Persist it and echo it where a call accepts one.

## Where to go next

<Columns cols="2">
  <Card title="Money: actual vs promotional" icon="coins" href="/concepts/money-classes">
    The two-class balance model and the grant state machine: LOCKED, RELEASED, CLAWED\_BACK, EXPIRED.
  </Card>

  <Card title="Transactions" icon="receipt" href="/guides/transactions">
    Read the transaction record where redeemed promotional credit and clawbacks land.
  </Card>

  <Card title="Idempotency and errors" icon="repeat" href="/concepts/idempotency-and-errors">
    The idempotency contract and the typed error codes both operations return.
  </Card>

  <Card title="API conventions" icon="book-open" href="/api-reference/conventions">
    The response envelope, money-as-minor-units, pagination, and versioning.
  </Card>
</Columns>
