Documentation Index Fetch the complete documentation index at: https://developers.paxoslabs.com/llms.txt
Use this file to discover all available pages before exploring further.
This guide walks through the full webhook setup: creating an endpoint in the Paxos Labs Console, writing a minimal receiver, and verifying the connection with a test event.
Prerequisites
A Paxos Labs organization with admin access
A publicly reachable HTTPS server (or a tunnel like ngrok for local development)
Step 1: Create a Webhook Endpoint
Open the Paxos Labs Console
Add an endpoint
Click Add Endpoint . Enter your HTTPS URL, an optional display name, and select the event types you want to receive.
Save your signing secret
After creation, a signing secret starting with pxlwh_ is displayed once . Copy it immediately and store it in your secrets manager (e.g., AWS Secrets Manager, Vault, or .env for local development).
The signing secret is shown only at creation time. If you lose it, delete the endpoint and create a new one.
Step 2: Build a Receiver
Create a minimal HTTP server that accepts POST requests, verifies the signature, and responds with 200 OK.
import { createServer } from 'node:http'
import { createHmac , timingSafeEqual } from 'node:crypto'
const SECRET = process . env . WEBHOOK_SECRET // pxlwh_...
const PORT = process . env . PORT || 4000
const TOLERANCE_SECONDS = 300 // 5-minute replay window
function verify ( rawBody , signatureHeader , timestampHeader ) {
const timestamp = new Date ( timestampHeader )
if ( Math . abs ( Date . now () - timestamp . getTime ()) / 1000 > TOLERANCE_SECONDS ) {
throw new Error ( 'Timestamp outside tolerance' )
}
const expected = createHmac ( 'sha256' , SECRET )
. update ( ` ${ timestampHeader } . ${ rawBody } ` )
. digest ( 'hex' )
const received = Buffer . from ( signatureHeader , 'hex' )
if ( ! timingSafeEqual ( received , Buffer . from ( expected , 'hex' ))) {
throw new Error ( 'Invalid signature' )
}
}
const server = createServer (( req , res ) => {
if ( req . method !== 'POST' ) {
res . writeHead ( 405 ). end ()
return
}
const chunks = []
req . on ( 'data' , ( c ) => chunks . push ( c ))
req . on ( 'end' , () => {
const rawBody = Buffer . concat ( chunks ). toString ( 'utf-8' )
try {
verify (
rawBody ,
req . headers [ 'x-paxos-labs-signature' ],
req . headers [ 'x-paxos-labs-timestamp' ],
)
const event = JSON . parse ( rawBody )
console . log ( `Received ${ event . type } — ${ event . id } ` )
res . writeHead ( 200 ). end ( 'ok' )
} catch ( err ) {
console . error ( 'Verification failed:' , err . message )
res . writeHead ( 401 ). end ( 'unauthorized' )
}
})
})
server . listen ( PORT , () => console . log ( `Listening on : ${ PORT } ` ))
import hashlib, hmac, json, os
from datetime import datetime, timezone
from http.server import HTTPServer, BaseHTTPRequestHandler
SECRET = os.environ[ "WEBHOOK_SECRET" ].encode() # pxlwh_...
TOLERANCE = 300 # 5-minute replay window
class Handler ( BaseHTTPRequestHandler ):
def do_POST ( self ):
length = int ( self .headers.get( "Content-Length" , 0 ))
raw_body = self .rfile.read(length).decode()
sig_header = self .headers.get( "X-PAXOS-LABS-SIGNATURE" , "" )
ts_header = self .headers.get( "X-PAXOS-LABS-TIMESTAMP" , "" )
try :
self ._verify(raw_body, sig_header, ts_header)
except Exception as e:
self .send_response( 401 )
self .end_headers()
self .wfile.write( str (e).encode())
return
event = json.loads(raw_body)
print ( f "Received { event[ 'type' ] } — { event[ 'id' ] } " )
self .send_response( 200 )
self .end_headers()
self .wfile.write( b "ok" )
def _verify ( self , raw_body , sig_header , ts_header ):
ts = datetime.fromisoformat(ts_header)
now = datetime.now(timezone.utc)
if abs ((now - ts).total_seconds()) > TOLERANCE :
raise ValueError ( "Timestamp outside tolerance" )
expected = hmac.new(
SECRET , f " { ts_header } . { raw_body } " .encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig_header, expected):
raise ValueError ( "Invalid signature" )
HTTPServer(( "" , 4000 ), Handler).serve_forever()
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" encoding/json "
" fmt "
" io "
" math "
" net/http "
" os "
" time "
)
const tolerance = 300 // seconds
func verify ( body [] byte , sigHeader , tsHeader , secret string ) error {
ts , err := time . Parse ( time . RFC3339 , tsHeader )
if err != nil {
return fmt . Errorf ( "bad timestamp: %w " , err )
}
if math . Abs ( time . Since ( ts ). Seconds ()) > tolerance {
return fmt . Errorf ( "timestamp outside tolerance" )
}
mac := hmac . New ( sha256 . New , [] byte ( secret ))
fmt . Fprintf ( mac , " %s . %s " , tsHeader , body )
expected := hex . EncodeToString ( mac . Sum ( nil ))
if ! hmac . Equal ([] byte ( sigHeader ), [] byte ( expected )) {
return fmt . Errorf ( "invalid signature" )
}
return nil
}
func main () {
secret := os . Getenv ( "WEBHOOK_SECRET" )
http . HandleFunc ( "/" , func ( w http . ResponseWriter , r * http . Request ) {
body , _ := io . ReadAll ( r . Body )
if err := verify (
body ,
r . Header . Get ( "X-Paxos-Labs-Signature" ),
r . Header . Get ( "X-Paxos-Labs-Timestamp" ),
secret ,
); err != nil {
http . Error ( w , err . Error (), 401 )
return
}
var evt map [ string ] any
json . Unmarshal ( body , & evt )
fmt . Printf ( "Received %s — %s \n " , evt [ "type" ], evt [ "id" ])
w . WriteHeader ( 200 )
})
fmt . Println ( "Listening on :4000" )
http . ListenAndServe ( ":4000" , nil )
}
Step 3: Expose Your Local Server
For local development, use a tunnel so Paxos Labs can reach your machine:
Copy the generated https:// URL and use it as your webhook endpoint URL in the Paxos Labs Console.
Step 4: Send a Test Event
Back in the Paxos Labs Console, expand your endpoint row and click Send Test . Choose ping to fire a lightweight connectivity check.
Your server should log output like:
Received ping — evt_test_a1b2c3d4-e5f6-7890-abcd-ef1234567890
If the delivery succeeds, the endpoint’s delivery history shows a Success badge with the HTTP status code. You’re ready to subscribe to real events.
Step 5: Subscribe to Events
Edit your endpoint to add event types like direct_transfer.deposit.pending, direct_transfer.deposit.confirmed, or direct_transfer.deposit.failed. When those events occur in your organization, your server will receive them automatically.
Next Steps
Signature Verification Deep dive into the HMAC-SHA256 signature scheme, replay protection, and production best practices.
Event Catalog Full reference of every event type with payload schemas and field descriptions.