Appearance
Submitting documents
Driving orders via the API
Most tenants receive orders through the mailbox AIOTIC watches. The API adds two more intake paths — for portals, scanners, EDI gateways, or your own mail handling.
Files → one order
POST/order/uploadintegration key
multipart/form-data. All files together form one purchase order (a PO with a separate price list, or a scan of two pages). Optional request_id (UUID v4) and any extra form fields, which are stored as metadata.
bash
curl -X POST https://acme.aiotic.ai/order/upload -H "X-API-Key: $KEY" \
-F "files=@PO-4711.pdf;type=application/pdf" \
-F "request_id=7f0c2b6e-1c2a-4f0e-9d4c-2c1a0b7e5a11" \
-F "source=portal" -F "my_reference=TICKET-8812"
# {"request_id":"7f0c2b6e-1c2a-4f0e-9d4c-2c1a0b7e5a11","split":false}python
import uuid
up = client.orders.upload(["PO-4711.pdf"], request_id=uuid.uuid4(), metadata={"source": "portal", "my_reference": "TICKET-8812"})
status = client.orders.wait(up.request_id) # polls with backoff until it landsAccepted types: .pdf .jpg .jpeg .png .txt .md. Keep files under ~10 MB.
Always pass your own request_id
It makes the upload idempotent (a retry after a network error does not create a second order) and gives you the id before the call returns — handy for logging and for correlating in your ERP.
Raw e-mail → one or more orders
POST/order/raw/uploadintegration key
Upload a complete .eml (headers, body, attachments). AIOTIC classifies it first:
- Purchase order → processed like a mailbox mail. The e-mail body counts as input ("please change line 3 to 200 pcs" overrides the attachment).
- Not a purchase order →
400with a structureddetail(error: not_a_purchase_order, the detectedcategory) and the mail is recorded under rejected e-mails so an operator can override. - Several orders in one mail (tenant feature order splitting) →
split: trueplus one child order per detected order. Poll the children, not the source id.
python
from aiotic import AioticValidationError
try:
up = client.orders.upload_raw_email("mail.eml")
except AioticValidationError as e:
if isinstance(e.detail, dict) and e.detail.get("error") == "not_a_purchase_order":
log.info("skipped %s: %s", e.detail["category"], e.detail["subject"])
raise SystemExit(0)
raise
for rid in up.request_ids: # 1 id, or the children when split
status = client.orders.wait(rid)
if up.split:
group = client.orders.group(up.email_group_id) # aggregate viewClassify without processing (dry run): POST /order/raw/classify → {"category": "purchase_order"}.
After the upload
You get a request_id immediately; processing runs in the background. Continue with Polling & notifications.
What you cannot do through upload
- Split multipart uploads into several orders — use the raw e-mail path.
- Attach a customer id up front. AIOTIC identifies the customer itself. (Your extra form fields land in
metadatabut are not used for identification.) - Provide corrections — see Headless limits today.