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

# Integrating a platform

> Open a deal for every job, failed ones included, fetch the original files, never miss a job, and erase an end user's data.

A platform that sends its own users' deals to Parser (a CRM, a funder portal, a broker tool) needs four things
beyond the result: a deal for every packet, the original documents, no lost jobs, and a way to honour an end
user's deletion request. This guide covers each.

## Tag each submission with your ids

`POST /v1/parse` takes your ids as form fields. Both are recorded with the job and echoed back; neither changes
how the documents are read.

| Field | Use |
| - | - |
| `end_user_id` | Your record of the end user (the merchant or the account) the packet belongs to. Filter jobs by it, and erase all of an end user's jobs in one call (below). |
| `external_business_id` | Your id for the merchant. It links the merchant's submissions to each other (`related_jobs`), and filters jobs. |

```bash theme={null}
curl -sS -X POST "$API/v1/parse" -H "X-API-Key: $KEY" -H "Idempotency-Key: deal-1042" \
     -F files=@application.pdf -F files=@march.pdf \
     -F broker_email=deals@iso.example -F business_name="Blue Fern Bakery LLC" \
     -F end_user_id=user-381 -F external_business_id=crm-42
```

## Open a deal for every job, failed ones included

Every job carries `intake`, whatever its status: how the packet came in and what it declared. It is on
`GET /v1/jobs/{job_id}`, on every row of `GET /v1/jobs`, and in the payload of every job event, `job.failed`
included. A job that failed before anything could be read (an unreadable file, a run that timed out) still
names its broker, its email and its files, so you can open the deal and ask for better documents.

```json theme={null}
"intake": {
  "source": "email",
  "received_at": "2026-10-14T20:29:41Z",
  "broker_email": "broker@example-iso.com",
  "business_name": null, "business_ein": null, "external_business_id": null, "end_user_id": null,
  "contact_email": null, "requested_advance_amount": null,
  "email": {"email_id": "em_3f1c0e9a6b2d4c8e1a7f5b90", "provider_message_id": "deal-123@example-iso.com",
            "mailbox_id": "…", "from": "broker@example-iso.com",
            "to": ["submissions@funder.example"], "subject": "New deal: Blue Fern Bakery",
            "received_at": "2026-10-14T20:29:41+00:00", "attachment_count": 3, "processable_attachments": 2,
            "thread_id": "…/deal-123@example-iso.com", "supersedes": null,
            "routing": {"intent": "new_submission", "confidence": 0.97, "model": "…", "served_model": "…"}},
  "files": [{"position": 0, "filename": "march.pdf", "content_type": "application/pdf",
             "sha256": "…", "bytes": 482113, "from_email": true}]
}
```

| Field | What it holds |
| - | - |
| `source`, `received_at` | `upload` or `email`; when the email was received or the upload submitted. |
| `broker_email` | The ISO or broker: the `broker_email` form field, or an email's sender. |
| `business_name`, `business_ein`, `external_business_id`, `end_user_id`, `contact_email`, `requested_advance_amount` | What an upload declared; null when not sent (an email declares none). |
| `email` | An emailed job's envelope: the [tracked email](/guides/emails) that started it (`email_id`), sender (`from`), recipients (`to`), `subject`, message id, mailbox, thread (`thread_id`), the job it replaces in its thread and the routing verdict. Null for an upload. The body is never in it. |
| `files` | Every file of the request, in order (below). |

`intake` never carries a file's contents or an email's body. The merchant's details as read from the documents
are in the result (`submission_packet.submission_intelligence.company_profile`, `applications[].fields`), once the
job has one.

## Fetch the original files

```bash theme={null}
curl -sS "$API/v1/jobs/$JOB_ID/files" -H "X-API-Key: $KEY"              # position, filename, type, sha256, bytes, from_email
curl -sS "$API/v1/jobs/$JOB_ID/files/0" -H "X-API-Key: $KEY" -OJ         # the bytes, as sent
```

Each file comes back exactly as it was uploaded or attached (its `sha256` matches), with
`Content-Disposition: attachment` and the content type it was declared with. Both calls need `jobs:read`, and
answer `404` for another tenant's job, an erased job or a position the job does not have. For an emailed job the
files are the email's attachments that were taken (PDF, PNG and JPEG). The message itself is
`GET /v1/emails/{intake.email.email_id}/raw`, as received, unless its mailbox turned `store_raw_email` off.

## Follow every email of a deal

An emailed deal is a thread. `intake.email.thread_id` names it, and `GET /v1/emails/threads/{thread_id}` lists
every email of it in order (the submission, replies, forwards, questions), each with its outcome and job. To track
every email as it arrives, ignored ones included, subscribe an endpoint to
[`email.received`](/guides/webhooks#email-received); tag emails with your own labels to file them. See
[Emails and threads](/guides/emails).

## Never miss a job

Webhooks are the primary channel: each job's final event is written with its status, and retried for about
23 hours ([the schedule](/guides/webhooks#delivery)). Two more tools close the gaps:

* **Polling**: `GET /v1/jobs?finished_since=` lists every job that finished since a time, oldest finish first,
  paged with `cursor`. Each finished job appears once across the pages, and a job erased between two pages does
  not break the cursor.
* **Re-sending**: a delivery that died (your endpoint was down) can be sent again, one at a time or every dead
  one since a time ([re-send](/guides/webhooks#missed-events)). It is the same event, with the same
  `X-Parser-Event-Id`.

A sync loop that cannot lose a job:

```python theme={null}
from datetime import UTC, datetime

import requests

def sync(api: str, key: str, since: float, upsert) -> float:
    """Pass every job finished since ``since`` (unix seconds) to ``upsert``; return the next ``since`` to store."""
    params = {"finished_since": datetime.fromtimestamp(since, UTC).isoformat(), "limit": 200}
    newest = since
    while True:
        page = requests.get(f"{api}/v1/jobs", params=params, headers={"X-API-Key": key}, timeout=30).json()
        for job in page["jobs"]:
            upsert(job)  # keyed by job["id"]: open or update the deal from job["status"] and job["intake"]
            newest = max(newest, job["completed_at"])
        if not page["next_cursor"]:
            return newest - 300  # the next run overlaps the last five minutes
        params["cursor"] = page["next_cursor"]
```

Keep the filters the same on every page. Start each run a few minutes before the last finish you saw: a job's
finish time is taken a moment before it is committed, so a job can appear just behind one already listed. Your
upsert keyed by the job id makes the overlap harmless. Run it every few minutes, or after your endpoint has been
down.

## Erase an end user's data

```bash theme={null}
curl -sS -X DELETE "$API/v1/jobs?end_user_id=user-381" -H "X-API-Key: $KEY"
# {"end_user_id": "user-381", "erased": ["…", "…"], "still_running": []}
```

Every finished job submitted with that `end_user_id` is erased exactly as `DELETE /v1/jobs/{job_id}` erases one:
its request and result, its input files, the cached reads of its documents, the model answers it paid for, its
metering, email thread entries, the [emails that fed it](/guides/emails#erasure-and-retention) with their raw
messages, and webhook events. Each erasure is in the audit log (`job.delete`, with the
`end_user_id`). A job still queued or running is not erased but listed in `still_running`: call again once it
ends; the call is safe to repeat. It needs `jobs:delete` and never touches another tenant's jobs.

Jobs that came in by email carry no `end_user_id` (an email declares none): erase those one by one with
`DELETE /v1/jobs/{job_id}`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.