Integrations

Generic Webhook Integration

For payment systems that aren't Stripe, Paddle, or Chargebee — bank transfers, WooCommerce, Square, Braintree, or any custom checkout — send conversion events directly from your server using a signed HTTP webhook.

Before you start:
  • Install the AFFY tracking script on your site so affiliate clicks are captured and a affy_click_id cookie is set
  • Create at least one commission flow — AFFY uses it to calculate the commission amount when a conversion arrives

How it works

When a customer completes a purchase on your site, your server sends a signed JSON payload to AFFY's webhook endpoint. AFFY verifies the signature, attributes the conversion to the correct affiliate, creates a commission, and makes the customer visible in your dashboard.

Secure

Every request is verified with HMAC-SHA256 — only your server can send valid events.

Idempotent

Send the same transactionId twice and AFFY returns the existing conversion, no duplicate created.

Flexible attribution

Attribute via click ID, affiliate ref code, or customer email — in that priority order.

Setup

Get your credentials

Go to Settings → Integration in your AFFY dashboard and expand the Generic Webhooksection. You'll find:
  • Webhook URL — the endpoint your server POSTs to
  • Public ID — identifies your account in the URL path
  • Webhook Secret — 64-character hex key used to sign requests

Capture the click ID on your checkout page

After the AFFY tracking script initialises, the affiliate's click is stored in a cookie named affy_click_id. Read it on your checkout page and pass it to your server when the order is created.
// Read the click ID from the cookie
function getAffyClickId() {
  const match = document.cookie.match(/(?:^|; )affy_click_id=([^;]*)/);
  return match ? match[1] : null;
}

// Pass to your checkout handler
const clickId = getAffyClickId();

Send the webhook from your server

After a successful payment, POST to AFFY with the JSON payload signed using HMAC-SHA256. See code examples below.

Webhook endpoint

POST /api/v1/webhooks/generic/{publicId}

Replace {publicId} with your Public ID from Settings → Integration.

Request format

FieldTypeRequiredDescription
transactionIdstring (≤ 100)YesYour unique order/payment ID. Used for idempotency.
amountnumberYesSale amount (positive).
currencystring (3)YesISO 4217 currency code, e.g. "USD".
clickIdstringOne of these threeaffy_click_id cookie value from the customer's browser.
affiliateRefstringOne of these threeAffiliate ref code (from their referral link).
customerEmailstringOne of these threeCustomer email — used for email-based attribution.
customerNamestringNoCustomer display name.
orderIdstringNoYour order ID (for reference).
paymentMethodstringNoe.g. "BANK_TRANSFER", "CARD".
At least one attribution field is required: clickId, affiliateRef, or customerEmail. AFFY tries them in that order and uses the first one that resolves to an active affiliate.

Signing requests

Compute an HMAC-SHA256 digest of the raw JSON body using your Webhook Secret, then send it in the X-Affy-Signature header as sha256=<hex>.

Node.js

const crypto = require('crypto');

const WEBHOOK_SECRET = process.env.AFFY_WEBHOOK_SECRET;
const PUBLIC_ID      = process.env.AFFY_PUBLIC_ID;

async function sendConversion(order, clickId) {
  const body = JSON.stringify({
    transactionId:  order.id,
    amount:         order.total,
    currency:       order.currency,
    clickId,
    customerEmail:  order.customerEmail,
    customerName:   order.customerName,
    paymentMethod:  'BANK_TRANSFER',
  });

  const signature = 'sha256=' +
    crypto.createHmac('sha256', WEBHOOK_SECRET)
          .update(body)
          .digest('hex');

  const res = await fetch(`https://app.affy.pro/api/v1/webhooks/generic/${PUBLIC_ID}`, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json', 'X-Affy-Signature': signature },
    body,
  });

  return res.json(); // { conversionId, status, message }
}

Python

import hmac, hashlib, json, os, requests

WEBHOOK_SECRET = os.environ['AFFY_WEBHOOK_SECRET']
PUBLIC_ID      = os.environ['AFFY_PUBLIC_ID']

def send_conversion(order, click_id):
    body = json.dumps({
        'transactionId': order['id'],
        'amount':        order['total'],
        'currency':      order['currency'],
        'clickId':       click_id,
        'customerEmail': order['customer_email'],
        'paymentMethod': 'BANK_TRANSFER',
    }, separators=(',', ':'))

    sig = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(), body.encode(), hashlib.sha256
    ).hexdigest()

    r = requests.post(
        f'https://app.affy.pro/api/v1/webhooks/generic/{PUBLIC_ID}',
        data=body,
        headers={'Content-Type': 'application/json', 'X-Affy-Signature': sig},
    )
    return r.json()

PHP

<?php
$secret   = getenv('AFFY_WEBHOOK_SECRET');
$publicId = getenv('AFFY_PUBLIC_ID');

$body = json_encode([
    'transactionId' => $order['id'],
    'amount'        => $order['total'],
    'currency'      => $order['currency'],
    'clickId'       => $clickId,
    'customerEmail' => $order['email'],
    'paymentMethod' => 'BANK_TRANSFER',
]);

$sig = 'sha256=' . hash_hmac('sha256', $body, $secret);

$ch = curl_init("https://app.affy.pro/api/v1/webhooks/generic/{$publicId}");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        "X-Affy-Signature: {$sig}",
    ],
]);
$result = json_decode(curl_exec($ch), true);

Ruby

require 'net/http'
require 'openssl'
require 'json'

secret    = ENV['AFFY_WEBHOOK_SECRET']
public_id = ENV['AFFY_PUBLIC_ID']

body = JSON.generate(
  transactionId: order[:id],
  amount:        order[:total],
  currency:      order[:currency],
  clickId:       click_id,
  customerEmail: order[:email],
  paymentMethod: 'BANK_TRANSFER'
)

sig = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, body)

uri = URI("https://app.affy.pro/api/v1/webhooks/generic/#{public_id}")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'X-Affy-Signature' => sig)
  req.body = body
  http.request(req)
end

Response

StatusMeaning
200 OKConversion recorded (or already existed — idempotent). Body: { conversionId, status, message }.
400 Bad RequestMissing required field or no attribution field provided.
401 UnauthorizedMissing or invalid X-Affy-Signature.
404 Not FoundUnknown publicId.
422 Unprocessable EntityAttribution failed — no active affiliate could be resolved from the provided fields.

Rotating your secret

Go to Settings → Integration → Generic Webhook and click Rotate Secret. A new 64-character secret is generated immediately. Update your server's environment variable before rotating — any in-flight requests signed with the old secret will be rejected as soon as the rotation completes.