> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enrow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Hoe webhooks werken

> Ontvang realtime webhook-meldingen wanneer Enrow-zoekopdrachten en -verificaties voltooid zijn, in plaats van de API te pollen

export const PollingVsWebhook = () => {
  const [pollingStep, setPollingStep] = useState(0);
  const [webhookPhase, setWebhookPhase] = useState("idle");
  useEffect(() => {
    const timers = [];
    const addTimer = (fn, ms) => {
      const id = setTimeout(fn, ms);
      timers.push(id);
    };
    const runCycle = () => {
      setPollingStep(0);
      for (let i = 0; i < 5; i++) {
        addTimer(() => setPollingStep(i + 1), (i + 1) * 900);
      }
      addTimer(() => setPollingStep(0), 6500);
      setWebhookPhase("idle");
      addTimer(() => setWebhookPhase("sent"), 400);
      addTimer(() => setWebhookPhase("processing"), 1200);
      addTimer(() => setWebhookPhase("callback"), 2800);
      addTimer(() => setWebhookPhase("idle"), 6500);
    };
    runCycle();
    const loop = setInterval(runCycle, 7200);
    return () => {
      clearInterval(loop);
      timers.forEach(clearTimeout);
    };
  }, []);
  const POLLING_STEPS = [{
    response: "pending",
    success: false
  }, {
    response: "pending",
    success: false
  }, {
    response: "pending",
    success: false
  }, {
    response: "pending",
    success: false
  }, {
    response: "complete",
    success: true
  }];
  const svgIcon = ({className, circle, paths}) => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
      {circle ? <circle cx={circle.cx} cy={circle.cy} r={circle.r} /> : null}
      {paths.map((d, i) => <path key={i} d={d} />)}
    </svg>;
  const icons = {
    arrowRight: className => svgIcon({
      className,
      paths: ["M5 12h14", "m12 5 7 7-7 7"]
    }),
    bell: className => svgIcon({
      className,
      paths: ["M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", "M10.3 21a1.94 1.94 0 0 0 3.4 0"]
    }),
    checkCircle: className => svgIcon({
      className,
      circle: {
        cx: 12,
        cy: 12,
        r: 10
      },
      paths: ["m9 12 2 2 4-4"]
    }),
    xCircle: className => svgIcon({
      className,
      circle: {
        cx: 12,
        cy: 12,
        r: 10
      },
      paths: ["m15 9-6 6", "m9 9 6 6"]
    }),
    loader2: className => svgIcon({
      className,
      paths: ["M21 12a9 9 0 1 1-6.219-8.56"]
    })
  };
  return <div className="my-6 grid grid-cols-1 sm:grid-cols-2 gap-4 not-prose">
      {}
      <div className="rounded-xl border border-rose-500/20 bg-zinc-50/50 dark:bg-zinc-900/40 p-4">
        <div className="flex items-start gap-2.5 mb-3">
          <div className="size-8 rounded-lg bg-rose-500/10 flex items-center justify-center shrink-0">
            {icons.xCircle("size-4 text-rose-600 dark:text-rose-400")}
          </div>
          <div className="min-w-0">
            <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0 leading-8">
              Polling
            </h3>
            <div className="text-[11px] text-zinc-500 dark:text-zinc-400" style={{
    lineHeight: 1.5
  }}>
              Your app repeatedly asks "is it done yet?" until the result is ready.
            </div>
          </div>
        </div>

        <div className="space-y-1.5 mb-3 min-h-[140px]">
          {POLLING_STEPS.map((step, i) => <div key={i} className="flex items-center gap-2 text-xs font-mono transition-all duration-300" style={{
    opacity: i < pollingStep ? 1 : 0.2,
    transform: i < pollingStep ? "translateX(0)" : "translateX(-4px)"
  }}>
              <span className="text-zinc-500 dark:text-zinc-400 w-20 shrink-0">
                GET /status
              </span>
              {icons.arrowRight("size-3 text-zinc-400/70 shrink-0")}
              {step.success ? <span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
                  {icons.checkCircle("size-3")}
                  {step.response}
                </span> : <span className="flex items-center gap-1 text-rose-600 dark:text-rose-400">
                  {icons.xCircle("size-3")}
                  {step.response}
                </span>}
            </div>)}
        </div>

        <div className="border-t border-zinc-200 dark:border-zinc-800 pt-3 space-y-1">
          {["5+ requests per result", "Wasted bandwidth", "Delayed results"].map(stat => <div key={stat} className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
              {icons.xCircle("size-3 text-rose-500/60 shrink-0")}
              {stat}
            </div>)}
        </div>
      </div>

      {}
      <div className="rounded-xl border border-emerald-500/20 bg-zinc-50/50 dark:bg-zinc-900/40 p-4">
        <div className="flex items-start gap-2.5 mb-3">
          <div className="size-8 rounded-lg bg-emerald-500/10 flex items-center justify-center shrink-0">
            {icons.checkCircle("size-4 text-emerald-600 dark:text-emerald-400")}
          </div>
          <div className="min-w-0">
            <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0 leading-8">
              Webhook
            </h3>
            <div className="text-[11px] text-zinc-500 dark:text-zinc-400" style={{
    lineHeight: 1.5
  }}>
              Enrow notifies your server the instant the result is ready.
            </div>
          </div>
        </div>

        <div className="space-y-1.5 mb-3 min-h-[140px]">
          <div className="flex items-center justify-between text-xs font-mono transition-all duration-500" style={{
    opacity: webhookPhase !== "idle" ? 1 : 0.2,
    transform: webhookPhase !== "idle" ? "translateX(0)" : "translateX(-4px)"
  }}>
            <span className="text-emerald-700 dark:text-emerald-400 font-semibold whitespace-nowrap">
              POST /email/find/single
            </span>
            <span className="text-zinc-500 dark:text-zinc-400 shrink-0 ml-2">
              {webhookPhase === "sent" && "sent ✓"}
              {webhookPhase === "processing" && <span className="inline-flex items-center gap-1">
                  {icons.loader2("size-3 animate-spin")}
                  Processing...
                </span>}
              {webhookPhase === "callback" && <span className="text-emerald-600 dark:text-emerald-400">✓ done</span>}
            </span>
          </div>

          {webhookPhase === "callback" && <div className="mt-3 rounded-md border border-emerald-400/30 bg-emerald-500/5 px-3 py-2 font-mono text-xs">
              <div className="flex items-center gap-1.5 mb-1">
                {icons.bell("size-3 text-emerald-500")}
                <span className="text-emerald-600 dark:text-emerald-400 font-semibold text-[10px] uppercase tracking-wider">
                  Webhook callback
                </span>
              </div>
              <span className="text-zinc-600 dark:text-zinc-400">
                {'{"email":"found","status":"valid"}'}
              </span>
            </div>}
        </div>

        <div className="border-t border-zinc-200 dark:border-zinc-800 pt-3 space-y-1">
          {["1 request, 1 callback", "Zero waste", "Real-time delivery"].map(stat => <div key={stat} className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
              {icons.checkCircle("size-3 text-emerald-500/70 shrink-0")}
              {stat}
            </div>)}
        </div>
      </div>
    </div>;
};

