REST · OpenAPI 3.1 · HMAC webhooks · Incremental SQL patches

Integrate EU cosmetic regulatory data into your product

Stop scraping ec.europa.eu and stop maintaining your own diff engine. BD-API ships the EU CosIng database as signed incremental SQL patches, and multi-source regulatory events (EC Publications, SCCS, Safety Gate, EUR-Lex) as HMAC-signed webhooks. No SDK lock-in, no proprietary runtime.

# Check current CosIng version
curl -X POST https://bdapi.bighubai.com/api/versions/check \
  -H "Authorization: Bearer $LICENSE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"current_version": "1.0.0"}'

# If newer version available, download the incremental patch
curl https://bdapi.bighubai.com/api/versions/1.1.0/patch \
  -H "Authorization: Bearer $LICENSE_KEY" \
  -o patch.sql

# Verify SHA-256 integrity (hash ships in response headers)
curl -sI https://bdapi.bighubai.com/api/versions/1.1.0/patch \
  -H "Authorization: Bearer $LICENSE_KEY" \
  | grep -i x-hash-sha256

# Apply inside a single PostgreSQL transaction, then confirm
psql -1 -f patch.sql && curl -X POST .../api/versions/confirm \
  -H "Authorization: Bearer $LICENSE_KEY" \
  -d '{"version":"1.1.0","success":true,"error":null}'

Full pack on request: OpenAPI 3.1 spec, code samples in curl/Node/Python/Go/PHP, webhook contracts for both channels, operational runbooks.

What you get

Four artifacts. No SDK lock-in.

BD-API ships four well-defined deliverables. Everything is HTTP, JSON or SQL — no proprietary client, no opaque binary protocol. Anything that speaks REST and PostgreSQL can integrate.

01CosIng as SQL patches

Incremental, signed, applied in one transaction

Every CosIng release ships as an idempotent patch.sql plus a changelog.json and meta.json. SHA-256 integrity hash sent in response headers (X-Hash-SHA256); apply the patch inside a PostgreSQL transaction and roll back on mismatch.

Patch bundle structure

bdapi-patch-1.2.0.zip
├── patch.sql           # idempotent SQL, runs in a single PostgreSQL transaction
├── changelog.json      # human + machine-readable diff summary
└── meta.json           # version, generated_at, sha256, row counts
02Regulatory events as webhooks

HMAC-SHA256, two parallel channels, granular per source

EC Publications (Official Journal EU) dispatch as HMAC-signed webhooks with X-BDAPI-* headers. SCCS opinions, Safety Gate alerts and EUR-Lex acts dispatch through a second channel with X-BDAPI-Watch-* headers. Same algorithm, distinct headers. Three retries with 1s / 2s / 4s exponential backoff, idempotency keys on every event. Subscribe per-source.

Inbound webhook request — both channels

POST https://your-app.example/webhooks/bdapi
# Channel 1 — EC Publications:
#   X-BDAPI-Event: ec.publication.detected
#   X-BDAPI-Timestamp: 1748016600  (unix seconds)
#   X-BDAPI-Signature: sha256=9f2c…b71d

# Channel 2 — Regulatory Watch (SCCS / Safety Gate / EUR-Lex):
#   X-BDAPI-Watch-Event: watch.safety_gate.alert.detected
#   X-BDAPI-Watch-Timestamp: 1748016600  (unix seconds)
#   X-BDAPI-Watch-Signature: sha256=9f2c…b71d
#   X-BDAPI-Watch-Delivery: 01HQZK9YBV…  (UUID per delivery)

{ "event": "watch.safety_gate.alert.detected", "tier": "premium", … }
03REST API

Versioned, JSON, OpenAPI 3.1 spec

The full surface is described by an OpenAPI 3.1 YAML document delivered as part of the integration pack. Generate a typed client with openapi-generator, oapi-codegen, openapi-typescript or any other tool that consumes OpenAPI — or stay on raw HTTP. Both paths are first-class.

OpenAPI spec excerpt

openapi: 3.1.0
info:
  title: BD-API — Contrato público para integradores
  version: 1.0.0

# Shipped as part of the integration pack on request — drop into:
#   openapi-generator generate -i 15-OPENAPI.yaml -g typescript-axios
#   oapi-codegen --package bdapi 15-OPENAPI.yaml > bdapi.gen.go
#   prism mock 15-OPENAPI.yaml   # mock server for integration tests
04License-key auth

Bearer token per installation, rotation on request

