Reference snippets and integration helpers.
Three pieces engineering teams want before they start integrating: webhook signature verification, an API call example, and a sandbox seed command so the integration has data to chew on. Snippets in Node.js, Python, and Ruby.
HMAC-SHA256 with replay defense.
Every webhook from Attestyx carriesX-JIL-SignatureandX-JIL-Timestampheaders.
ComputeHMAC-SHA256(secret, timestamp + "." + raw_body)and compare with timing-safe equality.
Reject anything older than 300 seconds.
Node.js (Express)
// Node.js - verify a Attestyx webhook signature
import crypto from 'crypto';
import express from 'express';
const app = express();
const SIGNING_SECRET = process.env.GRANTSPROOF_WEBHOOK_SECRET; // from /portal/settings
// Capture the raw body BEFORE express.json() so the signature
// matches the exact bytes we signed.
app.post('/webhooks/attestyx',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header('X-JIL-Signature');
const timestamp = req.header('X-JIL-Timestamp');
if (!signature || !timestamp) return res.status(401).send('missing headers');
// Reject events older than 5 minutes (replay defense)
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) return res.status(401).send('stale timestamp');
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(timestamp + '.' + req.body.toString('utf8'))
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send('signature mismatch');
}
const event = JSON.parse(req.body.toString('utf8'));
// process event...
res.status(200).send('ok');
});Python (Flask)
# Python (Flask) - verify a Attestyx webhook signature
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SIGNING_SECRET = os.environ['GRANTSPROOF_WEBHOOK_SECRET'].encode()
@app.post('/webhooks/attestyx')
def webhook():
signature = request.headers.get('X-JIL-Signature')
timestamp = request.headers.get('X-JIL-Timestamp')
if not signature or not timestamp:
abort(401)
if abs(time.time() - int(timestamp)) > 300:
abort(401) # stale
body = request.get_data() # raw bytes BEFORE JSON parse
payload = f'{timestamp}.'.encode() + body
expected = hmac.new(SIGNING_SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
event = request.get_json()
# process event...
return ('ok', 200)Ruby (Rails)
# Ruby (Rails) - verify a Attestyx webhook signature
require 'openssl'
class AttestyxWebhooksController < ActionController::API
SIGNING_SECRET = ENV.fetch('GRANTSPROOF_WEBHOOK_SECRET')
def create
signature = request.headers['X-JIL-Signature']
timestamp = request.headers['X-JIL-Timestamp']
head :unauthorized and return unless signature && timestamp
head :unauthorized and return if (Time.now.to_i - timestamp.to_i).abs > 300
body = request.raw_post # before JSON parse
payload = "#{timestamp}.#{body}"
expected = OpenSSL::HMAC.hexdigest('sha256', SIGNING_SECRET, payload)
head :unauthorized and return unless ActiveSupport::SecurityUtils
.secure_compare(expected, signature)
event = JSON.parse(body)
# process event...
head :ok
end
endIssue an attestation.
Authentication is theX-API-Key header. Issue a sandbox key from /portal/api-keys; switch to a live key after the MSA + DPA are signed.
// Node.js - issue an attestation
const r = await fetch('https://attestyx.com/api/v1/attestations', {
method: 'POST',
headers: {
'X-API-Key': process.env.GRANTSPROOF_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: {
legal_name: 'Acme Charitable Trust',
ein: '12-3456789',
jurisdiction: 'us'
},
payment: {
amount_cents: 50_000_00,
currency: 'USD',
program_code: 'education-rural-2026',
purpose: 'Q2 milestone'
}
})
});
const att = await r.json();
console.log(att.decision, att.attestation_id);Demo data, one curl.
POST /v1/sandbox/seed populates your tenant with 8 sample attestations across 5 jurisdictions (6 YES, 1 REVIEW, 1 NO with hard blocks) plus 6 corresponding disbursements. Idempotent - re-calling returns the existing seed_id rather than duplicating.
# Seed your sandbox tenant with 8 demo attestations + 6 disbursements curl -X POST https://attestyx.com/api/v1/sandbox/seed \ -H "X-API-Key: $GRANTSPROOF_API_KEY" # Then exercise the integration flow against seeded data: curl https://attestyx.com/api/v1/reports/quarterly \ -H "X-API-Key: $GRANTSPROOF_API_KEY"
Stuck on something?
We sit on the call with your engineers if it helps. No NDA needed for technical discovery.
Schedule integration help →