> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/azfar-imtiaz/PayPulse-Cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest retail invoices

> Fetch retail invoice HTML emails from Gmail for all configured vendors and upload them to S3.

## Authentication

All requests to this endpoint must include a valid JWT in the `Authorization` header.

```
Authorization: Bearer <token>
```

## What this endpoint does

This endpoint queries the `VendorConfig` table for all active vendors, then searches the user's Gmail account for matching invoice emails within the determined date range. HTML content from matching emails is uploaded to S3. Duplicate emails (already present in S3) are skipped.

If no custom date range is provided, the search window is determined automatically:

* **First fetch**: last 30 days.
* **Subsequent fetches**: from the `last_retail_invoice_fetch` timestamp stored on the user record.

After a successful run, the user's `last_retail_invoice_fetch` timestamp is updated.

The Gmail account must be connected before calling this endpoint. See [Store Gmail tokens](/api-reference/auth/store-gmail-tokens).

## Request body

The request body is optional. All fields are optional.

<ParamField body="start_date" type="string">
  Start of the custom search date range, in `YYYY-MM-DD` format. Must be provided together with `end_date`.
</ParamField>

<ParamField body="end_date" type="string">
  End of the custom search date range, in `YYYY-MM-DD` format. Must be provided together with `start_date`.
</ParamField>

<ParamField body="vendor_category" type="string">
  Optionally restrict processing to a single vendor category. Valid values: `food-delivery`, `clothing`, `technology`, `subscriptions`, `grocery`, `utility`, `miscellaneous`, `travel`.
</ParamField>

## Response

<ResponseField name="message" type="string">
  A human-readable confirmation message. Example: `"Retail invoices fetched successfully"`
</ResponseField>

<ResponseField name="code" type="number">
  HTTP status code. `200` on success.
</ResponseField>

<ResponseField name="data" type="object">
  <Expandable title="properties" defaultOpen>
    <ResponseField name="totalEmailsFound" type="number">
      Total number of new retail invoice emails ingested across all vendors.
    </ResponseField>

    <ResponseField name="byVendor" type="object">
      A map of vendor ID to the number of new emails ingested for that vendor. Example: `{"dominos": 2, "foodora": 3}`.
    </ResponseField>

    <ResponseField name="dateRange" type="object">
      The date range that was searched.

      <Expandable title="properties">
        <ResponseField name="start" type="string">
          Start date of the search window in `YYYY/MM/DD` format.
        </ResponseField>

        <ResponseField name="end" type="string">
          End date of the search window in `YYYY/MM/DD` format.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="errors" type="array">
      List of per-email or per-vendor errors encountered during processing. Capped at 20 entries. An empty array indicates no errors.
    </ResponseField>
  </Expandable>
</ResponseField>

## Error responses

| Status | Error code              | Description                                                                       |
| ------ | ----------------------- | --------------------------------------------------------------------------------- |
| `400`  | `MISSING_FIELDS`        | Only one of `start_date` / `end_date` was provided, or a required key is missing. |
| `400`  | `INVALID_JSON`          | The request body is not valid JSON.                                               |
| `401`  | `INVALID_CREDENTIALS`   | The `Authorization` header is missing or the OAuth token is invalid.              |
| `401`  | `TOKEN_EXPIRED`         | The JWT has expired.                                                              |
| `404`  | `USER_NOT_FOUND`        | No account exists for the user ID encoded in the token.                           |
| `502`  | `DEPENDENCY_FAILURE`    | The Gmail API returned an error.                                                  |
| `502`  | `GMAIL_TOKEN_EXPIRED`   | The Gmail OAuth refresh token has expired and the account must be re-connected.   |
| `500`  | `INTERNAL_SERVER_ERROR` | An unexpected server-side error occurred.                                         |

Error responses follow this structure:

```json theme={null}
{
  "error": {
    "code": "MISSING_FIELDS",
    "message": "Both start_date and end_date must be provided for custom date range"
  }
}
```

<RequestExample>
  ```bash cURL (automatic date range) theme={null}
  curl --request POST \
    --url https://api.paypulse.io/v1/invoices/retail/ingest \
    --header 'Authorization: Bearer <token>'
  ```

  ```bash cURL (custom date range) theme={null}
  curl --request POST \
    --url https://api.paypulse.io/v1/invoices/retail/ingest \
    --header 'Authorization: Bearer <token>' \
    --header 'Content-Type: application/json' \
    --data '{
      "start_date": "2025-01-01",
      "end_date": "2025-03-21"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "message": "Retail invoices fetched successfully",
    "code": 200,
    "data": {
      "totalEmailsFound": 5,
      "byVendor": {
        "dominos": 2,
        "foodora": 3
      },
      "dateRange": {
        "start": "2025/02/19",
        "end": "2025/03/21"
      },
      "errors": []
    }
  }
  ```

  ```json 200 (no vendors configured) theme={null}
  {
    "message": "No active vendors configured for retail invoice fetching",
    "code": 200,
    "data": {
      "totalEmailsFound": 0,
      "byVendor": {},
      "dateRange": {
        "start": "2025/02/19",
        "end": "2025/03/21"
      }
    }
  }
  ```

  ```json 502 theme={null}
  {
    "error": {
      "code": "GMAIL_TOKEN_EXPIRED",
      "message": "Gmail account needs to be re-connected"
    }
  }
  ```
</ResponseExample>
