Paytrie Developer Documentation

Webhooks

Receive real-time notifications for user and transaction events

Webhooks allow you to receive real-time notifications when events occur in your Paytrie integration. Instead of polling the API for updates, webhooks push event data to your server as events happen.

Available webhooks

WebhookTrigger
User VerifiedWhen a user completes KYC verification
Transaction InitiatedWhen a new transaction is created
Transaction CompleteWhen a transaction finishes processing
Transaction Status UpdateWhen a transaction status changes

Setting up webhooks

To receive webhooks, you need to:

  1. Create POST endpoints on your server to receive webhook payloads
  2. Register your webhook URLs using the API (see below)
  3. Verify endpoints using a secret signing key

You can configure separate URLs for each webhook type, or use a single endpoint that handles all webhook types.

Get current webhook configuration

curl "https://api.paytrie.com/webhooks" \
  -H "x-api-key: your-api-key"

API Reference: Get Webhooks

View complete request parameters and response schema

Update webhook configuration

curl -X PATCH "https://api.paytrie.com/webhooks" \
  -H "x-api-key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "verifiedEmailEnabled": true,
    "verifiedEmailUrl": "https://your-server.com/webhooks/user-verified",
    "transactionInitiatedEnabled": true,
    "transactionInitiatedUrl": "https://your-server.com/webhooks/tx-initiated",
    "transactionCompleteEnabled": true,
    "transactionCompleteUrl": "https://your-server.com/webhooks/tx-complete",
    "transactionStatusUpdateEnabled": true,
    "transactionStatusUpdateUrl": "https://your-server.com/webhooks/tx-status"
  }'

API Reference: Update Webhooks

View complete request parameters and response schema

Your webhook endpoints must be publicly accessible via HTTPS and respond with a 2xx status code to acknowledge receipt.

Webhook payloads

All webhooks are sent as POST requests with a JSON body.

User verified

Triggered when a user completes KYC verification and is ready to transact.

{
  "email": "user@example.com",
  "status": "verified"
}

Transaction initiated

Triggered when a new transaction is created.

{
  "email": "user@example.com",
  "txId": "3943bb00-1551-4f1d-bf32-2d82608bc15e",
  "status": "pending request money transfer",
  "wallet": "0x1234567890abcdef1234567890abcdef12345678",
  "paymentId": null,
  "leftSideLabel": "CAD",
  "leftSideValue": 100.00,
  "rightSideLabel": "USDC-ETH",
  "rightSideValue": 72.50,
  "interacSecurityAnswer": null,
  "externalSessionId": "partner-session-abc123"
}

Transaction complete

Triggered when a transaction completes successfully.

{
  "email": "user@example.com",
  "txId": "3943bb00-1551-4f1d-bf32-2d82608bc15e",
  "status": "complete",
  "wallet": "0x1234567890abcdef1234567890abcdef12345678",
  "paymentId": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "leftSideLabel": "CAD",
  "leftSideValue": 100.00,
  "rightSideLabel": "USDC-ETH",
  "rightSideValue": 72.50,
  "interacSecurityAnswer": "maple2024",
  "externalSessionId": "partner-session-abc123"
}

Transaction status update

Triggered whenever a transaction status changes. This provides more granular tracking than the initiated/complete webhooks. See Transaction statuses for all possible values.

{
  "email": "user@example.com",
  "txId": "3943bb00-1551-4f1d-bf32-2d82608bc15e",
  "status": "processing request money transfer",
  "wallet": "0x1234567890abcdef1234567890abcdef12345678",
  "paymentId": null,
  "leftSideLabel": "CAD",
  "leftSideValue": 100.00,
  "rightSideLabel": "USDC-ETH",
  "rightSideValue": 72.50,
  "interacSecurityAnswer": null,
  "externalSessionId": "partner-session-abc123"
}

Payload fields

User verified webhook

FieldTypeDescription
emailstringThe user's registered email address
statusstringThe verification status ("verified")

Transaction webhooks

The following fields are included in all transaction webhooks (Initiated, Complete, Status Update):

FieldTypeDescription
emailstringThe user's registered email address
txIdstringThe unique transaction ID
statusstringThe current transaction status
walletstringThe user's wallet address for the transaction
paymentIdstring | nullThe blockchain transaction hash (null if not yet on chain)
leftSideLabelstringThe currency being sent (e.g., "CAD" for buy, "USDC-ETH" for sell)
leftSideValuenumberThe amount being sent
rightSideLabelstringThe currency being received (e.g., "USDC-ETH" for buy, "CAD" for sell)
rightSideValuenumberThe amount being received
interacSecurityAnswerstring | nullThe Interac e-Transfer security answer (null if autodeposit is enabled)
externalSessionIdstring | nullYour custom session identifier passed when creating the transaction (null if not provided)

Webhook security/verification

All webhooks are signed with a secret key unique to your integration. This allows you to verify that webhooks are genuinely from Paytrie and secure.

How it works

Each webhook request includes two headers:

