> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.fygaro.com/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Payment Button - Hook


## Custom payment buttons offer the ability to send notifications when a successful payment is made, this is achieved by configuring a hook.

### Activate a *Hook* for a Payment Button

Activating a Hook takes only 2 steps:

1. Define the default API key that will be used for signing each Hook request and will secure the information been shared
2. Setup a URL to the one we will send out the notifications.

To use the Hook you must first create a Dynamic Payment Button: [View tutorial](https://help.fygaro.com/en-us/article/payment-button-tck05y/)

|| To make tests you can use our [Postman Project](https://www.postman.com/orbital-module-cosmonaut-25453527/payment-button-sandbox/collection/3vxuy6p/fygaro-webhooks?action=share&creator=21282867).

### 1. Define the API Key credentials for Hooks

To define the credentials head over to **Settings** > **API Credentials**

![](https://storage.crisp.chat/users/helpdesk/website/4781636965b6c000/4d3dca02-accd-47bc-b198-8470ea_1oaq6j.png)

In the example above we can see a dropdown list of the options from where we will choose the credential that we will use as default for hooks.

### 2. Setup your systems URL

The second step is to setup the path to which requests will be made each time successful payments are received on a custom payment button. Some considerations to keep in mind:  
  
1. It must be a public path (accessible from the Internet).
2. You must have a valid security certificate (path must use HTTPS).
  
To configure the URL, simply go into the settings of the payment button(s) in which you want to activate the Hook, click on edit and then on "Advanced Options".  
  
Once in the Advanced Options you will find a field called "Hook URL" where you can place the route.

![](https://storage.crisp.chat/users/helpdesk/website/7a2107247c6ff400/fdbfdd41-33e4-4168-ace9-f89522_pu19li.png)


## Signature Validation
### 1. Official helper libraries

Prefer using our maintained helpers:

| Runtime | Package | Docs & Install |
| ---- |
| **Python ≥ 3.8** | `fygaro-webhook` | [`pypi.org/project/fygaro-webhook`](https://pypi.org/project/fygaro-webhook/) |
| **Node ≥ 16** | `@fygaro/webhook` | [`npmjs.com/package/@fygaro/webhook`](https://www.npmjs.com/package/@fygaro/webhook) |

> **TL;DR:** Install the package, create a validator with your shared secret(s), call `verify_signature`, and process the JSON only if it returns `True` (Python) or `true` (JS).

---

### 2. DIY algorithm (for every other language)

If a helper isn't available, implement these steps:

1. **Read** the raw request body (bytes).
2. **Parse** the `Fygaro-Signature` header → extract timestamp `t=` and every `v1=` hash.
3. **Parse** `Fygaro-Key-ID` to know which secret to use.
4. **(Recommended)** reject if `abs(now - t) > 300 s` (basic replay protection).
5. **Re-create** the message exactly as Fygaro did:

```
   message = t + "." + raw_body
```

6. **Compute** `HMAC-SHA-256(secret, message)` → hex.
7. **Constant-time compare** the result against **each** `v1`; accept if any match.
8. On success, **respond HTTP 200** and continue processing the JSON payload; otherwise return 4xx.

---

### 3. Language-specific examples

#### PHP ≥ 7.4 (framework-agnostic)

```php
<?php
// 1. Map Fygaro-Key-ID → shared secret (raw string)
$secrets = [
    '1234abcd' => 'your-secret-here',
];

// 2. Read raw body & headers
$rawBody = file_get_contents('php://input') ?: '';
$sigHdr  = $_SERVER['HTTP_FYGARO_SIGNATURE'] ?? '';
$keyId   = $_SERVER['HTTP_FYGARO_KEY_ID']    ?? '';

if ($sigHdr === '' || $keyId === '') {
    http_response_code(400);
    exit('Missing Fygaro headers');
}

// 3. Parse Fygaro-Signature  (t=...,v1=...,v1=...)
$timestamp = null;
$hashes    = [];
foreach (explode(',', $sigHdr) as $part) {
    [$k, $v] = array_map('trim', explode('=', $part, 2));
    if ($k === 't')  { $timestamp = $v; }
    if ($k === 'v1') { $hashes[]  = $v; }
}
if (!$timestamp || !$hashes || !isset($secrets[$keyId])) {
    http_response_code(400);
    exit('Malformed signature header');
}

// 4. Optional replay check (±5 min window)
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(400);
    exit('Stale timestamp');
}

// 5. Compute expected HMAC
$secret   = $secrets[$keyId];
$message  = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $message, $secret);

// 6. Validate: any v1 must match
$valid = false;
foreach ($hashes as $h) {
    if (hash_equals($expected, $h)) {
        $valid = true;
        break;
    }
}
if (!$valid) {
    http_response_code(400);
    exit('Invalid signature');
}

// 7. Signature verified → process payload
$data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);

/* …your business logic… */

// MUST reply with HTTP 200 so Fygaro marks the hook successful
http_response_code(200);
echo 'OK';
```

*(For C#, Ruby, Go, etc., swap in equivalent header/body logic and HMAC calls.)*

---

### 4. Best practices

* **Store secrets securely** (environment variables, vaults).
* **Rotate secrets** smoothly: keep both old & new until traffic shows only new hashes.
* **Always** use constant-time comparison (`hash_equals`, `timingSafeEqual`, etc.).
* **Never** log secrets or full HMACs—log only failure reasons and request IDs.
* **Respond HTTP 200** quickly on success; 200 is treated as “delivered” by Fygaro.

---

## Payment Button Webhook
### 1. Webhook **payload** (`POST` body)

All monetary values are **strings** with two decimals.

```jsonc
{
  "transactionId": "08d7360a-fc4b-46ad-a513-0a3d3fd3771c",
  "reference": "ORDER-98765",
  "customReference": "INV-2025-0420",
  "authCode": "A12345",
  "currency": "USD",
  "amount": "59.99",
  "createdAt": "2025-06-20T14:32:07Z",

  // ↓ The next three blocks are OPTIONAL; they are sent only when data exists.
  "card": {
    "last4": "4242",
    "expMonth": 12,
    "expYear": 2030,
    "brand": "Visa"
  },

  "client": {
    "email": "jane.doe@example.com",
    "phone": "15551234567"
  },

  "billing": {
    "country":  { "name": "United States", "code": "US" },
    "state":    { "name": "California" },
    "city":     { "name": "Los Angeles" },
    "locality": { "name": "Hollywood" },
    "address":  "123 Main St Apt 4B",
    "postal_code": "90028"
  },

  "gratuity_amount": "5.00",   // Present only when tips are captured

  "jwt": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzRhYmNkIn0…"   // LEGACY; will be removed
}
```

### Field reference

| Field | Type | Description |
| ---- |
| `transactionId` | string (UUID) | Fygaro’s immutable payment transaction ID. |
| `reference` | string | Payment reference. |
| `customReference` | string \ | null | Reference code set by you or your integration. |
| `authCode` | string \ | null | Authorization code from issuer. |
| `currency` | string | ISO 4217 code (e.g., `USD`). |
| `amount` | string | Captured amount (`"59.99"`). |
| `createdAt` | ISO-8601 string | UTC timestamp when the payment was created. |
| `gratuity_amount` | string \ | absent | Captured tip amount when gratuity is enabled. |
| `card` | object \ | absent | Payment's card data. |
| `client` | object \ | absent | Shopper's contact data. |
| `billing` | object \ | absent | Structured billing address. |
| `jwt` | string | **Deprecated.** Present for backward compatibility only. |

> **Heads-up:** begin migrating away from the `jwt` field now; future API versions will omit it.

#### `card` object

| Field | Type | Description |
| ---- |
| `last4` | string \ | null | The four right-most digits of the card PAN. |
| `expMonth` | integer \ | null | Expiration month **1-12**. `null` if unavailable. |
| `expYear` | integer \ | null | Full four-digit expiration year (e.g., `2030`). `null` if unavailable. |
| `brand` | string |  | One of: `Visa`, `MasterCard`, `American Express`, `Discover Card`, `Diners Club`, `jcb`, `unionpay`, `maestro`, or `unknown`. |
| `walletProvider ` | string \ | null | The name of the Digital Wallet used if any. |


#### `client` object

| Field | Type | Description |
| ---- |
| `name` | string | Shopper’s name. |
| `email` | string \ | absent | Shopper’s email |
| `phone` | string \ | absent | Shopper’s phone, if supplied during checkout. |

#### `billing` object

| Field | Type | Description |
| ---- |
| country.name | string \ | null | Country’s full name. |
| `country.code` | string \ | null | ISO 3166-1 alpha-2 code (`"CR"`). |
| state.name | string \ | null | State / province / region. |
| city.name | string \ | null | City / canton. |
| locality.name | string \ | null | Neighborhood / district. |
| `address` | string \ | null | Street address. |
| `postal_code` | string \ | null | Postal / ZIP code. |

---

Need help? → support@fygaro.com