Your end users forward receipts to one address you own (for example, receipts@yourorg.com). Your service decides which user the email belongs to, sends each receipt to the Veryfi Documents API, and stores the JSON against that user.
What you build: the mailbox, ingestion, a sender-to-user lookup, the API call, and storage.
What Veryfi provides: OCR and extraction for receipts and invoices (vendor, date, totals, tax, line items, payment), as well as fraud and validation checks and returns as JSON.
Why API instead of Veryfi’s Email Collector (@veryfi.cc): you keep your domain and the inbox rules, and a normal upload returns the JSON in the same HTTP response. Spam filtering, retries, and mailbox operations stay with you. Veryfi’s own collector is a different product. Mail to a @veryfi.cc address is processed asynchronously. By default, that collector merges every attachment in one email into one PDF and one document. Passing user identifiers via Email collector could be a challenge.
Suggested Architecture for Email Collector Implementation Guide: Shared Inbox + Veryfi API
Shared inbox data flow · 8 steps, 1 decision
User forwards a receipt to your shared address.
Your mail provider or your Rules checks SPF, DKIM, and DMARC before your service sees the message.
It might be optional in many cases; users initiate the email send, checking the validity of the message/content.
Your service stores the raw email, reads the authenticated From, and looks up the user.
Unregistered or failing-auth senders never reach Veryfi.
Your service chooses what exactly to send. *
You store the returned document id with your user id (external_id).
The app lists receipts from your database, scoped to the signed-in user.
SPF, DKIM, and DMARC are checks your mail provider runs. They stop someone spoofing a registered address. Read the provider’s result (usually Authentication-Results) and drop or quarantine failures. Veryfi does not do this check for you on a direct API upload.
Prerequisites
Veryfi API and credentials: Client ID and an API key from the Keys section of Veryfi Hub. A Bearer key is account-scoped (Authorization: Bearer <key> plus CLIENT-ID). A standard key uses Authorization: apikey <username>:<api_key>. Prefer a scoped key
Shared mailbox: a dedicated address, not a person’s inbox. Google Workspace, Microsoft 365, or an inbound parse service (SendGrid Inbound Parse, Mailgun Routes, Amazon SES).
User directory: verified sender addresses mapped to a stable internal user id.
Ingestion service: worker or function that can receive or poll mail and call HTTPS.
Storage: database for results, object storage for the raw email and attachments.
Webhook: not required for this guide. The upload below is synchronous. Add one only if you later switch that call to async.
SDKs exist for Python, Node.js, Java, .NET, Go, and others. REST is fine. Current reference: Process a Document.
Steps
1. Shared inbox
Create the address. Prefer push (inbound parse posts each message to your HTTPS endpoint). Pull (Gmail API, Microsoft Graph, or IMAP) also works. Turn on SPF, DKIM, and DMARC at the provider. Publish the address in the app, onboarding, and help center.
2. Ingest
Store the raw MIME untouched. Record Message-ID. Extract the authenticated From, subject, HTML and plain bodies, and attachments. Queue the Veryfi call so a slow extraction never blocks mail receipt.
3. Identify the user/submission
Look up your internal user id. No match: do not call Veryfi. Reply with how to register that address, and log it. On a forward, trust the authenticated From of the forwarder, not an unverified original sender inside the body. That From is the person who owns the receipt in your app.
4. Drop / Filter what should not be processed
Drop SPF/DKIM failures and mail your spam filter flags. Skip auto-replies, out-of-office, and bounces (Auto-Submitted, Precedence). - Only applicable if you implement inbox crawler, that will select emails that meet forwarding criteria.
Main lift: Skip tiny inline images (logos, signatures, tracking pixels). Enforce your own attachment count and size before calling the API.
5. Decide what becomes a document
By default, one POST creates one document. Veryfi does not split an email for you on this path. If the received file has multiple instances attached, like body+ attachment or body + multiple attachments Veryfi API will combine it all into one multi-page PDF and extract as if a multi-page document was uploaded, unless you handle what/how exactly is being send over to Veryfi.
One PDF or image: send that file.
Several receipts: one POST per file, same user id on each.
E-receipt in the body, no file: send the HTML as a .html file. Do not also send a logo image from the same message.
Body and attachment both look like the receipt: send the attachment only.
You want one document that contains several files: build one PDF yourself, or pass those files as file_urls on a single POST. file_urls is one document, not one document per URL.
A simple decision rule
Look for real attachments: PDF or image, marked attachment, and over a small size floor (around 20 KB, to skip logos). If there are any, send each one as its own document and ignore the body.
If there are none, the receipt is in the body. Send the HTML part as a .html file. This is the Uber case, and Veryfi renders the HTML and extracts from it.
If there’s no HTML part, send the plain-text body.
If nothing qualifies, skip the email and log it.
Both .html and .eml are accepted on the documents endpoint and are converted to PDF for extraction. Enabled by default also includes PDF and common images (.jpg, .png, .heic, .webp, and others). .txt, .doc, .docx, .odt, and .rtf are not enabled until support turns them on. If a type is off, the API returns The file type is not enabled for your client.
Platform limits: 20 MB per file, 250 bytes minimum, 15 pages per document, 60 requests per second.
6. Call the Documents API
POST https://api.veryfi.com/api/v8/partner/documents
Send the file as a multipart file (preferred), file_url (a URL Veryfi can fetch; the connect timeout is short, so the URL must be public and fast), or base64 file_data. Base64 is the least efficient of the three. Send exactly one of these inputs. file_name is optional but worth sending when you use file_data, with the real extension (.pdf, .jpg, .png, .html, .eml).
Put the end user’s stable id in external_id. That field is how you reconcile. It is not a uniqueness key. Many documents should share one user’s id. Do not put the email address (Address can change; keep personal data out)
tags such as email are optional. A new tag is created when you send it. Helps in future to filter volumes coming from email collector and /or other submission pipelines, if expected e.g lens capture.
POST /api/v8/partner/documents HTTP/1.1
Host: api.veryfi.com
CLIENT-ID: <your client id>
AUTHORIZATION: Bearer <api key>
Content-Type: application/json
Accept: application/json
Idempotency-Key: 9f1d2c3b-4a5e-6f70-8192-a3b4c5d6e7f8
{
"file_name": "uber-receipt-2026-11-02.pdf",
"file_data": "<base64>",
"external_id": "ldpc-user-48213",
"tags": ["email"]
}
The call is synchronous unless you set async to true. The extracted JSON is the HTTP 201 body. Set the client timeout below 120 seconds, Veryfi Internal timeout.
7. Store the result (optional)
Save the Veryfi document id, your user id, the external_id you sent, Message-ID, attachment hash, the idempotency key, and the JSON. Keep a status (received, processing, processed, failed) so support can trace an email.
external_id is returned when that field is enabled. It is on for a standard partner account. If it is missing from responses, ask support to enable it before you rely on list-by-external_id.
JSON Response (sample)
{
"id": 251630047,
"external_id": "ldpc-user-48213",
"vendor.name": "Uber",
"date": "2026-11-02 18:41:00",
"currency_code": "USD",
"subtotal": 21.40,
"tax": 1.83,
"total": 23.23,
"line_items": [],
"tags": "email
}
Fields you do not have enabled are omitted. Full schema: Process a Document.
Idempotency
Send Idempotency-Key on every upload. Optional, but it is what makes a retry safe.
A request that runs longer than 120 seconds can be cut off by the load balancer with a 504 and no body on your end, while the upload behind it still finishes and is billed. Retrying without the header creates a second document and a second charge. With the same key, the retry returns the original document.
Value: opaque string, at most 255 characters.
New key for every distinct upload (each attachment is its own upload). Reuse it only when retrying that same upload. Store it on the job.
Keys are scoped to your client.
If a retry arrives while the original is still running, the API can return 409 with A request with this Idempotency-Key is still being processed. Wait and retry the same key. Do not generate a new one.
A failed request releases the key, so a later retry with the same key is allowed to run again.
This synchronous call does not emit document.created. That event fires only when async is true and the event is enabled. A blind retry of a sync call still duplicates the document and the charge
Errors
Retry only what can succeed on a second try. Dedupe on Message-ID plus attachment hash so the same email is never sent twice.
Veryfi responses
504, timeout, or 5xx: retry with backoff (for example 30s, 2 min, 10 min) and the same Idempotency-Key. Then mark failed and alert.
409 idempotency in flight: wait, retry the same key. Not a permanent failure.
429: back off. Drain bursts through your queue. Default limit is 60 requests per second.
400: do not retry. Includes a bad or unsupported file, a type that is not enabled, a file that is too large or too small, and File is encrypted (password-protected PDF). Tell the user what to resend.
401 or 403: do not retry. Alert: credentials or access.
Veryfi Error catalog: API error responses.
Python
import base64
import uuid
import requests
def send_to_veryfi(file_bytes, file_name, user_id, idempotency_key):
resp = requests.post(
"https://api.veryfi.com/api/v8/partner/documents",
headers={
"CLIENT-ID": CLIENT_ID,
"AUTHORIZATION": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={
"file_name": file_name,
"file_data": base64.b64encode(file_bytes).decode(),
"external_id": user_id,
"tags": ["email"],
},
timeout=120,
)
resp.raise_for_status()
return resp.json()
# Generate idempotency_key once per upload and persist it on the job.
# Pass that same value on every retry. Do not derive it from user_id.
Prefer the multipart file field, or file_url, over base64 when you can.
Go-live checklist
Run these with at least two test users before you publish the address.
☐ Single PDF lands on the right user, with the external_id you sent.
☐ HTML e-receipt with no attachment is extracted from the body you sent, not from a logo image.
☐ Three attachments become three documents for the same user (three POSTs).
☐ An unregistered sender gets the registration reply, and nothing is sent to Veryfi to process
☐ A retried upload does not create a second document.
☐ Failed jobs are visible to support, with the raw email available for replay.
Support: support@veryfi.com. Include the Veryfi document id and your external_id.

