Gareth Wilson Gareth Wilson

Guide to FullEnrich Webhooks: Features and Best Practices

Published


FullEnrich webhooks deliver the results of contact enrichment. FullEnrich finds work emails, personal emails and phone numbers for the contacts you send it, and because that takes 30 to 90 seconds per contact, the API returns straight away with an ID and pushes the results to your URL when they are ready. If you enrich leads from a CRM or your own product, webhooks are how those results come back.

This guide covers how FullEnrich callbacks are requested and signed, what their bodies contain, the retry behaviour you need to plan around, and the practices that keep a FullEnrich receiver reliable.

What are FullEnrich webhooks?

FullEnrich doesn't have webhook subscriptions or event types. Instead, you put callback URLs in each enrichment or reverse email lookup request:

  • webhook_url receives one POST when the whole batch is finished, runs out of credits, or is cancelled.
  • webhook_events.contact_finished receives one POST for each contact as soon as it is enriched, without waiting for the rest of the batch.

You can set either or both. The body is the same object you would get from the Get Enrichment Result endpoint.

Every callback has an X-Signature-SHA1 header: the lowercase hex HMAC-SHA1 of the raw request body. The key is your FullEnrich API key, the same one you use to call the API. There is no separate webhook secret.

FullEnrich webhook features

FeatureDetails
ConfigurationPer request: webhook_url and webhook_events.contact_finished in the request body
CallbacksBatch completion, and optionally one per finished contact
Event typesNone. The body's status gives the job state
Signature headerX-Signature-SHA1
SchemeHMAC-SHA1, lowercase hex, no prefix
Signed contentThe raw request body, nothing else
KeyYour FullEnrich API key
Correlationcustom object on each contact, echoed back; the job id
Success responseAny 2xx
RetriesEvery minute, up to 5 times
Delivery logsFullEnrich's team can check them on request

FullEnrich doesn't state a response timeout or publish source IPs.

Payloads and statuses

A callback body looks like the Get Enrichment Result response: the job's id and name, its status, the credit cost, and a data array of contacts. The status tells you which callback you are looking at:

statusCallbackMeaning
FINISHEDwebhook_urlThe whole batch completed
CANCELEDwebhook_urlThe batch was cancelled
CREDITS_INSUFFICIENTwebhook_urlThe batch ran out of credits
IN_PROGRESScontact_finishedOne contact is done, and the batch is still running

A per-contact callback carries exactly one contact in data. Here is the shape from FullEnrich's example, trimmed:

{
  "id": "<string>",
  "name": "<string>",
  "status": "IN_PROGRESS",
  "cost": { "credits": 10 },
  "data": [
    {
      "input": {
        "first_name": "<string>",
        "last_name": "<string>",
        "company_domain": "<string>"
      },
      "custom": {},
      "contact_info": {
        "most_probable_work_email": { "email": "<string>", "status": "DELIVERABLE" },
        "most_probable_phone": {
          "number": "+1 415-555-0100",
          "region": "US",
          "line_type": "MOBILE"
        },
        "work_emails": [{ "email": "<string>", "status": "DELIVERABLE" }],
        "personal_emails": [{ "email": "<string>", "status": "DELIVERABLE" }],
        "phones": []
      },
      "profile": { "full_name": "<string>", "headline": "<string>" }
    }
  ]
}

custom is how you match results to your own records. Put identifiers such as a user ID or CRM contact ID in each contact's custom object when you start the enrichment, and FullEnrich returns them "exactly as provided". The values must be strings: a number makes the request fail.

Setting up FullEnrich webhooks

  1. Copy your API key from the FullEnrich API dashboard and store it server-side, for example as FULLENRICH_API_KEY. It authenticates your API calls and verifies the callbacks.
  2. Start an enrichment with your callback URLs in the body:
curl -X POST "https://app.fullenrich.com/api/v2/contact/enrich/bulk" \
  -H "Authorization: Bearer $FULLENRICH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sales Operations in London",
    "webhook_url": "https://example.com/webhooks/fullenrich/batch",
    "webhook_events": { "contact_finished": "https://example.com/webhooks/fullenrich/contact" },
    "data": [
      {
        "first_name": "John",
        "last_name": "Snow",
        "domain": "example.com",
        "enrich_fields": ["contact.work_emails", "contact.phones"],
        "custom": { "crm_contact_id": "abc-123" }
      }
    ]
  }'
  1. Store the id the API returns, so you can match it to the callback.

Reverse email lookup takes the same webhook_url and webhook_events parameters.

If you receive FullEnrich webhooks through Hookdeck, create a source with the FullEnrich type and paste your FullEnrich API key as the secret, since FullEnrich signs webhooks with that key. Use the source URL, or paths on it, as your callback URLs.

Securing FullEnrich webhooks

Read X-Signature-SHA1, compute an HMAC-SHA1 of the raw request body with your API key as the key, hex-encode it in lowercase, and compare in constant time. The algorithm is SHA-1, not SHA-256.

const crypto = require('crypto');
const express = require('express');

