Skip to main content

Records

Send JSON records with your own field names:
An upload carries exactly one document type, declared when you open it.

Document types

invoice and purchase_order are mapped types. A purchase order needs the order number, the customer’s name, and the order date. An invoice needs the invoice number, the invoice date, and the total amount. Pascal normalizes dates in common formats and reports an unparseable required date as missing. A mapped upload may contain at most 20 distinct record layouts. Any other declared name is a custom type, stored as sent. There are no requirements around custom types, and they are not searchable. Ids are unique across the datasource. Re-sending an id under a different type moves the record to that type.

Open an upload

Send POST /uploads with the document type this upload carries:
An undeclared doc_type returns 400 undeclared_doc_type. Choose incremental or full using the mode table. The 201 response includes upload_id, status: "in_progress", mode, dry_run, datasource, doc_type, and created_at. Save the upload id. An in_progress upload is writable while closed_at is null. Opening another upload of the same type expires the previous writable one and discards its staged records; other types keep their uploads. If the current upload of that type has closed_at set, opening returns 409 upload_processing with Retry-After: 30.

Send records

Send batches to PUT /uploads/{upload_id}/records:
A record carrying doc_type returns 400 invalid_request. Each call allows at most 1,000 records and 10 MB (10,485,760 bytes). Pascal validates the entire batch before any write. A rejected batch stages nothing. Invalid ids return 400 invalid_record. Its details.records entries contain index, id, and reason: invalid_id. A non-object data value returns 400 invalid_request. Sending the same id again within an upload replaces its staged value: last write wins. Retrying the same records is safe. The response contains accepted (records accepted in this batch), upload_id, and record_count (distinct staged ids).

Close

Call POST /uploads/{upload_id}/close. Close sets closed_at and returns 202 with upload_id and status. Processing is asynchronous. Close rejects further writes. Repeated closes return the current status, except expired uploads return 410 upload_expired. If scheduling fails, close returns 503 publish_failed with Retry-After: 5. Wait five seconds and close again to retry scheduling.

Poll

Call GET /uploads/{upload_id}. While the upload is in_progress with closed_at set, honor Retry-After: 15. The response includes record count and timestamps. merged_counts holds inserted, updated, and deleted after a successful merge. report is null until validation finishes; then it holds status, errors, and per_type. A mapped upload gets one per_type entry per distinct record layout; a custom upload gets exactly one. Rejected or failed uploads carry an error envelope. See Errors and recovery. The hourly sweep expires writable uploads after at least 24 hours without a write. Worker failures leave closed uploads in_progress during retries. After at least one hour of processing, the stuck-upload sweep marks them failed with processing_timeout. Pascal discards staging for rejected, failed, and expired uploads.

Full snapshots and the deletion brake

full upserts submitted records and removes current records of the upload’s type that are absent from it. It never touches other types, so send each type its own full upload. With a non-empty current set for that type, removing more than 20% or submitting zero records rejects the upload with too_many_deletions. Nothing changes. Error details include current_count, would_delete, and max_delete_fraction. The 20% threshold applies to every datasource. Resend the intended full set for that type, and contact Pascal if a larger removal is intentional.

Dry runs

Set dry_run: true when opening. Close validates the records and returns done or rejected with a report. It discards staging and leaves merged_counts null. Records and each type’s last accepted upload remain unchanged. A dry run never merges, so it never evaluates the deletion brake. A full dry run returns done even when the real upload would fail with too_many_deletions.

Inspect the datasource

Call GET /datasources/{name} with your bearer token. The name must match the token’s datasource, or the response is 403 datasource_mismatch.