Activepieces
Verify a RefRef webhook in an Activepieces flow, grant the Reward, and confirm it with the RefRef API.
This flow receives reward.earned, verifies the RefRef signature in a Code step, and confirms the Reward with an HTTP step. Read the overview first for the subscription and signature rules.
The flow has four steps:
- Webhook → Catch Webhook
- Code: verify the RefRef signature
- Your grant step
- HTTP → Send HTTP request: confirm the fulfillment
1. Catch the request
- Create a flow with the trigger Webhook → Catch Webhook, with authentication None.
- Copy the webhook URL and subscribe it with
fulfillmentMode: "explicit". Keep the returnedsecret(see Subscribe the URL). - Send one event so that the trigger has sample data.
Do not use the trigger's own HMAC authentication. It signs only the body, but
a RefRef signature covers <webhook-id>.<webhook-timestamp>.<body>.
2. Verify the signature
Add a Code step with these inputs:
| Input | Value |
|---|---|
headers | {{trigger['output'].headers}} |
rawBody | {{trigger['output'].rawBody}} |
body | {{trigger['output'].body}} |
secret | The endpoint secret, starting whsec_ |
Code:
import { createHmac, timingSafeEqual } from "crypto";
// Verifies the Standard Webhooks headers of a RefRef delivery and returns the
// fields that the next steps use. A request that fails the check stops the run.
export const code = async (inputs) => {
const headers = Object.fromEntries(
Object.entries(inputs.headers ?? {}).map(([name, value]) => [
name.toLowerCase(),
String(value),
]),
);
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signature = headers["webhook-signature"] ?? "";
// RefRef sends compact JSON, so the parsed body serializes to the same bytes.
const raw =
typeof inputs.rawBody === "string" && inputs.rawBody.startsWith("{")
? inputs.rawBody
: JSON.stringify(inputs.body);
if (!id || !/^\d+$/.test(timestamp ?? ""))
throw new Error("Missing webhook headers");
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
throw new Error("Old webhook timestamp");
const key = Buffer.from(inputs.secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${raw}`)
.digest();
const valid = signature.split(" ").some((part) => {
const given = Buffer.from(part.slice(3), "base64");
return (
part.startsWith("v1,") &&
given.length === expected.length &&
timingSafeEqual(given, expected)
);
});
if (!valid) throw new Error("Invalid RefRef signature");
const event = JSON.parse(raw);
const reward = event.data.reward;
return {
eventId: event.id,
projectId: event.projectId,
rewardId: reward.id,
beneficiary: reward.beneficiary.externalId,
};
};A thrown error stops the run. The timestamp check refuses a sample older than five minutes: send a fresh event before you test this step.
3. Grant the Reward
Add the steps that grant the Reward in your product. Use {{step_1['output'].rewardId}} as the idempotency key and {{step_1['output'].beneficiary}} to find the user.
4. Confirm the fulfillment
Add HTTP → Send HTTP request:
| Field | Value |
|---|---|
| Method | POST |
| URL | https://<your RefRef API>/v1/rewards/{{step_1['output'].rewardId}}/fulfillment |
| Headers | x-api-key = your Workspace API key |
| Body type | JSON |
| Body | { "projectId": "{{step_1['output'].projectId}}", "externalReference": "activepieces:{{step_1['output'].eventId}}" } |
| Failure mode | Retry on 5xx |
| Follow redirects | Off |
Publish the flow and earn a real Reward. The Reward's status becomes fulfilled, and GET /v1/rewards/{rewardId} shows the externalReference that the flow sent.