Cashback Claim

How to notify Fruga when a user completes a purchase to trigger a cashback reward.

POST https://api.fruga.com/cashback/claim
PropertyValue
AuthenticationHMAC-SHA256 request signing (see below)
Content typeapplication/json
IdempotencyVia partnerEventId — safe to retry
CallerPartner backend only — not the browser or widget

Request headers

HeaderRequiredDescription
Content-TypeYesMust be application/json
X-Partner-KeyYesYour partner environment key — pk_test_... for sandbox, pk_live_... for production
X-Partner-TimestampYesCurrent Unix timestamp in seconds. Requests with a timestamp more than 300 seconds from Fruga’s server time are rejected.
X-Partner-SignatureYesBase64-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
  }
}

Fields

FieldTypeRequiredDescription
partnerEventIdstringYesA 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.
userRefstringYesYour 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.
amountnumberYesThe cashback amount in GBP, as a positive decimal. For example, 18.00 represents £18.00. Must be greater than zero.
redemptionContextenumYesThe nature of the qualifying event. One of: NEW_POLICY · POLICY_ADDON · CLAIM_EXCESS · OTHER. When OTHER is used, redemptionContextNotes is required.
redemptionContextNotesstringConditionalA short description of the event. Required when redemptionContext is OTHER. Ignored for all other context values. Max 255 characters.
referenceobjectNoOptional object for your own reconciliation data. Fruga stores this but does not act on it. Currently supports policyId (integer). Additional fields may be supported in future.

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';

const buildSignedHeaders = (body) => {
  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,
});
import hashlib, hmac, base64, time, json, os
import requests

def build_signed_headers(body: str) -> dict:
    timestamp = str(int(time.time()))
    secret    = os.environ["FRUGA_SIGNING_SECRET"].encode()
    path      = "/cashback/claim"

    body_hash = hashlib.sha256(body.encode()).hexdigest()
    signing_string = f"{timestamp}.POST.{path}.{body_hash}"
    signature = base64.b64encode(
        hmac.new(secret, signing_string.encode(), hashlib.sha256).digest()
    ).decode()

    return {
        "Content-Type":        "application/json",
        "X-Partner-Key":       os.environ["FRUGA_PARTNER_KEY"],
        "X-Partner-Timestamp": timestamp,
        "X-Partner-Signature": signature,
    }

# Usage
payload = json.dumps({
    "partnerEventId":    "evt_9f8a3bd2-41c0-4f2e-9bda-abc123",
    "userRef":           "user_123",
    "amount":            18.00,
    "redemptionContext": "NEW_POLICY",
    "reference":         { "policyId": 4451 },
}, separators=(",", ":"))  # compact — no extra whitespace

response = requests.post(
    "https://api.fruga.com/cashback/claim",
    headers=build_signed_headers(payload),
    data=payload,
)
⚠️
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

StatusWhenBody
201 CreatedClaim recorded for the first timeNo body
200 OKIdempotent replay — partnerEventId already exists with the same bodyNo body

Errors

StatusCodeMeaning
401INVALID_SIGNATUREThe X-Partner-Signature could not be verified. Check your signing string construction and that you are using the correct Signing Secret for this environment.
401TIMESTAMP_EXPIREDThe 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.
401UNKNOWN_PARTNERThe X-Partner-Key does not match any active partner environment. Confirm you are using the correct key for sandbox or production.
409 ConflictIDEMPOTENCY_CONFLICTThe 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.
422VALIDATION_ERROROne 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

FieldRule
partnerEventIdMust be unique per partner. Max 128 characters. We recommend UUID v4 or a stable ID from your system.
userRefCase-sensitive. Max 255 characters. Must match the userRef in the widget JWT assertion exactly.
amountMust be a positive number greater than zero. Decimal values are accepted. Currency is always GBP.
redemptionContextMust be one of the four accepted enum values. Unknown values return 422.
redemptionContextNotesRequired if and only if redemptionContext is OTHER. Max 255 characters. Ignored for other context values.
Timestamp toleranceThe X-Partner-Timestamp must be within ±300 seconds of Fruga’s server time.