One license key per installation, sent as Authorization: Bearer $LICENSE_KEY. Keys are installation-scoped — queries filter by client_id automatically. Rotation is requested via support; the planned policy is a 24-hour overlap window (Coming Q3 2026) so you can roll deployments without a hard cutover.

Authenticated request

curl https://bdapi.bighubai.com/api/versions/check \
  -H "Authorization: Bearer $LICENSE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"current_version":"1.0.0"}'

# Key rotation: requested via support — old + new accepted
# for a 24h overlap window (Coming Q3 2026).

Sample changelog.json

{
  "from_version": "1.1.4",
  "to_version": "1.2.0",
  "generated_at": "2026-05-15T08:30:00Z",
  "sha256": "9f2c…b71d",
  "changes": {
    "added":    [{ "inci_name": "…", "annex": "VI", "cas": "…" }],
    "modified": [{ "inci_name": "…", "field": "restriction" }],
    "removed":  []
  }
}

Architecture overview

Where BD-API fits in your stack

BD-API is a single upstream component between official EU sources and your application. Two channels: a REST API you poll on your schedule, and outbound HMAC-signed webhooks for time-sensitive events. Nothing else reaches into your database.

┌─────────────────────┐                          ┌──────────────────────┐
│  EU sources         │                          │   Your product       │
│  CosIng · SCCS      │                          │   (cosmetic SW)      │
│  Safety Gate        │      pull  (REST)        │                      │
│  EUR-Lex            │ ◄─────────────────────── │   • PostgreSQL       │
└──────────┬──────────┘                          │   • Webhook endpoint │
           │                                     │   • Your domain UI   │
           │ poll (CRON, advisory-locked)        │                      │
           ▼                                     │                      │
┌─────────────────────┐      push (HMAC)         │                      │
│      BD-API         │ ───────────────────────► │                      │
│   (this service)    │                          │                      │
└─────────────────────┘                          └──────────────────────┘

Integration topology — pull on the REST side, push on the webhook side.

Pull

GET /api/versions/check

Your application polls /api/versions/check on its own schedule (recommended: daily). The response is the current CosIng version plus the SHA-256 of the patch that takes you from your last_applied_version to current. No data is downloaded until you ask for it.

Push

POST to your webhook URL

BD-API sends HMAC-SHA256 signed webhooks when a regulatory event is detected. Two channels share infrastructure: EC Publications uses X-BDAPI-* headers; Regulatory Watch (SCCS / Safety Gate / EUR-Lex) uses X-BDAPI-Watch-* headers. Three retries, 1s / 2s / 4s exponential backoff. Each Watch dispatch carries X-BDAPI-Watch-Delivery (UUID) for idempotent processing on your side.

Apply

BEGIN; \i patch.sql; COMMIT;

Patches are idempotent SQL designed to run inside a single PostgreSQL transaction. Apply on a replica first if you want — the changelog.json declares row-level impact ahead of time so you can plan downtime if any (most patches are non-blocking).

Verify

SHA-256 + HMAC on every step

Every patch carries a SHA-256 in the X-Hash-SHA256 response header; every webhook carries an HMAC-SHA256 signature over `timestamp + "." + body`. Mismatch on either side means rollback (patch) or 4xx return (webhook). Failures are observable from your end without contacting support.

Authentication and rate limits

How the wire is secured

Authentication is a single Bearer token per installation. There is no OAuth handshake, no per-request signing, no client-side cert. Operational simplicity is intentional — devs should not need a 30-page integration guide to make the first authenticated call.

01License key

Bearer token in the Authorization header

Each installation receives one license key (UUID v4). Send it as a standard Bearer token on every request. Keys are revocable on request — flipping bdapi_clients.is_active = false invalidates the key immediately.

Request header

Authorization: Bearer $LICENSE_KEY
02Rate limits

No hostile throttle today; abuse triggers manual review

