Skip to main content
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.

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

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; tag emails with your own labels to file them. See Emails and threads.

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). 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). It is the same event, with the same X-Parser-Event-Id.
A sync loop that cannot lose a job:
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

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