Cashback Claim

How to correctly process cashback claims? When a qualifying event occurs on your platform - a policy sold, an add-on applied, a claim excess paid - your backend notifies Fruga via a single server-to-server call. Fruga handles everything after that: recording the entitlement, updating the user's balance, and initiating the payout. This guide explains how that flow works and what to expect in each scenario.

💡
This is a guide to help you understand the end-to-end flow. For the exact request format, headers, and response codes, see Partner API → Cashback Claim.

How it works

The cashback claim flow allows users to redeem their insurance savings either as a direct payout to their bank account or as a discount applied to their next policy renewal.

The flow is initiated by your backend and follows a secure multi-step process:

1
Authentication & Identity Linking
Your backend generates a signed JWT assertion for the user and exchanges it for a Fruga access token. This securely links your user identity to a Fruga Wallet.
2
Balance Verification (Optional)
The application can call getBalance() from the SDK to show the user how much they have available to claim before they proceed.
3
Initiating the Claim
When a user clicks "Claim" or "Pay Now", your frontend collects the redemption details (amount and context) and calls your local backend claim endpoint.
4
Secure API Call (HMAC Signing)
Your backend computes an HMAC-SHA256 signature of the request and notifies Fruga via a server-to-server call. Fruga validates the signature and records the entitlement.

The userRef you include in a cashback claim must be exactly the same value you use as userRef in the JWT assertion when that user opens the widget. This is how Fruga connects the claim to the right user’s wallet and display.

⚠️
A mismatch between the `userRef` in a claim and the `userRef` in the widget assertion is the most common cause of claims not appearing in a user's widget. Use a stable, unique identifier from your system, typically a user ID, and use it consistently in both places.

Idempotency

Every claim request requires a partnerEventId - a unique string you generate for each event. Fruga uses this to make the endpoint idempotent: if the same partnerEventId is submitted more than once, Fruga returns the original response without creating a duplicate payout.

This protects you in real production scenarios - network timeouts, retry logic, or double-submission bugs will not result in a user being paid twice. The safest approach is to derive the partnerEventId from something that is naturally unique in your system, such as a policy transaction ID or order reference.

⚠️
Submitting the same `partnerEventId` with a different body returns a `409 Conflict`. The idempotency key is tied to the exact claim. It cannot be reused for a different amount or user.

Redemption context

The redemptionContext field tells Fruga what kind of event triggered the claim. This is used for reporting, display in the widget, and future analytics. Set it to the value that most accurately describes the event.

ValueWhen to use it
NEW_POLICYA user purchased a new insurance policy through your platform
POLICY_ADDONA user added a supplementary product or add-on to an existing policy
CLAIM_EXCESSA user paid a claim excess and is receiving cashback on that amount
OTHERAnything that does not fit the above must be accompanied by a redemptionContextNotes string explaining the event

Payout routing - how your configuration determines what happens next

Once Fruga receives and validates a claim, it determines how to route the payout based on your partner-level configuration, set up by Fruga during onboarding. You do not pass a routing destination in the claim request itself - the routing is always derived from your configuration.

There are two routing modes:

Option A - Pay the user directly (TO_USER)

Fruga pays the end user directly to their bank account. This is the default mode for most integrations. What happens after the claim depends on whether the user has already added their bank details in the widget.

ScenarioTransaction statusWhat the user sees
User has bank details on filePROCESSINGWidget shows the payout is on its way. No action required.
User has no bank details on fileACTION_REQUIREDWidget prompts the user to add their bank account details before the payout can proceed.

When a user in ACTION_REQUIRED status adds their bank details via the widget, the payout is automatically unblocked and moves to PROCESSING. No further action is needed from your side.

💡
In TO_USER mode, you have no responsibility for the last-mile payment. Fruga handles the bank transfer, confirmation, and any failure handling. The widget reflects the current status to the user at all times.

Option B - Pay the partner (TO_PARTNER)