HeaderDescription
X-Paytrie-TimestampUnix timestamp (seconds) when the webhook was sent
X-Paytrie-SignatureHMAC-SHA256 signature in format v1=<signature>

The signature is computed as:

HMAC-SHA256(signing_secret, timestamp + "." + payload)

Generating your signing secret

To generate or rotate your webhook signing secret, call the signing secret endpoint with your api key:

curl -X POST "https://api.paytrie.com/webhook-signing-secret" \
  -H "x-api-key: your-api-key"

Response:

{
  "status": "success",
  "signingSecret": "whsec_..."
}

Store your signing secret securely. It will only be shown once when generated. If you lose it, you'll need to rotate to a new secret.

Verifying webhook signatures

When you receive a webhook, verify its authenticity by computing the expected signature and comparing it to the one in the header.

Always use the raw request body when verifying signatures. The cryptographic signature is sensitive to even the slightest change—if your framework parses the JSON and then re-stringifies it, verification will fail. The examples below show the correct approach: express.raw() with req.body.toString() in JS/TS, request.get_data(as_text=True) in Python, io.ReadAll(r.Body) in Go, and request.raw_post in Rails.

import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  // Remove 'v1=' prefix from signature
  const receivedSignature = signature.replace('v1=', '');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

app.post('/webhooks/paytrie', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-paytrie-signature'];
  const timestamp = req.headers['x-paytrie-timestamp'];
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, timestamp, process.env.PAYTRIE_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook
  const event = JSON.parse(payload);
  console.log('Received webhook:', event);

  res.status(200).send('OK');
});
import hmac
import hashlib
from flask import Flask, request

def verify_webhook_signature(payload: str, signature: str, timestamp: str, secret: str) -> bool:
    expected_signature = hmac.new(
        secret.encode(),
        f"{timestamp}.{payload}".encode(),
        hashlib.sha256
    ).hexdigest()

    # Remove 'v1=' prefix from signature
    received_signature = signature.replace('v1=', '')

    return hmac.compare_digest(expected_signature, received_signature)

@app.route('/webhooks/paytrie', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Paytrie-Signature')
    timestamp = request.headers.get('X-Paytrie-Timestamp')
    payload = request.get_data(as_text=True)

    if not verify_webhook_signature(payload, signature, timestamp, PAYTRIE_WEBHOOK_SECRET):
        return 'Invalid signature', 401

    # Process the webhook
    event = request.get_json()
    print('Received webhook:', event)

    return 'OK', 200
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"strings"
)

func verifyWebhookSignature(payload, signature, timestamp, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "." + payload))
	expectedSignature := hex.EncodeToString(mac.Sum(nil))

	// Remove 'v1=' prefix from signature
	receivedSignature := strings.TrimPrefix(signature, "v1=")

	return hmac.Equal([]byte(expectedSignature), []byte(receivedSignature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	signature := r.Header.Get("X-Paytrie-Signature")
	timestamp := r.Header.Get("X-Paytrie-Timestamp")

	body, _ := io.ReadAll(r.Body)
	payload := string(body)

	if !verifyWebhookSignature(payload, signature, timestamp, paytrieWebhookSecret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	// Process the webhook
	w.WriteHeader(http.StatusOK)
}
require 'openssl'

def verify_webhook_signature(payload, signature, timestamp, secret)
  expected_signature = OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{payload}")

  # Remove 'v1=' prefix from signature
  received_signature = signature.sub('v1=', '')

  ActiveSupport::SecurityUtils.secure_compare(expected_signature, received_signature)
end

# Rails example
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def paytrie
    signature = request.headers['X-Paytrie-Signature']
    timestamp = request.headers['X-Paytrie-Timestamp']
    payload = request.raw_post

    unless verify_webhook_signature(payload, signature, timestamp, ENV['PAYTRIE_WEBHOOK_SECRET'])
      return head :unauthorized
    end

    # Process the webhook
    event = JSON.parse(payload)
    Rails.logger.info("Received webhook: #{event}")

    head :ok
  end
end

Always use constant-time comparison functions (like crypto.timingSafeEqual or hmac.compare_digest) to prevent timing attacks.

Preventing replay attacks

To protect against replay attacks, check that the timestamp is recent:

# typescript example
const WEBHOOK_TOLERANCE_SECONDS = 300; // 5 minutes

function isTimestampValid(timestamp: string): boolean {
  const webhookTime = parseInt(timestamp, 10);
  const currentTime = Math.floor(Date.now() / 1000);
  return Math.abs(currentTime - webhookTime) <= WEBHOOK_TOLERANCE_SECONDS;
}

Best practices

Respond quickly

Return a 2xx response immediately, then process the webhook asynchronously

Verify signatures

Always verify webhook signatures before processing to ensure authenticity

Check timestamps

Reject webhooks with timestamps too far in the past to prevent replay attacks

Log payloads

Log all webhook payloads for debugging and auditing

Webhooks are sent once and not retried. Ensure your endpoint is reliable and returns a 2xx status code promptly.

On this page