RecoupIQ provides business intelligence from public UK records. Nothing here constitutes financial advice, a regulated credit assessment, or a regulated activity under FSMA 2000. Evidence indicators summarise available records and are not credit decisions. ICO ZC077511. Privacy · Terms · Corrections

Skip to main content
RecoupIQ
Check a companyHow it worksUse casesPayment GatewaysPricingCompany
Sign inSign upCheck a company
Check

Developer docs · Webhooks

Verify every webhook before you trust it.

Every webhook RecoupIQ dispatches carries an X-RecoupIQ-Signature header containing an HMAC-SHA256 over the request body. Your receiver must verify it before acting on the payload. The verification scheme is identical to Stripe’s, if you already verify Stripe webhooks, the code is the same shape.

1. What the header looks like

POST /your-webhook-endpoint HTTP/1.1
Host: your.domain
Content-Type: application/json
User-Agent: RecoupIQ-Webhook-Dispatcher/2.0
X-RecoupIQ-Event: gazette.insolvency.published
X-RecoupIQ-Signature: t=1715520000,v1=8a3f9e1b...d4

{"event":"gazette.insolvency.published","timestamp":"2026-05-12T10:23:00Z","data":{...}}

Components:

  • t=<unix-seconds>, when we signed the request.
  • v1=<hex>, HMAC-SHA256 of `${t}.${rawBody}` using your subscription’s webhookSecret as the key.

2. The verification recipe

  1. Read the X-RecoupIQ-Signature header. Split on , and parse the t and v1 values.
  2. Reject the request if |now - t| > 300 seconds (replay protection).
  3. Compute HMAC-SHA256(`$${t}.${rawBody}`, webhookSecret). Use the raw bytes of the request body, do not parse and re-serialise.
  4. Constant-time compare the result to v1. Mismatch = reject.
  5. Only after both checks pass, parse the JSON body and act on the event.

3. Node.js / TypeScript receiver

import { createHmac, timingSafeEqual } from 'node:crypto';
import { Request, Response } from 'express';

const WEBHOOK_SECRET = process.env.RECOUPIQ_WEBHOOK_SECRET!;   // whsec_...
const REPLAY_WINDOW_SECONDS = 300;

export function recoupiqWebhook(req: Request, res: Response) {
  // 1. Read raw body, make sure your framework hasn't parsed it.
  //    In Express: app.use('/recoupiq', express.raw({ type: 'application/json' }));
  const rawBody = (req.body as Buffer).toString('utf8');

  // 2. Parse signature header.
  const header = String(req.header('X-RecoupIQ-Signature') ?? '');
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('=') as [string, string])
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1) {
    return res.status(400).send('malformed signature header');
  }

  // 3. Replay protection.
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - t) > REPLAY_WINDOW_SECONDS) {
    return res.status(400).send('replay window exceeded');
  }

  // 4. Recompute HMAC and constant-time compare.
  const expected = createHmac('sha256', WEBHOOK_SECRET)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(400).send('invalid signature');
  }

  // 5. Trusted, handle the event.
  const event = JSON.parse(rawBody);
  // event.event === 'gazette.insolvency.published'
  // event.data === { company_number, insolvency_practitioner, ip_number, last_day_for_claims, ... }

  // Always respond fast; do real work in a queue.
  return res.status(204).send();
}

4. Python (FastAPI) receiver

import hmac
import hashlib
import os
import time
from fastapi import FastAPI, Request, HTTPException

WEBHOOK_SECRET = os.environ["RECOUPIQ_WEBHOOK_SECRET"].encode()  # b"whsec_..."
REPLAY_WINDOW = 300

app = FastAPI()