/api/versions/* is designed for daily polling per installation. There is no rate limit enforced on the authenticated surface today — up to once per minute is fine for integration testing. The public /api/leads endpoint (used by this marketing site) is rate-limited per IP and per email. Adaptive per-license rate limits are on the roadmap (see code).

Rate-limit policy (Q3 2026 roadmap)

# /api/versions/* — designed for daily polling per installation.
# No throttle enforced today; up to once per minute is fine for testing.
# Heavy abusive patterns may trigger manual review by BigHubAI ops.

# Public /api/leads (used by this landing page) is rate-limited per IP
# and per email — returns HTTP 429 with Retry-After when exceeded.

# Roadmap (Coming Q3 2026): per-license-key adaptive rate limit
# with response headers X-RateLimit-Remaining and X-RateLimit-Reset.
03Rotation

Rotation via support — 24h overlap on the roadmap

Today, license-key rotation is requested via the support channel. The planned policy is self-service rotation with a 24-hour overlap window where both the old and the new key authenticate — letting you roll deployments without coordinating a hard cutover. ETA Q3 2026.

Rotation flow (Q3 2026 roadmap)

# 1. Request a new key via support (replaces the admin UI flow,
#    which is internal to BigHubAI ops).
# 2. Roll your deployment with the new $LICENSE_KEY.
# 3. Old key remains active during the overlap window.

# Roadmap (Coming Q3 2026): self-service rotation with a 24h
# overlap window where both old and new keys authenticate.
04Scope

License keys are installation-scoped

A license key authenticates exactly one installation; every query filters by client_id derived from the bearer token. For multi-tenant integrations, request one key per tenant if you need isolation, or one key with tenant attribution handled inside your application. Explicit 403 forbidden_scope responses for cross-installation attempts are on the roadmap.

Scope enforcement

GET /api/versions/check
Authorization: Bearer $LICENSE_KEY_TENANT_A

# Returns version state for installation A only — every query
# filters by client_id derived from the bearer token automatically.

# Roadmap (Coming Q3 2026): explicit 403 forbidden_scope response
# when a request tries to reach data outside its installation.

Webhook payload

What lands on your endpoint

Every event is a JSON POST with attestation headers. Two channels share infrastructure but use distinct header families: EC Publications uses X-BDAPI-* headers; the Regulatory Watch subsystem (SCCS, Safety Gate, EUR-Lex — shown below) uses X-BDAPI-Watch-* headers. The body is the canonical event document, including AI-enriched analysis when available. Failures on your end (5xx, timeout) trigger the retry policy automatically.

Sample payload — Safety Gate alert (Watch channel)

{
  "event": "watch.safety_gate.alert.analyzed",
  "event_id": "01HQZK9YBV-7M8K-3R2N-0P4S-5T6U7V8W9X0Y",
  "source_engine": "safety_gate",
  "source_id": 12345,
  "tier": "premium",
  "timestamp": "2026-05-15T08:32:11.000Z",
  "item": {
    "alert_number": "A12/01234/26",
    "report_id": "12345",
    "product_name": "Cosmetic moisturiser, brand X",
    "brand": "Brand X",
    "risk_type": "Chemical",
    "risk_level": "serious",
    "notifying_country": "DE",
    "country_of_origin": "CN",
    "source_url": "https://ec.europa.eu/safety-gate-alerts/screen/webReport/alertDetail/12345",
    "published_at": "2026-05-15T00:00:00Z",
    "detected_at": "2026-05-15T08:00:00Z"
  },
  "analysis": {
    "criticality": "CRITICAL",
    "summary_es": "Notificación A12/01234/26: producto leave-on excede el límite Annex V de MIT (0.0015 %). Estado miembro notificante: Alemania. Sin período de transición — retirada recomendada para SKUs afectados.",
    "risk_assessment": "Concentración de MIT detectada por encima del límite regulatorio en formulación de aplicación prolongada.",
    "regulatory_implications": "Annex V entry 57 — MIT prohibido en leave-on desde el Reglamento (UE) 2017/1224.",
    "cross_market_relevance": "Relevante para los 27 EM. Producto en e-commerce paneuropeo.",
    "recommended_action_for_brand_owners": "Auditar fórmulas, retirar lotes afectados, notificar PCPC nacional.",
    "needs_human_review": false,
    "confidence": 0.92,
    "model": "claude-sonnet-4-5"
  }
}

Full HTTP request (headers + body excerpt)

POST /webhooks/bdapi HTTP/1.1
Host: your-app.example
Content-Type: application/json
X-BDAPI-Watch-Event:     watch.safety_gate.alert.analyzed
X-BDAPI-Watch-Timestamp: 1747297931
X-BDAPI-Watch-Signature: sha256=9f2c4d1e8a7b3f6c2e8d1a5b4c7e9f0a2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f
X-BDAPI-Watch-Delivery:  01HQZK9YBV-7M8K-3R2N-0P4S-5T6U7V8W9X0Y
User-Agent:              BDAPI-Watch/1.0 (+https://bdapi.bighubai.com)

{ "event": "watch.safety_gate.alert.analyzed", "source_engine": "safety_gate", ... }

Attestation headers — Watch channel

X-BDAPI-Watch-Event
Full event name. Format: `watch.{engine}.{noun}.{type}` — e.g. `watch.safety_gate.alert.analyzed`. The receiver should ignore unknown event types and return 200.
X-BDAPI-Watch-Timestamp
Unix epoch in SECONDS (not milliseconds, not ISO 8601). Used to construct the signature. Reject requests older than 5 minutes to defend against replay.
X-BDAPI-Watch-Signature
HMAC-SHA256 hex digest of `timestamp + "." + raw_body`, keyed with your webhook secret. Format: `sha256=<hex>`. NOTE: the signed string is the concatenation, not the body alone.
X-BDAPI-Watch-Delivery
UUID v4 unique per delivery attempt. Use it as a unique constraint on your inbound table so retries become no-ops. Mirrored in `payload.event_id`.

Footgun

Note: BD-API signs `timestamp + "." + raw_body`, NOT the body alone. And it signs the raw bytes of the body — if your framework JSON-parses before exposing the body, capture the raw payload before parsing. Re-serialising and then hashing will not match the signature. For the EC Publications channel, swap the header names to X-BDAPI-Event / X-BDAPI-Timestamp / X-BDAPI-Signature — same algorithm, no X-BDAPI-Delivery.

Signature verification

Verify the HMAC before you trust the payload

Four reference implementations — Node, Python, Go, PHP. Each one does the same four things: detect which channel sent the request (EC Publications or Watch), enforce a 300-second replay window, recompute HMAC-SHA256 over `timestamp + "." + raw_body` with your shared secret, and compare in constant time. Drop them into your webhook handler.

verify.mjsNode.js
import crypto from 'node:crypto'

// Express: capture the raw body BEFORE JSON parsing.
// app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } }))

export function verifyBdApiSignature(req, secret) {
  // Detect channel: Watch uses X-BDAPI-Watch-*, EC Publications uses X-BDAPI-*.
  const isWatch = !!req.header('X-BDAPI-Watch-Signature')
  const prefix  = isWatch ? 'X-BDAPI-Watch-' : 'X-BDAPI-'

  const ts        = req.header(prefix + 'Timestamp') || ''
  const sigHeader = req.header(prefix + 'Signature') || ''

  // Replay protection — same window for both channels (300s).
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(ts, 10)) > 300) {
    return false
  }

  const received = sigHeader.replace(/^sha256=/, '')

  // CRITICAL: the signed message is `timestamp + "." + body`, NOT the body alone.
  const message  = ts + '.' + req.rawBody.toString('utf8')
  const expected = crypto.createHmac('sha256', secret).update(message).digest('hex')

  const a = Buffer.from(received, 'hex')
  const b = Buffer.from(expected, 'hex')
  if (a.length !== b.length) return false

  // Constant-time compare — NEVER use a === b for signatures.
  return crypto.timingSafeEqual(a, b)
}

Footgun

Never compare signatures with `==` or `bytes.Equal`. Use crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hmac.Equal (Go), or hash_equals (PHP). String/byte equality short-circuits on the first mismatched byte, which leaks the position of the differing byte through timing — enough for a determined attacker to forge a signature. And never sign the body alone — the message is timestamp concatenated with the body via a literal period.

Test license and sandbox practices

Test against real production behavior, not a stubbed mock

We do not run a separate sandbox installation. Instead, we issue a dedicated test license key that points to the same production environment with side effects attenuated — same code paths, same signing logic, same retry policy. What you exercise in test is exactly what runs in prod.

  1. 01
    Test license keyComing Q3 2026

    Same environment, attenuated side effects

    Provisioned manually on request. Authenticates identically to production keys but the test mode flag (Coming Q3 2026) sends X-BDAPI-Mode: test on outbound webhooks and skips commercial counters on feedback. Audit trail is preserved. No artificial rate limits during integration.

  2. 02
    Local receiver via tunnel

    ngrok / Cloudflare Tunnel for local webhook testing

    Expose your localhost over HTTPS with ngrok, Cloudflare Tunnel or localtunnel. Hand the temporary URL to BigHubAI for registration against your test license; real dispatches land on your laptop. Best path for the first development cycle.

  3. 03
    Replayable offline testing

    Capture, re-sign, replay against dev

    Capture a real dispatch payload from your staging receiver, regenerate the HMAC with your dev secret using the replay-firma.sh script in the pack, and POST it against your local dev. Lets you iterate on parsing and idempotency without waiting for a real upstream event.

  4. 04
    OnboardingComing Q3 2026

    Manual provisioning, no fixed trial window

    Test keys are issued during commercial discussion — there is no fixed 30-day clock. Production keys are issued separately once the integration is signed off. Production and test keys never share state at the data level — they are different rows in bdapi_clients with different is_active and (planned) is_test flags.

For developers

Common technical questions

Is there an OpenAPI spec?+

Yes. We ship an OpenAPI 3.1 spec covering every public endpoint plus the webhook payloads. It is delivered as part of the full integration pack after we review your request — drop it into Postman, Insomnia, openapi-generator, oapi-codegen or any tool that consumes OpenAPI. The full pack is sent to qualified integrators only; request access via the contact form.

Do I need an SDK or can I use raw HTTP?+

Raw HTTP is fully supported and the recommended path. No SDK lock-in. The pack ships executable code samples in curl, Node.js, Python, Go (signature verification) and PHP — drop them into your project and ship. Any HTTP client (axios, fetch, requests, httpx, Guzzle, Go net/http) works.

How do I receive webhooks during local development?+

Use ngrok, Cloudflare Tunnel or localtunnel to expose your localhost over HTTPS. For replayable offline testing, the pack includes a `replay-firma.sh` script that re-signs a captured payload with your dev secret so you can iterate without waiting for a real dispatch.

What happens if my webhook endpoint is down for hours?+

BD-API retries with exponential backoff (1s / 2s / 4s, 3 attempts) per dispatch. After full failure, the dispatch row stays in the failed pool and the next CRON tick re-evaluates it as long as `retry_count < 3`. After permanent failure, the event is queryable via the REST API so you can backfill manually. Receivers should be idempotent — the same `event_id` may arrive more than once.

How is the license key scoped?+

Each license key authenticates exactly one installation. Cross-installation queries are not addressable. For multi-tenant integrations, request one key per tenant if you need isolation at the BD-API layer, or one key with tenant attribution handled inside your application.

What about rate limits on the /api/versions/* endpoints?+

Designed for daily polling per installation. There is no hostile rate limit on this surface today — higher-frequency polling is supported (up to once per minute is fine for integration testing) but production patterns should stay close to daily, because the upstream CosIng source itself does not update faster than that.

How many regulatory sources does BD-API cover?+

Five active sources, two integration channels. CosIng is delivered as signed incremental SQL patches via REST pull. EC Publications (Official Journal EU via EC Drupal), SCCS opinions, Safety Gate alerts and EUR-Lex acts are delivered as HMAC-signed webhooks. You can subscribe per-source — you do not have to take all of them to take one. How each source fits into the regulatory monitoring playbook →

How long does a typical integration take?+

A working integration against a sandbox license key — webhook receiver verifying HMAC, daily CosIng patch cron applying inside a single PostgreSQL transaction, feedback loop closed — is 4 to 12 engineering hours with the pack we provide. Production hardening (alerting, audit logging, multi-tenant attribution) is on top of that and depends on your stack.

Do you offer a sandbox or test license?+

Yes. Test license keys are provisioned manually on request. Production and test keys point to the same production environment but the test mode flag attenuates side effects (no impact on commercial metrics, separate audit trail). We do not run a separate sandbox installation — that gives you a more honest signal that everything you test will behave identically in prod.

What happens if BD-API stops operating?+

The CosIng data you have already applied lives in your own PostgreSQL — you keep it. Patches are pure SQL with no proprietary runtime. Webhook events you received are in your own database. There is no vendor-managed runtime to retire, no SDK that stops working, no opaque binary protocol that locks you in. We give you durability by design, not by promise.

Why sign webhooks with HMAC if I'm already using HTTPS?+

HTTPS protects the transport — it prevents someone on the network from reading or tampering with the payload in transit. HMAC protects the origin — it lets your receiver prove that the payload actually came from BD-API and not from anyone who learned your endpoint URL. Without HMAC, any attacker who guesses or discovers your webhook URL can forge events. HTTPS without HMAC is encryption without authentication, and authentication is the part that matters for compliance evidence. Five-minute HMAC verification guide with code samples →

What is a timing attack and why does it matter when verifying HMAC signatures?+

A timing attack exploits the fact that a naive string comparison (== or ===) returns as soon as it finds a mismatched character. An attacker who can measure response times can deduce signature bytes one by one until they forge a valid one. The fix is constant-time comparison — crypto.timingSafeEqual in Node, hmac.compare_digest in Python, hash_equals in PHP. The cost is identical to a normal comparison; the safety gain is total. Common HMAC verification mistakes — including timing comparisons →

Request access