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.
- User completes an offer
- Offermaru triggers your callback URL
- Your backend verifies and credits the reward
- 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×tamp=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"];
}