function verifyFullEnrichSignature(rawBody, signatureHeader, apiKey) {
  if (!signatureHeader || !apiKey) return false;
  const expected = crypto.createHmac('sha1', apiKey).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/fullenrich/:callback', express.raw({ type: '*/*' }), (req, res) => {
  const apiKey = process.env.FULLENRICH_API_KEY;
  if (!apiKey) return res.sendStatus(500); // fail closed

  if (!verifyFullEnrichSignature(req.body, req.get('x-signature-sha1'), apiKey)) {
    return res.sendStatus(401);
  }

  const result = JSON.parse(req.body.toString('utf8'));
  // Branch on result.status; match contacts to your records with data[].custom.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac


def verify_fullenrich_signature(raw_body: bytes, signature_header, api_key: str) -> bool:
    if not signature_header or not api_key:
        return False
    expected = hmac.new(api_key.encode("utf-8"), raw_body, hashlib.sha1).hexdigest()
    return hmac.compare_digest(
        signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8")
    )


# In FastAPI: raw_body = await request.body()
# verify_fullenrich_signature(raw_body, request.headers.get("x-signature-sha1"),
#                             os.environ["FULLENRICH_API_KEY"])

FullEnrich's Node sample mounts express.raw({ type: "application/json" }) and calls crypto.timingSafeEqual without a length check, which throws if the header is the wrong length. Using */* keeps the body raw whatever the content type, and the length check turns a malformed header into a clean 401.

There is no timestamp in the request, so nothing tells you how old a callback is.

Make FullEnrich webhooks production-ready. Hookdeck Event Gateway verifies the X-Signature-SHA1 header, stores every enrichment result as it arrives, and delivers it to your service with retries you control.

FullEnrich webhook limitations and pain points

Five minutes of retries

The Problem: A short outage or a slow deploy loses enrichment results you have already paid for.

Why It Happens: FullEnrich retries a failed callback every minute, up to 5 times. If your endpoint is down for longer than that, the callback is gone.

Workarounds:

  • Store the job id when you start an enrichment, and fall back to the Get Enrichment Result endpoint for any job whose batch callback never arrived.
  • If you need to poll, do it no more than once every 5 to 10 minutes, as FullEnrich asks.
  • Ask FullEnrich to check its delivery logs if a specific result is missing.

How Hookdeck Can Help: Hookdeck accepts each callback as soon as FullEnrich sends it and stores it, then delivers it to your service with retries you configure, so your downtime no longer has to fit inside FullEnrich's five minutes.

The API key is the signing key

The Problem: Every service that verifies callbacks holds a key that can also spend your credits, and regenerating it breaks verification.

Why It Happens: FullEnrich has no separate webhook secret. The key that calls the API is the key that signs the callbacks.

Workarounds:

  • Keep verification in one place, so only one service needs the key.
  • When you regenerate the key, update every verifier at the same time. Any verifier still holding the old key fails every callback signed with the new one.

How Hookdeck Can Help: With Hookdeck in front, the API key is stored on one Hookdeck source and your services behind it don't need it at all. Hookdeck answers FullEnrich with a 200 either way, then records a request that fails the check as failed verification, so it never reaches your destination.

No event type to route on

The Problem: One handler receives both batch and per-contact callbacks and treats a single contact as the whole batch.

Why It Happens: FullEnrich sends no event name. The only differences are the URL each callback was sent to and the status field.

Workarounds:

  • Use different paths for webhook_url and contact_finished, as in the example above.
  • Treat IN_PROGRESS as a single contact and the other statuses as the end of the batch.

How Hookdeck Can Help: Hookdeck filters on the path or on status can send per-contact results and batch completions to different destinations.

Duplicate results with both callbacks

The Problem: Each contact is written twice.

Why It Happens: When you set both callbacks, each contact arrives once in its own contact_finished callback and again in the batch callback's data.

Workarounds:

  • Write contacts from one callback only, and use the other for job status.
  • Or upsert on your custom identifier, so the second write changes nothing.

How Hookdeck Can Help: Hookdeck stores every callback, so you can compare what arrived per contact with the batch result when you're checking a job.

Best practices

Verify against the raw body

FullEnrich warns that re-serialised JSON produces a different hash. In Express, use express.raw({ type: '*/*' }). In Next.js, use await request.text(). In FastAPI, use await request.body().

Always send custom identifiers

Without custom, you have to match results to your records by name and domain. Add your own string IDs to every contact you submit.

Handle the failure statuses

CREDITS_INSUFFICIENT and CANCELED are batch callbacks too. Mark the job as incomplete, alert someone about credits, and don't treat missing contacts as "not found".

Make handlers idempotent

Retries resend the same body. Key batch callbacks on the job id and contact results on your custom identifier. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

With only five retries a minute apart, a slow handler is risky. Verify, store the body, return 200, and update your CRM afterwards. See why to process webhooks asynchronously.

Conclusion

FullEnrich pushes enrichment results to URLs you set on each request and signs them with X-Signature-SHA1, a lowercase hex HMAC-SHA1 of the raw body keyed with your API key. Route on the callback URL and status, match contacts with custom, and keep a fallback to the results endpoint, because FullEnrich stops retrying after five minutes.

Hookdeck Event Gateway sits in front of your receiver, verifies every FullEnrich callback, and holds each result until your service has processed it.

Get started with Hookdeck for free and handle FullEnrich webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.