Cashback Claim
How to notify Fruga when a user completes a purchase to trigger a cashback reward. Records a cashback entitlement for one of your users. This is a server-to-server endpoint - it must be called from your backend, signed with your Signing Secret, and must never be called from the browser.
POST
https://api.fruga.com/cashback/claim
| Property | Value |
|---|---|
| Authentication | HMAC-SHA256 request signing + JWT Session Assertion (see below) |
| Content type | application/json |
| Accept | application/json |
| Idempotency | Via partnerEventId - safe to retry |
| Caller | Partner backend only - not the browser or widget |
Request headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json |
X-Partner-Key | Yes | Your partner environment key - pk_test_... for sandbox, pk_live_... for production |
X-Partner-Timestamp | Yes | Current Unix timestamp in seconds. Requests with a timestamp more than 300 seconds from Fruga’s server time are rejected. |
X-Partner-Signature | Yes | Base64-encoded HMAC-SHA256 signature of the canonical request string. See Signing the request below. |
Request body
All fields are at the top level of a JSON object.
{
"partnerEventId": "evt_9f8a3bd2-41c0-4f2e-9bda-abc123",
"userRef": "user_123",
"amount": 18.00,
"redemptionContext": "NEW_POLICY"
}
{
"partnerEventId": "evt_9f8a3bd2-41c0-4f2e-9bda-abc123",
"userRef": "user_123",
"amount": 18.00,
"redemptionContext": "OTHER",
"redemptionContextNotes": "Renewal loyalty bonus",
"reference": {
"policyId": 4451
},
"payout": {
"method": "BANK",
"bank": {
"accountNumber": "12345678",
"sortCode": "540105"
}
}
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
partnerEventId | string | Yes | A unique identifier you generate for this event. Fruga uses this for idempotency, submitting the same value again returns the original response without creating a duplicate. Must be unique per partner. We recommend using a UUID or a stable reference from your own system such as a transaction ID. |
userRef | string | Yes | Your identifier for the user receiving the cashback. Must exactly match the userRef claim you use in the JWT assertion when that user opens the widget - this is how Fruga links the claim to the right wallet. Case-sensitive. |
amount | number | Yes | The cashback amount in GBP, as a positive decimal. For example, 18.00 represents £18.00. Must be greater than zero. |
redemptionContext | enum | Yes | The nature of the qualifying event. One of: NEW_POLICY · POLICY_ADDON · CLAIM_EXCESS · OTHER. |
redemptionContextNotes | string | Conditional | A short description of the event. Required when redemptionContext is OTHER. |
payout | object | No | Optional payout destination details. Required for direct-to-bank redemptions if bank details are not already stored. |
reference | object | No | Optional object for your own reconciliation data. Currently supports policyId. |
Payout Object
| Field | Type | Required | Description |
|---|---|---|---|
method | string | Yes | The payout method. Currently only BANK is supported. |
bank | object | Yes | Bank account details for BANK method. |
bank.accountNumber | string | Yes | 8-digit UK bank account number. |
bank.sortCode | string | Yes | 6-digit UK sort code (numeric only). |
Signing the request
Every request must carry a valid HMAC-SHA256 signature. The signature proves the request came from your backend and has not been tampered with in transit. Requests with missing, invalid, or expired signatures are rejected with 401.
How to construct the signature
1
Record the current Unix timestamp
Get the current time as a Unix timestamp in seconds. This becomes the value of your
X-Partner-Timestamp header. Fruga rejects requests where the timestamp is more than 300 seconds from its own clock - ensure your server is NTP-synced.2
Hash the raw request body
Compute a SHA-256 hash of the raw JSON body string and encode it as lowercase hex. Do not pretty-print or reformat the JSON between hashing and sending - the body must be byte-for-byte identical.
3
Build the signing string
Concatenate the four components with dots:
{timestamp}.POST./cashback/claim.{bodyHash}4
Sign and encode
Compute HMAC-SHA256 of the signing string using your Signing Secret as the key. Base64-encode the result. This is your
X-Partner-Signature header value.Signing example
import crypto from 'crypto';
interface SignedHeaders {
'Content-Type': string;
'X-Partner-Key': string;
'X-Partner-Timestamp': string;
'X-Partner-Signature': string;
}
const buildSignedHeaders = (body: string): SignedHeaders => {
const timestamp = Math.floor(Date.now() / 1000).toString();
const secret = process.env.FRUGA_SIGNING_SECRET!;
const path = '/cashback/claim';
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');
return {
'Content-Type': 'application/json',
'X-Partner-Key': process.env.FRUGA_PARTNER_KEY!,
'X-Partner-Timestamp': timestamp,
'X-Partner-Signature': signature,
};
};
// Usage
const body = JSON.stringify({
partnerEventId: 'evt_9f8a3bd2-41c0-4f2e-9bda-abc123',
userRef: 'user_123',
amount: 18.00,
redemptionContext: 'NEW_POLICY',
reference: { policyId: 4451 },
});
const response = await fetch('https://api.fruga.com/cashback/claim', {
method: 'POST',
headers: buildSignedHeaders(body),
body,
});
Axios implementation
import axios from 'axios';
import crypto from 'crypto';
interface ClaimRequest {
partnerEventId: string;
userRef: string;
amount: number;
redemptionContext: string;
reference?: { policyId: number };
}
const submitClaim = async (claim: ClaimRequest) => {
const timestamp = Math.floor(Date.now() / 1000).toString();
const secret = process.env.FRUGA_SIGNING_SECRET!;
const path = '/cashback/claim';
// Important: The body string must be identical to what is hashed
const body = JSON.stringify(claim);
const bodyHash = crypto
.createHash('sha256')
.update(body)
.digest('hex');
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.POST.${path}.${bodyHash}`)
.digest('base64');
const response = await axios.post('https://api.fruga.com/cashback/claim', body, {
headers: {
'Content-Type': 'application/json',
'X-Partner-Key': process.env.FRUGA_PARTNER_KEY!,
'X-Partner-Timestamp': timestamp,
'X-Partner-Signature': signature,
}
});
return response.data;
};
The JSON body must be serialised consistently. Fruga hashes the exact bytes it receives, so any difference in whitespace or key ordering between your hash and the sent body will cause a signature mismatch. Use compact serialisation (no extra spaces) and do not re-encode after hashing.
Responses
Success
| Status | When | Body |
|---|---|---|
201 Created | Claim recorded for the first time | {"status": "SUCCESS", "partnerEventId": "..."} |
200 OK | Idempotent replay | {"status": "SUCCESS", "partnerEventId": "..."} |
If the payout requires further user action (e.g., adding a bank account), the response may include a specific status:
{
"status": "NEEDS_DETAILS",
"message": "User must provide bank details via the widget",
"partnerEventId": "evt_123..."
}
Errors
| Status | Code | Meaning |
|---|---|---|
401 | INVALID_SIGNATURE | The X-Partner-Signature could not be verified. Check your signing string construction and that you are using the correct Signing Secret for this environment. |
401 | TIMESTAMP_EXPIRED | The X-Partner-Timestamp is more than 300 seconds from Fruga’s server time. Ensure your server clock is NTP-synced and that you are generating a fresh timestamp per request. |
401 | UNKNOWN_PARTNER | The X-Partner-Key does not match any active partner environment. Confirm you are using the correct key for sandbox or production. |
409 Conflict | IDEMPOTENCY_CONFLICT | The partnerEventId has already been used, but the request body differs from the original. You cannot modify an existing claim. Contact Fruga support if a correction is needed. |
422 | VALIDATION_ERROR | One or more fields failed validation - for example, a negative amount, an invalid redemptionContext value, or a missing redemptionContextNotes when context is OTHER. The response body includes a details array describing each failure. |
Error response body
{
"error": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "redemptionContextNotes",
"message": "Required when redemptionContext is OTHER"
}
]
}
Constraints and validation rules
| Field | Rule |
|---|---|
partnerEventId | Must be unique per partner. Max 128 characters. We recommend UUID v4 or a stable ID from your system. |
userRef | Case-sensitive. Max 255 characters. Must match the userRef in the widget JWT assertion exactly. |
amount | Must be a positive number greater than zero. Decimal values are accepted. Currency is always GBP. |
redemptionContext | Must be one of the four accepted enum values. Unknown values return 422. |
redemptionContextNotes | Required if and only if redemptionContext is OTHER. Max 255 characters. Ignored for other context values. |
Timestamp tolerance | The X-Partner-Timestamp must be within ±300 seconds of Fruga’s server time. |