OM
Offermaru
Publisher Area

Server to Server

S2S callbacks

Offermaru notifies your backend when users complete offers. This document describes the callback format, headers, and how to verify callbacks using HMAC signatures.

Overview

When a user completes an offer, Offermaru sends a server-to-server HTTP GET request to your configured callback URL. Your server should verify the request, credit the user, and return HTTP 200.

  1. User completes an offer
  2. Offermaru triggers your callback URL
  3. Your backend verifies and credits the reward
  4. You return HTTP 200

Callback URL

Configure your callback URL in App Settings → S2S Callback URL.

https://yourdomain.com/callback?
user_id={user_id}&
user_reward={user_reward}&
offer_id={offer_id}&
offer_name={offer_name}&
transaction_id={transaction_id}&
publisher_payout={publisher_payout}&
timestamp={timestamp}
        

Offermaru replaces placeholders like {user_id} with real values before sending the request.

Supported placeholders

Placeholder Type Description
{user_id} string User identifier from your original request (Offerwall or API).
{user_reward} number Amount of your in-app currency you should give the user.
{offer_id} string ID of the completed offer.
{offer_name} string Name of the completed offer.
{transaction_id} string Unique transaction identifier. Use as an idempotency key to avoid duplicates.
{publisher_payout} number Your payout in cents for this completion.
{timestamp} number Timestamp of completion (milliseconds elapsed since the epoch).

Code example

Below is an example of S2S callback handler. Adapt it according to your requirements.

Node.js

// Express callback handler
app.get("/offermaru/callback", async (req, res) => {
  const {
    user_id,
    offer_id,
    offer_name,
    transaction_id,
  } = req.query;

  const user_reward = parseFloat(req.query.user_reward || "0");
  const publisher_payout = parseFloat(req.query.publisher_payout || "0");
  const timestamp = parseInt(req.query.timestamp || "0", 10);

  // 1. Verify S2S signature (recommended - see below)
  // if (!verifySignature(req.query, req.headers, S2S_SECRET)) {
  //   return res.status(401).send("Invalid signature");
  // }

  // 2. Check if transaction_id already exists
  // 3. Credit user_reward to user balance
  // 4. Persist transaction

  return res.status(200).send("OK");
});
            

S2S authentication & security

Offermaru secures server-to-server callbacks using HMAC-SHA256 signatures. This allows your backend to verify that a callback genuinely originated from Offermaru and was not modified in transit.

Authentication is automatically enabled once you configure an S2S Secret for your app.

Callback headers

When authentication is enabled, every callback request includes:

Header Description
X-Offermaru-Signature HMAC-SHA256 signature of the canonical payload (hex encoded)
X-Offermaru-Timestamp Same timestamp value as the timestamp query parameter
X-Offermaru-App-Id App ID that generated the callback

Parameters used for signing

The signature is generated from a fixed subset of query parameters. Only the parameters listed below are used. All other query parameters (such as offer_name) are ignored for signature purposes.

  • transaction_id
  • user_id
  • offer_id
  • user_reward
  • publisher_payout
  • timestamp

Canonical payload construction

Offermaru constructs a canonical base string using the parameters above:

  • Parameters are sorted alphabetically by key
  • Each pair is formatted as key=value
  • Pairs are joined using &
  • Values are used exactly as received (no URL encoding)

Example canonical base string:

offer_id=abc123&publisher_payout=250&timestamp=1719859200000&transaction_id=tx_987654&user_id=user_42&user_reward=100
  

Signature generation

The canonical base string is signed using your app’s S2S secret:

  • Algorithm: HMAC-SHA256
  • Secret key: your app’s S2S secret
  • Output format: lowercase hexadecimal string
  • Result is sent in the X-Offermaru-Signature header

Security notes

  • Always reject callbacks with missing or invalid signatures
  • Use constant-time comparison when checking signatures
  • Ensure transaction_id is processed only once
  • Optionally reject callbacks with old timestamps (replay protection)
  • Never expose your S2S secret in client-side code

Signature verification example (Node.js)

const crypto = require("crypto");

function verifySignature(query, headers, secret) {
  const payload = {
    transaction_id: query.transaction_id,
    user_id: query.user_id,
    offer_id: query.offer_id,
    user_reward: query.user_reward,
    publisher_payout: query.publisher_payout,
    timestamp: query.timestamp,
  };

  const baseString = Object.keys(payload)
    .sort()
    .map(k => `${k}=${payload[k]}`)
    .join("&");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(baseString)
    .digest("hex");

  return expected === headers["x-offermaru-signature"];
}