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.
- Install the AFFY tracking script on your site so affiliate clicks are captured and a
affy_click_idcookie 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
- 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
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
Webhook endpoint
Replace {publicId} with your Public ID from Settings → Integration.
Request format
| Field | Type | Required | Description |
|---|---|---|---|
| transactionId | string (≤ 100) | Yes | Your unique order/payment ID. Used for idempotency. |
| amount | number | Yes | Sale amount (positive). |
| currency | string (3) | Yes | ISO 4217 currency code, e.g. "USD". |
| clickId | string | One of these three | affy_click_id cookie value from the customer's browser. |
| affiliateRef | string | One of these three | Affiliate ref code (from their referral link). |
| customerEmail | string | One of these three | Customer email — used for email-based attribution. |
| customerName | string | No | Customer display name. |
| orderId | string | No | Your order ID (for reference). |
| paymentMethod | string | No | e.g. "BANK_TRANSFER", "CARD". |
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)
endResponse
| Status | Meaning |
|---|---|
| 200 OK | Conversion recorded (or already existed — idempotent). Body: { conversionId, status, message }. |
| 400 Bad Request | Missing required field or no attribution field provided. |
| 401 Unauthorized | Missing or invalid X-Affy-Signature. |
| 404 Not Found | Unknown publicId. |
| 422 Unprocessable Entity | Attribution 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.