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

# Receive research results with webhooks

Webhooks let Seamless push completed research results to your server the moment enrichment finishes, without requiring you to poll for status. When research completes, the platform sends a `POST` request containing the enriched record to the HTTPS endpoint you configure. Your server validates the shared secret in the request header, processes the payload, and returns an HTTP `2xx` response to acknowledge receipt.

## Prerequisites

Before you set up webhooks:

* Authenticate with an [API key](/authentication/api-keys) (`Token` header) or [OAuth](/authentication/oauth) (`Authorization: Bearer`).
* Create a public HTTPS endpoint that accepts `POST` requests and is reachable from the internet.
* In your Seamless account, go to [**Settings → Webhooks**](https://login.seamless.ai/settings/webhooks), create a webhook, and select the event type: `company-researched` or `contact-researched`.

## How delivery works

```mermaid theme={null}
sequenceDiagram
  participant App as Your backend
  participant API as Seamless API
  participant WH as Your webhook URL
  App->>API: POST /contacts/research
  API-->>App: requestIds
  API->>WH: POST (enriched record)
  Note over WH: Validate x-seamless-webhook-secret
  WH-->>API: 2xx response
```

## Event payloads

| Event                | Fires when                 | Body shape                                                                                                          |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `company-researched` | Company research completes | Company object — see [Company Research](https://docs.seamless.ai/#tag/Company-Research) in the public API reference |
| `contact-researched` | Contact research completes | Contact object — see [Contact Research](https://docs.seamless.ai/#tag/Contact-Research)                             |

Example **company-researched** payload (fields may vary by record):

```json theme={null}
{
  "name": "Seamless.AI",
  "domain": "seamless.ai",
  "apiResearchId": "research-123",
  "phones": "+1-650-555-0100",
  "staffCountRange": "501 - 1,000",
  "revenueRange": "$1B+",
  "linkedInProfileUrl": "https://www.linkedin.com/company/seamless-ai/"
}
```

Example **contact-researched** payload (fields may vary by record):

```json theme={null}
{
  "fullName": "Jane Doe",
  "title": "Chief Technology Officer",
  "company": "Acme Corp",
  "email": "jane.doe@acme.com",
  "apiResearchId": "research-456",
  "phones": "+1-555-555-0199",
  "linkedInProfileUrl": "https://www.linkedin.com/in/janedoe/"
}
```

The `apiResearchId` field corresponds to the `requestId` from your research response. Use it to match webhook deliveries to in-flight jobs.

## Webhook setup

<Steps>
  <Step title="Submit a research request">
    Call [Research companies](/researchcompanies) or [Research contacts](/researchcontacts) to start enrichment. Save the `requestId` returned in the response.
  </Step>

  <Step title="Wait for the platform to send the event">
    When research completes, Seamless sends a `POST` request to your configured webhook endpoint. The request body contains the enriched record as a JSON object.
  </Step>

  <Step title="Validate the webhook secret">
    Read the `x-seamless-webhook-secret` header from the incoming request and compare it to the shared secret you configured in [**Settings → Webhooks**](https://login.seamless.ai/settings/webhooks). Reject requests where the values do not match.
  </Step>

  <Step title="Parse the request body and process the event">
    Parse the JSON body and extract the enriched data. Run your application logic — store the record, trigger a downstream action, or pass it to another system.
  </Step>

  <Step title="Return an HTTP 2xx response">
    Respond with a `2xx` status code after accepting the event. Seamless treats any other status as a delivery failure and may retry the request.
  </Step>
</Steps>

## Receiver code examples

<CodeGroup>
  ```javascript node.js theme={null}
  import express from 'express';

  const app = express();
  const port = process.env.PORT || 3000;
  const webhookSecret = process.env.WEBHOOK_SECRET;

  app.use(express.json());

  app.post('/webhooks/research-results', (req, res) => {
    const receivedSecret = req.header('x-seamless-webhook-secret');

    if (!receivedSecret || receivedSecret !== webhookSecret) {
      return res.status(401).json({ message: 'Invalid webhook secret' });
    }

    if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
      return res.status(400).json({ message: 'Invalid JSON payload' });
    }

    try {
      console.log('Webhook received:', JSON.stringify(req.body, null, 2));
      return res.status(200).json({ received: true });
    } catch (error) {
      console.error('Webhook processing failed:', error);
      return res.status(500).json({ message: 'Webhook processing failed' });
    }
  });

  app.use((error, req, res, next) => {
    if (error instanceof SyntaxError && 'body' in error) {
      return res.status(400).json({ message: 'Malformed JSON body' });
    }
    return next(error);
  });

  app.listen(port, () => {
    console.log(`Webhook receiver listening on port ${port}`);
  });
  ```

  ```python app.py theme={null}
  import os
  from flask import Flask, request, jsonify

  app = Flask(__name__)
  webhook_secret = os.environ.get('WEBHOOK_SECRET')


  @app.post('/webhooks/research-results')
  def receive_webhook():
      received_secret = request.headers.get('x-seamless-webhook-secret')

      if not received_secret or received_secret != webhook_secret:
          return jsonify({'message': 'Invalid webhook secret'}), 401

      payload = request.get_json(silent=True)

      if payload is None or not isinstance(payload, dict):
          return jsonify({'message': 'Invalid JSON payload'}), 400

      try:
          print('Webhook received:', payload)
          return jsonify({'received': True}), 200
      except Exception as error:
          print('Webhook processing failed:', error)
          return jsonify({'message': 'Webhook processing failed'}), 500


  if __name__ == '__main__':
      port = int(os.environ.get('PORT', 3000))
      app.run(port=port)
  ```
</CodeGroup>

The expected success response from your endpoint is:

```json theme={null}
{"received": true}
```

## Test your endpoint

Use this `curl` command to send a test event to your local receiver before deploying:

```bash theme={null}
curl -X POST http://localhost:3000/webhooks/research-results \
  -H "Content-Type: application/json" \
  -H "x-seamless-webhook-secret: your-shared-secret" \
  -d '{"test":true}'
```

## Use polling instead

If you prefer a request-response pattern or cannot expose a public HTTPS endpoint, use polling:

1. Submit a research request using [Research companies](/researchcompanies) or [Research contacts](/researchcontacts).
2. Save the `requestId` from the response.
3. Call [Poll company research results](/pollcompanyresearchresults) or [Poll contact research results](/pollcontactsresearchresults) with your `requestId` at a regular interval until the `status` field returns `done`.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Your endpoint does not receive events">
    Confirm all of the following:

    * A webhook exists in [**Settings → Webhooks**](https://login.seamless.ai/settings/webhooks) for your account.
    * The event type is set to `company-researched` or `contact-researched`, depending on what you submitted.
    * Your endpoint is publicly reachable over HTTPS — local addresses such as `localhost` will not receive deliveries from the platform.
  </Accordion>

  <Accordion title="Your endpoint returns 401">
    A `401` response means the secret validation failed. Check that the value of the `x-seamless-webhook-secret` header in the incoming request matches your configured shared secret exactly, including case and whitespace. Verify that the secret is correctly set in your deployed environment's variables.
  </Accordion>

  <Accordion title="Your endpoint returns 400">
    A `400` response means the request body was rejected. Confirm that your endpoint accepts `POST` requests and that your application parses the body as JSON before attempting to read any fields. If you're using Express, ensure `express.json()` middleware runs before your route handler.
  </Accordion>

  <Accordion title="You want a request-response pattern instead">
    Use the polling endpoints. See [Poll company research results](/pollcompanyresearchresults) for companies and [Poll contact research results](/pollcontactsresearchresults) for contacts. Polling requires no public endpoint and works in any environment that can make outbound HTTP requests.
  </Accordion>
</AccordionGroup>

## Related guides

<CardGroup cols={2}>
  <Card title="End-to-end company workflow" icon="building" href="/end-to-end-company-workflow">
    A complete walkthrough of the company search, research, and result-retrieval flow.
  </Card>

  <Card title="End-to-end contact workflow" icon="user" href="/end-to-end-contact-workflow">
    A complete walkthrough of the contact search, research, and result-retrieval flow.
  </Card>

  <Card title="Choose the right workflow" icon="map" href="/choose-the-right-workflow">
    Compare webhooks, polling, and org data endpoints to find the right delivery pattern for your use case.
  </Card>

  <Card title="Build a deterministic agent workflow" icon="robot" href="/build-a-deterministic-agent-workflow">
    Automate research workflows with an AI agent using a structured, repeatable sequence of API calls.
  </Card>

  <Card title="Troubleshoot authentication and request failures" icon="circle-exclamation" href="/troubleshoot-authentication-and-request-failures">
    Diagnose and resolve common errors in authentication, research submissions, and webhook delivery.
  </Card>
</CardGroup>