## Waarom webhooks gebruiken?

Met webhooks komen resultaten naar je toe in plaats van dat je er steeds opnieuw om vraagt. In plaats van een GET-endpoint te pollen totdat een zoekopdracht klaar is, stuurt Enrow elk resultaat naar je server op het moment dat het beschikbaar is — dat bespaart verzoeken, verlaagt de latentie en houdt je code eenvoudig.

**Stop met het verspillen van verzoeken — laat de resultaten naar je toe komen.**

<PollingVsWebhook />

Omdat elk Enrow-endpoint **asynchroon** is, zijn webhooks de aanbevolen manier om resultaten te ontvangen voor [Email Finder](/nl/api-reference/email-finder/find-single), [Email Verifier](/nl/api-reference/email-verifier/verify-single) en [Phone Finder](/nl/api-reference/phone/find-single). Webhooks omzeilen ook volledig de [rate limits](/nl/rate-limits), omdat Enrow jou aanroept in plaats van andersom.

## Hoe verloopt een webhook-flow?

Een webhook-flow zet één zoekverzoek om in een automatische levering. Je vertelt Enrow waar de resultaten naartoe moeten, en Enrow doet de rest:

1. Je doet een **POST** met een zoekverzoek inclusief een `webhook`-URL in de `settings`
2. Enrow geeft direct een zoek-ID terug
3. Enrow verwerkt de zoekopdracht op de achtergrond
4. Zodra deze voltooid is, doet Enrow een **POST** van de resultaten naar je webhook-URL

## Hoe stel ik een webhook in?

Je kunt een webhook op twee manieren registreren, afhankelijk van of je deze voor één zoekopdracht of voor elke zoekopdracht wilt:

1. **Per verzoek**: Voeg een `webhook`-URL toe aan het `settings`-object van een API-aanroep
2. **Globaal**: Configureer een standaard webhook via de [integratiepagina](https://app.enrow.io/integrations) op het dashboard

```json theme={null}
{
  "fullname": "Dwight Schrute",
  "company_domain": "dundermifflin.com",
  "settings": {
    "webhook": "https://your-app.com/webhooks/enrow"
  }
}
```

<Warning>
  Je webhook-URL moet een geldig **HTTPS**-endpoint zijn dat een `200`-statuscode teruggeeft.
</Warning>

## Welke events activeren een webhook-aanroep?

Zes soorten events kunnen een webhook-aanroep activeren, één per endpoint en zoektype:

| Event                          | Beschrijving                                      |
| ------------------------------ | ------------------------------------------------- |
| `single_search_finished`       | Een enkelvoudige e-mailzoekopdracht is voltooid   |
| `bulk_search_finished`         | Een bulk-e-mailzoekopdracht is voltooid           |
| `verification_finished`        | Een enkelvoudige e-mailverificatie is voltooid    |
| `bulk_verification_finished`   | Een bulk-e-mailverificatie is voltooid            |
| `single_phone_search_finished` | Een enkelvoudige telefoonzoekopdracht is voltooid |
| `bulk_phone_search_finished`   | Een bulk-telefoonzoekopdracht is voltooid         |

## Hoe ziet een webhook-payload eruit?

De webhook-payload hangt af van het endpoint en of de zoekopdracht enkelvoudig of in bulk was. Enkelvoudige zoekopdrachten leveren het volledige resultaat direct, terwijl bulkzoekopdrachten een voltooiingsmelding leveren waarop je vervolgens een GET-verzoek doet.

### Email Finder — Enkelvoudig

Bij enkelvoudige zoekopdrachten ontvang je het **volledige resultaat direct** in de webhook-melding. Hierdoor hoef je geen GET-verzoek meer te doen.

```json theme={null}
{
  "event": "single_search_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt",
  "credits": {
    "cost": 1
  },
  "result": {
    "email": "dwight.schrute@dundermifflin.com",
    "qualification": "valid",
    "custom": "your_custom_data",
    "info": {
      "company_domain": "dundermifflin.com",
      "fullname": "Dwight Schrute",
      "firstname": "Dwight",
      "lastname": "Schrute"
    }
  }
}
```

### Email Finder — Bulk

Bij bulkzoekopdrachten ontvang je een melding dat de batch is voltooid. Roep vervolgens het endpoint [GET /email/find/bulk](/nl/api-reference/email-finder/get-bulk-results) aan met de `id` om de resultaten op te halen.

```json theme={null}
{
  "event": "bulk_search_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt",
  "credits": {
    "cost": 2284
  }
}
```

### Email Verifier — Enkelvoudig

Het volledige resultaat wordt direct meegestuurd — geen GET-verzoek nodig.

```json theme={null}
{
  "event": "verification_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt",
  "email": "pam.beesly@dundermifflin.com",
  "qualification": "valid",
  "custom": "your_custom_data"
}
```

### Email Verifier — Bulk

Dit is alleen een melding. Roep [GET /email/verify/bulk](/nl/api-reference/email-verifier/get-bulk-verifications) aan met de `id` om de resultaten op te halen.

```json theme={null}
{
  "event": "bulk_verification_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt",
  "credits": {
    "cost": 386.25
  }
}
```

### Phone Finder — Enkelvoudig

Het volledige resultaat wordt direct meegestuurd — geen GET-verzoek nodig.

```json theme={null}
{
  "event": "single_phone_search_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt",
  "credits": {
    "cost": 50
  },
  "result": {
    "number": "+15705551234",
    "params": {
      "linkedin_url": "https://www.linkedin.com/in/michael-scott"
    },
    "qualification": "found"
  }
}
```

### Phone Finder — Bulk

Dit is alleen een melding. Roep [GET /phone/bulk](/nl/api-reference/phone/get-bulk-results) aan met de `id` om de resultaten op te halen.

```json theme={null}
{
  "event": "bulk_phone_search_finished",
  "id": "910f3e13-b2bf-442d-ab0b-4cf44dfrij84fjrt"
}
```

## Wat is het verschil tussen enkelvoudige en bulk-webhooks?

Webhooks voor enkelvoudige zoekopdrachten bevatten het volledige resultaat, dus er is geen extra aanroep nodig. Webhooks voor bulkzoekopdrachten geven alleen aan dat de batch klaar is — vervolgens haal je de resultaten op met het bijbehorende GET-endpoint.

| Type           | Enkelvoudige zoekopdrachten   | Bulkzoekopdrachten                        |
| -------------- | ----------------------------- | ----------------------------------------- |
| **Payload**    | Volledig resultaat inbegrepen | Alleen melding (ID + credits)             |
| **GET nodig?** | Nee                           | Ja — gebruik het GET-endpoint met de `id` |

<Info>
  Bij enkelvoudige zoekopdrachten bevat de webhook alles wat je nodig hebt. Bij bulkzoekopdrachten geeft de webhook aan dat de batch klaar is — vervolgens haal je de resultaten op.
</Info>

## Wat zijn de best practices voor webhook-endpoints?

Een betrouwbaar webhook-endpoint reageert snel, accepteert alleen HTTPS en tolereert af en toe een duplicaat. Volg deze praktijken om leveringen betrouwbaar te houden:

<AccordionGroup>
  <Accordion title="Geef snel een 200 terug">
    Verwerk webhook-payloads asynchroon. Geef direct een `200` terug en verwerk de data daarna in een achtergrondtaak.
  </Accordion>

  <Accordion title="Gebruik HTTPS">
    Gebruik altijd HTTPS-endpoints. HTTP-webhooks worden geweigerd.
  </Accordion>

  <Accordion title="Verwerk duplicaten">
    In zeldzame gevallen kan een webhook meer dan één keer worden geleverd. Gebruik het `id`-veld om te dedupliceren.
  </Accordion>

  <Accordion title="Gebruik aangepaste velden">
    Geef `custom`-data mee in je verzoeken om te bepalen bij welk record een webhook-resultaat hoort:

    ```json theme={null}
    {
      "fullname": "Dwight Schrute",
      "company_domain": "dundermifflin.com",
      "custom": { "crm_id": "lead_001" }
    }
    ```

    Het `custom`-veld wordt ongewijzigd teruggegeven in de webhook-payload.
  </Accordion>
</AccordionGroup>

## Moet ik webhooks of polling gebruiken?

Gebruik webhooks voor productie en polling alleen voor snel prototypen of debuggen. Webhooks leveren resultaten in realtime zonder je verzoekquotum te verbruiken, terwijl polling herhaalde GET-aanroepen doet die meetellen voor je [rate limits](/nl/rate-limits).

|                          | Webhooks                               | Polling (GET)                       |
| ------------------------ | -------------------------------------- | ----------------------------------- |
| **Latentie**             | Realtime                               | Afhankelijk van poll-interval       |
| **API-aanroepen**        | 0 (Enrow roept jou aan)                | Meerdere aanroepen per zoekopdracht |
| **Impact op rate limit** | Geen                                   | Verbruikt quotum                    |
| **Complexiteit**         | Vereist het instellen van een endpoint | Eenvoudiger te implementeren        |

<Note>
  We raden webhooks aan voor productiegebruik. Gebruik polling alleen voor snel prototypen of debuggen.
</Note>

## FAQ

<AccordionGroup>
  <Accordion title="Kosten webhooks extra credits?">
    Nee. Webhooks verbruiken geen extra credits — je betaalt alleen voor de zoekopdracht zelf. De creditkosten worden gerapporteerd in het `credits.cost`-veld van de payload. Zie [Credits & facturatie](/nl/credits-billing) voor de kosten per endpoint.
  </Accordion>

  <Accordion title="Wat gebeurt er als mijn endpoint geen 200 teruggeeft?">
    Je webhook-URL moet een geldig HTTPS-endpoint zijn dat een `200`-statuscode teruggeeft. Als je server onbereikbaar is of met een andere status reageert, wordt de levering als mislukt beschouwd. Als terugvaloptie kun je de resultaten altijd ophalen door het relevante GET-endpoint te pollen met de zoek-`id`.
  </Accordion>

  <Accordion title="Hoe koppel ik een webhook aan het oorspronkelijke verzoek?">
    Gebruik de `id` uit de zoekrespons, of geef een `custom`-object mee in je verzoek — dit wordt ongewijzigd teruggegeven in de webhook-payload, zodat je resultaten kunt terugkoppelen aan je eigen records, zoals een CRM-lead-ID.
  </Accordion>

  <Accordion title="Waarom heb ik geen webhook ontvangen?">
    De meest voorkomende oorzaken zijn een niet-HTTPS-URL, een endpoint dat geen `200` teruggeeft, of een server die een time-out geeft. Controleer of je endpoint openbaar bereikbaar is via HTTPS. Voor uitgebreidere probleemoplossing, zie [Foutafhandeling](/nl/error-handling) en [Statuscodes](/nl/status-codes).
  </Accordion>
</AccordionGroup>

## Volgende stappen

<CardGroup cols={2}>
  <Card title="Een e-mail vinden" icon="envelope" href="/nl/api-reference/email-finder/find-single">
    Geef een webhook-URL mee in settings om het resultaat automatisch geleverd te krijgen.
  </Card>

  <Card title="Bulkresultaten ophalen" icon="layer-group" href="/nl/api-reference/email-finder/get-bulk-results">
    Haal batchresultaten op nadat een bulk\_search\_finished-webhook is afgevuurd.
  </Card>

  <Card title="Authenticatie" icon="key" href="/nl/authentication">
    Hoe je je API-sleutel meegeeft in de x-api-key-header.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/nl/rate-limits">
    Lees waarom webhooks het verzoekquotum vermijden dat polling verbruikt.
  </Card>
</CardGroup>
