> ## 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.

# Sådan virker webhooks

> Modtag webhook-notifikationer i realtid, når Enrow-søgninger og -verifikationer er færdige, i stedet for at polle API'et

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>;
};

## Hvorfor bruge webhooks?

Webhooks lader resultaterne komme til dig, i stedet for at du gentagne gange spørger efter dem. I stedet for at polle et GET-endpoint, indtil en søgning er færdig, sender Enrow hvert resultat til din server i det øjeblik, det er klar — hvilket sparer requests, reducerer latens og holder din kode enkel.

**Stop med at spilde requests — lad resultaterne komme til dig.**

<PollingVsWebhook />

Fordi hvert Enrow-endpoint er **asynkront**, er webhooks den anbefalede måde at modtage resultater på på tværs af [Email Finder](/da/api-reference/email-finder/find-single), [Email Verifier](/da/api-reference/email-verifier/verify-single) og [Phone Finder](/da/api-reference/phone/find-single). Webhooks omgår også [rate limits](/da/rate-limits) helt, eftersom Enrow ringer til dig i stedet for omvendt.

## Hvordan fungerer et webhook-flow?

Et webhook-flow forvandler en enkelt søgeforespørgsel til en automatisk levering. Du fortæller Enrow, hvor resultaterne skal sendes hen, og Enrow klarer resten:

1. Du sender en **POST** med en søgeforespørgsel med en `webhook`-URL i `settings`
2. Enrow returnerer et søge-ID med det samme
3. Enrow behandler søgningen i baggrunden
4. Når den er færdig, sender Enrow resultaterne med **POST** til din webhook-URL

## Hvordan opsætter jeg en webhook?

Du kan registrere en webhook på to måder, afhængigt af om du vil bruge den til én søgning eller hver søgning:

