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.
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:
getBalance() from the SDK to show the user how much they have available to claim before they proceed.The userRef - the critical link
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.
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.
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.
| Value | When to use it |
|---|---|
NEW_POLICY | A user purchased a new insurance policy through your platform |
POLICY_ADDON | A user added a supplementary product or add-on to an existing policy |
CLAIM_EXCESS | A user paid a claim excess and is receiving cashback on that amount |
OTHER | Anything 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.
| Scenario | Transaction status | What the user sees |
|---|---|---|
| User has bank details on file | PROCESSING | Widget shows the payout is on its way. No action required. |
| User has no bank details on file | ACTION_REQUIRED | Widget 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.
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.
| Scenario | Transaction status | What the user sees |
|---|---|---|
| Claim received, settlement pending | CREATED or PROCESSING | Widget shows the cashback has been recorded and is being processed. No bank details prompt is shown. |
| Settlement confirmed | PAID | Widget 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.
| Status | Meaning |
|---|---|
CREATED | Claim recorded. Payout is queued but not yet initiated. |
ACTION_REQUIRED | TO_USER only. User needs to add bank details. The widget shows a prompt. |
NEEDS_DETAILS | Similar to Action Required; specific payout details are missing or invalid. |
PROCESSING | Payout has been initiated and sent to the payment processor. |
PENDING | Claim is awaiting review or manual processing. |
PAID | Payout confirmed and complete. |
FAILED | Payout attempt failed (e.g., invalid bank details). |
CANCELLED | Payout 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.
- 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.
- 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.
- 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
| Error | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Invalid HMAC signature | Verify your signing string format: {timestamp}.POST.{path}.{bodyHash}. |
409 Conflict | Same partnerEventId used with a different body | Use a unique event ID or keep the body identical for retries. |
Insufficient Funds | Claim amount exceeds available balance | Validate the user’s balance via getBalance() before initiating the claim. |
Missing Payout Info | TO_USER mode but no bank account on file | Redirect the user to the Fruga Widget to add their payout details. |
200 OK (instead of 201) | Idempotent replay | Expected on retries. No duplicate payout is created. |
| Claim not in widget | userRef mismatch | Ensure the ID used in the claim matches the one used in the JWT assertion. |