# Overview

> How the Easyparser Bulk API works: submit batch jobs, receive webhooks, verify signatures and fetch results from the Data Service.

Source: https://docs.easyparser.com/bulk-integration/overview

The Bulk API processes many Amazon inputs asynchronously. You submit one POST request with groups of inputs, Easyparser processes each input in the background, notifies your `callback_url`, and you fetch the parsed results from the Data Service.

## How it works

![Animation: your app submits a bulk job, Easyparser sends one webhook per query to your callback_url, and you fetch each parsed result from the Data Service](https://docs.easyparser.com/media/animations/docs-bulk-overview-flow.jpg)

*Animation: your app submits a bulk job, Easyparser sends one webhook per query to your callback_url, and you fetch each parsed result from the Data Service* ([MP4](https://docs.easyparser.com/media/animations/docs-bulk-overview-flow.mp4))

### Step 1: Submit a bulk job

Send a POST request to the Bulk Service with a JSON array of groups. Each group sets the `platform`, `operation`, `domain`, `callback_url` and a `payload` with the input values (for example `asins`, `keywords` or `urls`).

### Step 2: Store the result IDs

The response returns immediately. For every accepted input value it contains a result `id` in `data.accepted[].results[]`. Processing continues in the background.

### Step 3: Receive the webhook

When a query finishes, Easyparser sends a POST request to the group's `callback_url`. The body is the query record with a `links` array that points to the parsed result. It does not contain the parsed data.

### Step 4: Fetch the results

Call the Data Service with each result `id` (or follow the result link from the webhook) to get the parsed JSON. You can also open each result in the web app: on the [Bulk Requests](https://app.easyparser.com/bulk-requests) page, open the job and select the result icon of an item.

![Animation: on the Bulk Requests page, open a job, then open the parsed result of its first item](https://docs.easyparser.com/media/animations/docs-bulk-overview-item-result.jpg)

*Animation: on the Bulk Requests page, open a job, then open the parsed result of its first item* ([MP4](https://docs.easyparser.com/media/animations/docs-bulk-overview-item-result.mp4))

## When to use the Bulk API

- Collect data for thousands of ASINs, keywords or sellers in one submission.
- Run scheduled jobs such as daily price, offer or ranking snapshots.
- Process inputs where you do not need the answer in the same HTTP response.

To get data immediately in a single synchronous call, use the [Real-Time API](/real-time-integration/overview).

## Endpoints and authentication

| Service | Method and URL | Purpose |
|---|---|---|
| Bulk Service | `POST https://bulk.easyparser.com/v1/bulk` | Submit a bulk job |
| Data Service | `GET https://data.easyparser.com/v1/queries/{id}` | Get the query record, including its `status` |
| Data Service | `GET https://data.easyparser.com/v1/queries/{id}/results?format=json` | Get the query record with the parsed JSON result (no raw HTML) |

Both services authenticate with your API key in the `api-key` header. The `Authorization: Bearer` header is not supported. Send `Content-Type: application/json` when you submit a job.

You find your API key in the web app under **Account > Plan** at https://app.easyparser.com/account/plan. New accounts get 100 free credits per month, with no credit card required. For a visual walkthrough, see [Getting Started](/getting-started).

## Identifiers

A bulk job returns three kinds of IDs. Only the result `id` is used to fetch data.

| Identifier | Where it appears | What it identifies |
|---|---|---|
| `bulk_request_id` | `meta_data.bulk_request_id` | The whole submission. Use it to find the job on the [Bulk Requests](https://app.easyparser.com/bulk-requests) page of the web app. |
| `group_id` | `data.accepted[].group_id` | One group (one object in the request array). |
| `id` | `data.accepted[].results[].id` | One query (one input value). Pass it to the Data Service. |

## Example flow

### Submit the job

This request runs the Search operation (`SEARCH`) on `.ca` for two search result URLs.

**cURL**

```bash
curl --location 'https://bulk.easyparser.com/v1/bulk' \
--header 'api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '[
  {
    "platform": "AMZ",
    "operation": "SEARCH",
    "domain": ".ca",
    "callback_url": "https://example.com/webhook",
    "payload": {
      "urls": [
        "https://www.amazon.ca/s?k=mouse",
        "https://www.amazon.ca/s?k=table+tennis"
      ]
    }
  }
]'
```

### Read the result IDs

The response lists one result `id` per accepted input value (abbreviated):

```json
{
  "success": true,
  "meta_data": {
    "bulk_request_id": "01a0341d-c6a8-722b-9189-2f2ee09608ef",
    "total_count": 2,
    "accepted_count": 2,
    "invalid_count": 0,
    "insufficient_credit_count": 0,
    "failed_count": 0,
    "rate_limit_exceeded_count": 0
  },
  "data": {
    "accepted": [
      {
        "platform": "AMZ",
        "operation": "SEARCH",
        "domain": ".ca",
        "callback_url": "https://example.com/webhook",
        "group_id": "01a0341d-c6be-7ccb-b916-49e17627b560",
        "results": [
          { "url": "https://www.amazon.ca/s?k=mouse", "id": "250f8e2a-7614-40b3-9a40-def192b74810", "credit": 1 },
          { "url": "https://www.amazon.ca/s?k=table+tennis", "id": "cd4b5677-c8a7-47e1-91a1-966bc448e0e8", "credit": 1 }
        ]
      }
    ]
  }
}
```

### Wait for the webhook

Easyparser sends a webhook to `https://example.com/webhook` for each query when it finishes. See [Webhooks](#webhooks).

### Fetch each result

```bash
curl --location 'https://data.easyparser.com/v1/queries/250f8e2a-7614-40b3-9a40-def192b74810/results?format=json' \
--header 'api-key: YOUR_API_KEY'
```

`format` supports only `json`. To check whether a query is still `pending` or has ended as `success` or `failure`, call `GET https://data.easyparser.com/v1/queries/{id}` instead. The result structure for each operation is described in [Data Service Response](/bulk-integration/data-service-response).

## Webhooks

### Delivery and retries

- Easyparser sends one webhook per query (result `id`), not one per job, to the `callback_url` of the query's group.
- The body is the query record. Its `links` array contains the URL of the parsed result, so you can follow that link instead of building the URL from the `id`.
- Your endpoint must respond with HTTP `200` within 3 seconds. Otherwise the delivery is retried once, 5 minutes later. If the second attempt also fails, Easyparser stops trying. In the webhook logs the second attempt shows `retry_count` `2`.
- If 10 or more deliveries fail within one hour, Easyparser notifies you by e-mail so you can check that your endpoint is reachable.
- You can resend failed webhooks, one at a time or all failed ones of a job, from the job's webhook logs on the [Bulk Requests](https://app.easyparser.com/bulk-requests) page of the web app. A manual resend is a single attempt with no automatic retry.

![Animation: in the webhook logs of a job, a failed delivery with HTTP 500 is resent and its status changes to success](https://docs.easyparser.com/media/animations/docs-bulk-overview-webhook-resend.jpg)

*Animation: in the webhook logs of a job, a failed delivery with HTTP 500 is resent and its status changes to success* ([MP4](https://docs.easyparser.com/media/animations/docs-bulk-overview-webhook-resend.mp4))

The webhook is a notification only. Queries are processed whether or not the webhook is delivered, so you can always fetch results with the IDs from the submit response.

### Verify the signature

Each webhook includes an `X-Easyparser-Signature` header: a hex-encoded HMAC-SHA256 of the raw request body, keyed with your API key. Verification is optional, but recommended for production endpoints.

1. Take the raw request body exactly as received.
2. Compute an HMAC-SHA256 of it with your API key as the secret, as a hex string.
3. Compare the result with the `X-Easyparser-Signature` header using a constant-time comparison. If they match, the request came from Easyparser.

> **WARNING:** Always sign the raw, unparsed body. Parsing and re-serializing the JSON, even without changing values, can change the bytes and make the signature mismatch.

**Python**

```python
import hmac
import hashlib

def verify_signature(raw_body: bytes, signature_header: str, api_key: str) -> bool:
    # raw_body must be the exact bytes received, not a re-serialized dict
    expected = hmac.new(
        api_key.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")
```

**Node.js**

```javascript
const crypto = require("crypto");

function verifySignature(rawBody, signatureHeader, apiKey) {
  // rawBody must be the exact Buffer or string received, not a re-serialized object
  const expected = Buffer.from(
    crypto.createHmac("sha256", apiKey).update(rawBody).digest("hex")
  );
  const received = Buffer.from(signatureHeader || "");

  // timingSafeEqual throws if the lengths differ
  return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
```

## Limits and data retention

- A single bulk request accepts up to 5,000 input values, counted across all groups. If you send more, the values that fit are processed and the rest are returned in `data.invalid`.
- Each query has a processing timeout of 5 minutes. A query that does not finish in time ends with status `failure`.
- Inputs count against your plan's Bulk API per-minute limit, measured in credits (`minutely_bulk_credit_limit` in the [Account API](/account/account-api)). Inputs above the limit are not accepted and are returned in `data.rate_limit_exceeded`; the rest of the request is still accepted.
- Only supported platforms and operations are accepted. Input values that fail validation are returned in `data.invalid` with a `message`. See [Bulk Service Response](/bulk-integration/bulk-service-response).
- Data Service retrieval is limited to your plan's per-minute Bulk limit plus 20% (for example, a 1,000 per minute Bulk limit allows 1,200 result retrievals per minute).

> **WARNING:** Results and bulk records are kept for at most 24 hours and may be removed earlier. Fetch and store results on your side as soon as they are ready. An expired or unknown `id` returns a not found error (HTTP `404`, or `success: false` with `error_code` `3404`).

## Credits

Each input value is charged with the same rules as the matching Real-Time API request, and only accepted inputs are charged. The Bulk API uses a single `page` per input value instead of `min_page` and `max_page`, so `SEARCH`, `SELLER_PRODUCTS` and `SELLER_FEEDBACK` inputs cost 1 credit per input value. Each accepted result carries its cost in `credit`. When your credits run out, inputs are accepted in order until the balance is used up and the rest are returned in `data.insufficient_credit`. See [Credit Consumption](/essentials/credit-consumption#bulk-api-credits).

## Related

- [Bulk Service Request](/bulk-integration/bulk-service-request): request body, payload fields per operation and examples
- [Bulk Service Response](/bulk-integration/bulk-service-response): accepted, invalid and rejected items
- [Data Service Request](/bulk-integration/data-service-request): fetching results by `id`
- [Data Service Response](/bulk-integration/data-service-response): query record and result structure
- [Credit Consumption](/essentials/credit-consumption)