1. **Per request**: Inkluder en `webhook`-URL i `settings`-objektet i ethvert API-kald
2. **Globalt**: Konfigurer en standard-webhook fra [integrationssiden](https://app.enrow.io/integrations) på dashboardet

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

<Warning>
  Din webhook-URL skal være et gyldigt **HTTPS**-endpoint, der returnerer en `200`-statuskode.
</Warning>

## Hvilke events udløser et webhook-kald?

Seks typer events kan udløse et webhook-kald, én per endpoint og søgetype:

| Event                          | Description                               |
| ------------------------------ | ----------------------------------------- |
| `single_search_finished`       | En enkelt e-mailsøgning er afsluttet      |
| `bulk_search_finished`         | En bulk-e-mailsøgning er afsluttet        |
| `verification_finished`        | En enkelt e-mailverifikation er afsluttet |
| `bulk_verification_finished`   | En bulk-e-mailverifikation er afsluttet   |
| `single_phone_search_finished` | En enkelt telefonsøgning er afsluttet     |
| `bulk_phone_search_finished`   | En bulk-telefonsøgning er afsluttet       |

## Hvordan ser en webhook-payload ud?

Webhook-payloaden afhænger af endpointet, og om søgningen var enkelt eller bulk. Enkelte søgninger leverer det fulde resultat direkte, mens bulk-søgninger leverer en færdigmeldings-notifikation, som du følger op på med en GET-forespørgsel.

### Email Finder — Single

For enkelte søgninger modtager du det **fulde resultat direkte** i webhook-notifikationen. Dette fjerner behovet for at udføre en GET-forespørgsel.

```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

For bulk-søgninger modtager du en notifikation om, at batchen er færdig. Kald derefter endpointet [GET /email/find/bulk](/da/api-reference/email-finder/get-bulk-results) med `id` for at hente resultaterne.

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

### Email Verifier — Single

Det fulde resultat er inkluderet direkte — ingen GET-forespørgsel nødvendig.

```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

Dette er kun en notifikation. Kald [GET /email/verify/bulk](/da/api-reference/email-verifier/get-bulk-verifications) med `id` for at hente resultaterne.

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

### Phone Finder — Single

Det fulde resultat er inkluderet direkte — ingen GET-forespørgsel nødvendig.

```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

Dette er kun en notifikation. Kald [GET /phone/bulk](/da/api-reference/phone/get-bulk-results) med `id` for at hente resultaterne.

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

## Hvordan adskiller enkelte og bulk-webhooks sig?

Webhooks for enkelte søgninger indeholder det komplette resultat, så der er ikke behov for ekstra kald. Webhooks for bulk-søgninger signalerer kun, at batchen er færdig — derefter henter du resultaterne med det tilsvarende GET-endpoint.

| Type               | Single searches           | Bulk searches                     |
| ------------------ | ------------------------- | --------------------------------- |
| **Payload**        | Fuldt resultat inkluderet | Kun notifikation (ID + credits)   |
| **GET nødvendig?** | Nej                       | Ja — brug GET-endpointet med `id` |

<Info>
  For enkelte søgninger indeholder webhooken alt, hvad du har brug for. For bulk-søgninger fortæller webhooken dig, at batchen er færdig — derefter henter du resultaterne.
</Info>

## Hvad er best practices for webhook-endpoints?

Et pålideligt webhook-endpoint svarer hurtigt, accepterer kun HTTPS og tolererer en lejlighedsvis dublet. Følg disse praksisser for at holde leveringer pålidelige:

<AccordionGroup>
  <Accordion title="Returnér 200 hurtigt">
    Behandl webhook-payloads asynkront. Returnér en `200` med det samme, og håndtér derefter dataene i et baggrundsjob.
  </Accordion>

  <Accordion title="Brug HTTPS">
    Brug altid HTTPS-endpoints. HTTP-webhooks vil blive afvist.
  </Accordion>

  <Accordion title="Håndtér dubletter">
    I sjældne tilfælde kan webhooks blive leveret mere end én gang. Brug `id`-feltet til at deduplikere.
  </Accordion>

  <Accordion title="Brug custom-felter">
    Send `custom`-data med i dine requests for at identificere, hvilken record et webhook-resultat hører til:

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

    `custom`-feltet returneres uændret i webhook-payloaden.
  </Accordion>
</AccordionGroup>

## Skal jeg bruge webhooks eller polling?

Brug webhooks til produktion og polling kun til hurtig prototyping eller fejlfinding. Webhooks leverer resultater i realtid uden at forbruge din request-kvote, mens polling foretager gentagne GET-kald, der tæller mod dine [rate limits](/da/rate-limits).

|                              | Webhooks                     | Polling (GET)             |
| ---------------------------- | ---------------------------- | ------------------------- |
| **Latens**                   | Realtid                      | Afhænger af poll-interval |
| **API-kald**                 | 0 (Enrow ringer til dig)     | Flere kald per søgning    |
| **Påvirkning af rate limit** | Ingen                        | Forbruger kvote           |
| **Kompleksitet**             | Kræver opsætning af endpoint | Enklere at implementere   |

<Note>
  Vi anbefaler webhooks til produktionsbrug. Brug kun polling til hurtig prototyping eller fejlfinding.
</Note>

## FAQ

<AccordionGroup>
  <Accordion title="Koster webhooks ekstra credits?">
    Nej. Webhooks forbruger ikke yderligere credits — du betaler kun for selve søgningen. Credit-omkostningen rapporteres i `credits.cost`-feltet i payloaden. Se [Credits og fakturering](/da/credits-billing) for omkostninger per endpoint.
  </Accordion>

  <Accordion title="Hvad sker der, hvis mit endpoint ikke returnerer en 200?">
    Din webhook-URL skal være et gyldigt HTTPS-endpoint, der returnerer en `200`-statuskode. Hvis din server ikke kan nås eller svarer med en anden status, behandles leveringen som mislykket. Som reserveløsning kan du altid hente resultater ved at polle det relevante GET-endpoint med søgningens `id`.
  </Accordion>

  <Accordion title="Hvordan matcher jeg en webhook til den oprindelige request?">
    Brug `id` fra søgesvaret, eller send et `custom`-objekt med i din request — det returneres uændret i webhook-payloaden, så du kan mappe resultater tilbage til dine egne records, såsom et CRM-lead-ID.
  </Accordion>

  <Accordion title="Hvorfor modtog jeg ikke en webhook?">
    De mest almindelige årsager er en URL, der ikke er HTTPS, et endpoint, der ikke returnerer `200`, eller en server, der timer ud. Bekræft, at dit endpoint er offentligt tilgængeligt over HTTPS. For bredere fejlfinding, se [Fejlhåndtering](/da/error-handling) og [Statuskoder](/da/status-codes).
  </Accordion>
</AccordionGroup>

## Næste skridt

<CardGroup cols={2}>
  <Card title="Find en e-mail" icon="envelope" href="/da/api-reference/email-finder/find-single">
    Send en webhook-URL med i settings for at få resultatet leveret automatisk.
  </Card>

  <Card title="Hent bulk-resultater" icon="layer-group" href="/da/api-reference/email-finder/get-bulk-results">
    Hent batch-resultater, efter at en bulk\_search\_finished-webhook er udløst.
  </Card>

  <Card title="Autentificering" icon="key" href="/da/authentication">
    Sådan sender du din API-nøgle i x-api-key-headeren.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/da/rate-limits">
    Se hvorfor webhooks undgår den request-kvote, som polling forbruger.
  </Card>
</CardGroup>