@app.post("/recoupiq")
async def recoupiq_webhook(request: Request):
    # 1. Read raw body, do not let FastAPI parse it.
    raw = await request.body()

    # 2. Parse signature header.
    sig_header = request.headers.get("X-RecoupIQ-Signature", "")
    parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        raise HTTPException(status_code=400, detail="malformed signature header")

    # 3. Replay protection.
    if abs(int(time.time()) - t) > REPLAY_WINDOW:
        raise HTTPException(status_code=400, detail="replay window exceeded")

    # 4. Recompute HMAC and constant-time compare.
    signed_payload = f"{t}.".encode() + raw
    expected = hmac.new(WEBHOOK_SECRET, signed_payload, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        raise HTTPException(status_code=400, detail="invalid signature")

    # 5. Trusted, handle the event.
    import json
    event = json.loads(raw)
    # event["event"] == "gazette.insolvency.published"
    # event["data"]["insolvency_practitioner"] etc.
    return {"received": True}

5. Bash diagnostic (for testing only)

Useful when you receive a webhook locally and want to verify it from a shell rather than via your application:

WEBHOOK_SECRET="whsec_..."           # your subscription secret
RAW_BODY='<paste the raw request body here>'
T="<paste the t value from X-RecoupIQ-Signature>"
V1_RECEIVED="<paste the v1 value>"

V1_EXPECTED=$(printf '%s' "${T}.${RAW_BODY}" | \
  openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" -hex | \
  awk '{print $2}')

if [ "$V1_RECEIVED" = "$V1_EXPECTED" ]; then
  echo "valid"
else
  echo "INVALID"
fi

6. Common failure modes

SymptomCauseFix
All signatures failFramework parsed-and-reserialised the bodyUse the raw bytes of the request body, same bytes that went on the wire
Intermittent failuresClock drift between your server and oursNTP-sync your receiver; the 5-min window is generous but not infinite
Wrong secretSubscription was recreated; old secret invalidRecreate the subscription; new secret returned in POST response
Length-mismatch errorHex parsing dropped leading zerosCompare hex strings as fixed-length 64-char strings; or decode to bytes first

7. Where to get your webhook secret

Call POST /api/v1/watch with a webhookUrl. The response body includes data.webhookSecret, a whsec_-prefixed 64-character hex string. Shown once. Store it on your side; we never display it again. If lost, recreate the subscription to receive a new one.

8. Event types, what you receive

Every webhook carries an event field naming the signal-change type that triggered it. Subscribers can opt in to a subset by passing event_types on POST /api/v1/watch, when omitted, the subscription receives ALL event types (legacy behaviour preserved). Use the canonical event strings below verbatim.

Event typeFired when
gazette.insolvency.publishedUK Gazette insolvency notice published (Administration, CVL, CVA, winding-up).
companies_court.petition.scheduledA winding-up or administration petition for the watched company is listed on a UK Companies Court cause list. 4-6 weeks EARLIER than the Gazette.
ch.charge.registeredA new outstanding charge has been registered against the company (CA 2006 Part 25). Secured creditor sitting ahead of you in any future insolvency.
sanctions.ofsi.match_newDirector or PSC newly matches the OFSI Consolidated List. Stop-the-line under SAMLA 2018.
fca.warning.match_newCompany or director newly matches the FCA Warning List of unauthorised firms.
cdda.disqualification.match_newActive officer newly matches the CH Disqualifications register (CDDA 1986).
phoenix.six_indicator.flaggedSix-Indicator Phoenix Score newly fires for this counterparty (INSS-published methodology).
ch.officer.appointedNew active officer appointed to the company.
ch.officer.resignedActive officer has resigned from the company.
ch.psc.changedBeneficial-ownership chain has materially changed.
eccta.director_id.unverifiedDirector ID-verification status has flipped (ECCTA 2023).
sic_code.changedCompany's primary SIC code has changed on its latest CS01 filing.
hmlr.title.addedHMLR property title newly registered to the company. Recoverable-asset signal.
hmlr.title.disposedHMLR property title disposed of. Recovery signal change.
offshore.control.flaggedOffshore-control pattern flagged (HMLR OCOD ⋈ CH ROE). FATF high-risk jurisdiction watchpoint.
roe.entity.changedRegister of Overseas Entities event affecting the company.
hmrc.defaulter.match_newCompany newly appears on the HMRC published-defaulter list.
news.distress.detectedNews-distress signal detected from INSS RSS or UK press.
capital_bleed.flaggedCapital-bleed velocity has crossed the 25%/YoY threshold.
cooked_books.flaggediXBRL filings reconstruction loss above the 99th-percentile cohort threshold.
sector_stress.elevatedSector-level stress (cluster-momentum) has been classified as elevated. Watchlist-portfolio-wide event.

Every event payload carries companyNumber, companyName (when known), sourceId (idempotency key), detail (one-line plain-English description), and an optional metadata object with event-specific fields. Receivers MUST dedupe on (event, data.companyNumber, data.sourceId), re-deliveries during retry windows are normal.

Spec: /api/openapi.json (PricedRisk + watch-subscription schemas). Methodology: /methodology. Audit invitation: /audit-our-numbers.

RecoupIQ™

Check the company behind the deal.

RecoupIQ Ltd · England & Wales
Companies House 16947526
ICO ZC077511
Flat 1, 410 High Street, Lincoln, LN5 7TE

Product

How it worksPayment Gateway ClearancePricingCheck a British companyMonitor a companyUse casesNews and signalsMethodology

Company

AboutCompany directoryTeamFounderSupporters and ecosystemPartnersPress roomPress kitEagle Labs contextContactUK B2B risk patternsSector Quantum Stress IndexNDAs & UK lawShare your story

Legal

PrivacyTermsCookiesCorrections

RecoupIQ provides business intelligence derived from public UK records. Nothing on this site constitutes financial advice, a regulated credit assessment, or a regulated activity under the Financial Services and Markets Act 2000. Evidence indicators summarise available records and do not constitute a credit decision. Director network analysis is based on Companies House public filings; individuals may request review of inferences via [email protected]. Source data is published under the Open Government Licence v3.0. ICO registration ZC077511.

Microsoft
Partner
Available on Microsoft Marketplace

RecoupIQ is a member of the Microsoft AI Cloud Partner Program with an independent listing on the Microsoft commercial marketplace. RecoupIQ is not affiliated with, endorsed by, or sponsored by Microsoft. Microsoft, Azure, and the Microsoft logo are trademarks of the Microsoft group of companies.

© 2026 RecoupIQ Ltd. All rights reserved.recoup-iq.tech · [email protected]