Fruga records the payout entitlement and settles the amount with you as the partner, either via periodic batch settlement or automated transfer, depending on your agreed arrangement. You are then responsible for passing the funds to your end user through whatever mechanism your platform uses.

ScenarioTransaction statusWhat the user sees
Claim received, settlement pendingCREATED or PROCESSINGWidget shows the cashback has been recorded and is being processed. No bank details prompt is shown.
Settlement confirmedPAIDWidget shows the payout as complete.

In TO_PARTNER mode, the widget never asks the user for bank details - that interaction is handled entirely within your platform. The widget simply reflects the claim and its status.


Transaction status reference

Regardless of routing mode, every claim creates a payout transaction in Fruga’s system. Here are all the statuses a payout transaction can have and what they mean.

StatusMeaning
CREATEDClaim recorded. Payout is queued but not yet initiated.
ACTION_REQUIREDTO_USER only. User needs to add bank details. The widget shows a prompt.
NEEDS_DETAILSSimilar to Action Required; specific payout details are missing or invalid.
PROCESSINGPayout has been initiated and sent to the payment processor.
PENDINGClaim is awaiting review or manual processing.
PAIDPayout confirmed and complete.
FAILEDPayout attempt failed (e.g., invalid bank details).
CANCELLEDPayout was cancelled before completion.

Security Architecture

Relay uses a three-layer security model to ensure that every cashback claim is legitimate and every user session is secure.

  1. Server-to-Server communication: Sensitive operations like token exchange and cashback claims happen exclusively between your backend and Fruga. This keeps your private keys and secrets out of the browser.
  2. HMAC-SHA256 Request Signing: Every API request to Fruga is signed with a unique signature derived from the request body and a timestamp. This prevents request tampering and ensures only authorized partners can trigger claims.
  3. JWT Assertions: Securely links your internal user ID (userRef) to the Fruga Wallet. The short-lived assertion is exchanged for an access token, ensuring the user session is verified and temporary.

A complete example

The following shows a full cashback claim request in Node.js, including HMAC signing. This example uses the NEW_POLICY context and includes an optional policy reference.

import crypto from 'crypto';

interface ClaimRequest {
  userRef: string;
  amount: number;
  eventId: string;
  policyId: number;
}

const submitCashbackClaim = async ({ userRef, amount, eventId, policyId }: ClaimRequest): Promise<number> => {
  const partnerKey = process.env.FRUGA_PARTNER_KEY!;
  const secret = process.env.FRUGA_SIGNING_SECRET!;
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const path = '/cashback/claim';

  const body = JSON.stringify({
    partnerEventId: eventId,
    userRef,
    amount,
    redemptionContext: 'NEW_POLICY',
    reference: { policyId },
  });

  // Build the signature
  const bodyHash = crypto
    .createHash('sha256')
    .update(body)
    .digest('hex');

  const signingString = `${timestamp}.POST.${path}.${bodyHash}`;

  const signature = crypto
    .createHmac('sha256', secret)
    .update(signingString)
    .digest('base64');

  const response = await fetch('https://api.fruga.com/cashback/claim', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Partner-Key': partnerKey,
      'X-Partner-Timestamp': timestamp,
      'X-Partner-Signature': signature,
    },
    body,
  });

  if (!response.ok) {
    throw new Error(`Claim failed: ${response.status}`);
  }

  return response.status; // 201 created, 200 idempotent replay
};

Troubleshooting

ErrorLikely causeFix
401 UnauthorizedInvalid HMAC signatureVerify your signing string format: {timestamp}.POST.{path}.{bodyHash}.
409 ConflictSame partnerEventId used with a different bodyUse a unique event ID or keep the body identical for retries.
Insufficient FundsClaim amount exceeds available balanceValidate the user’s balance via getBalance() before initiating the claim.
Missing Payout InfoTO_USER mode but no bank account on fileRedirect the user to the Fruga Widget to add their payout details.
200 OK (instead of 201)Idempotent replayExpected on retries. No duplicate payout is created.
Claim not in widgetuserRef mismatchEnsure the ID used in the claim matches the one used in the JWT assertion.