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:

  1. Webhook → Catch Webhook
  2. Code: verify the RefRef signature
  3. Your grant step
  4. HTTP → Send HTTP request: confirm the fulfillment

1. Catch the request

  1. Create a flow with the trigger Webhook → Catch Webhook, with authentication None.
  2. Copy the webhook URL and subscribe it with fulfillmentMode: "explicit". Keep the returned secret (see Subscribe the URL).
  3. 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:

InputValue
headers{{trigger['output'].headers}}
rawBody{{trigger['output'].rawBody}}
body{{trigger['output'].body}}
secretThe 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:

FieldValue
MethodPOST
URLhttps://<your RefRef API>/v1/rewards/{{step_1['output'].rewardId}}/fulfillment
Headersx-api-key = your Workspace API key
Body typeJSON
Body{ "projectId": "{{step_1['output'].projectId}}", "externalReference": "activepieces:{{step_1['output'].eventId}}" }
Failure modeRetry on 5xx
Follow redirectsOff

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.

On this page