API docs
How to test BridgeOps from a terminal: send invoices in through every intake (REST, signed webhook, CSV, AI extractor, manual form), watch policy decide, approve as a finance manager and verify the audit chain.
All data here is mock-up data. DemoCorp, its suppliers, purchase orders, users, keys and the ERP are synthetic. The demo resets every 6 hours.
Quick start
Base URL https://bridgeops.enthernetservice.com/api. Send a token or key as Authorization: Bearer ….
# 1. An ERP-bound invoice over 5,000 USD arrives over REST
curl -s -X POST -H "Authorization: Bearer demo-key-rest" -H "Content-Type: application/json" \
-d '{"workflow_type":"invoice","payload":{"supplier_id":"SUP-1001","invoice_id":"INV-Q1","amount":"8900.00","currency":"USD","purchase_order":"PO-5001"}}' \
https://bridgeops.enthernetservice.com/api/v1/events
# → {"workflow_id":"wf_…","status":"APPROVAL_REQUIRED","ai_used":"NO",…}
# 2. Look at it
curl -s -H "Authorization: Bearer demo-token-u_view" https://bridgeops.enthernetservice.com/api/v1/workflows/<workflow_id>
# 3. The operator can't approve it…
curl -s -X POST -H "Authorization: Bearer demo-token-u_ops" -H "Content-Type: application/json" \
-d '{"reason":"looks fine"}' https://bridgeops.enthernetservice.com/api/v1/workflows/<workflow_id>/approve
# → 403 role OPERATOR lacks permission 'approve'
# 4. …the finance manager can, and BridgeOps writes it to the ERP and reads it back
curl -s -X POST -H "Authorization: Bearer demo-token-u_finance" -H "Content-Type: application/json" \
-d '{"reason":"Matches PO-5001"}' https://bridgeops.enthernetservice.com/api/v1/workflows/<workflow_id>/approve
# 5. Check the audit chain
curl -s -H "Authorization: Bearer demo-token-u_audit" https://bridgeops.enthernetservice.com/api/v1/audit/verifyTry it
Requests go to this server's API with the credential above.
Response appears here.
Credentials
Demo credentials for synthetic data only; the app refuses to start with them outside demo mode. User tokens can also be fetched with POST /v1/demo/login. Each machine key only works for its own intake.
| Bearer token or key | Who / what |
|---|---|
| demo-token-u_owner | owner@democorp.example · OWNER · everything |
| demo-token-u_ops | ops@democorp.example · OPERATOR · submit, run scenarios, correct, requeue |
| demo-token-u_finance | finance.manager@democorp.example · APPROVER · approve, reject |
| demo-token-u_audit | auditor@democorp.example · AUDITOR · read, verify the audit chain |
| demo-token-u_view | viewer@democorp.example · VIEWER · read only |
| demo-key-rest | REST intake key (source: rest) · POST /v1/events |
| demo-key-csv | CSV intake key (source: csv) · POST /v1/csv |
| demo-key-ai | AI extractor key (source: ai) · POST /v1/ai/events |
| whsec_demo_synthetic | Webhook signing secret for source supplier-portal |
Endpoints
Intake (machines)
POST/v1/eventsSubmit an invoice from a system over REST
Who: REST API key (demo-key-rest)
curl -X POST -H "Authorization: Bearer demo-key-rest" -H "Content-Type: application/json" \
-H "Idempotency-Key: my-invoice-1" \
-d '{"workflow_type":"invoice","payload":{"supplier_id":"SUP-1001","invoice_id":"INV-T1","amount":"1200.00","currency":"USD","purchase_order":"PO-5001"}}' \
https://bridgeops.enthernetservice.com/api/v1/events{"workflow_id": "wf_69ef998d5c0742c98aab", "status": "POLICY_CHECKED", "ai_used": "NO", "failure_reason": null}Body must be exactly {workflow_type, payload}; the tenant comes from the key. Sending the same Idempotency-Key again returns the same workflow instead of a second invoice. Money is a decimal string.
POST/v1/webhooks/supplier-portalSigned webhook from the supplier portal
Who: HMAC signature (secret whsec_demo_synthetic)
BODY='{"workflow_type":"invoice","payload":{"supplier_id":"SUP-1001","invoice_id":"INV-T1","amount":"1200.00","currency":"USD","purchase_order":"PO-5001"}}'
TS=$(date +%s)
SIG="v1=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac whsec_demo_synthetic | sed 's/^.* //')"
curl -X POST -H "Content-Type: application/json" \
-H "X-BridgeOps-Timestamp: $TS" -H "X-BridgeOps-Signature: $SIG" -H "X-BridgeOps-Delivery: dlv-$TS" \
-d "$BODY" https://bridgeops.enthernetservice.com/api/v1/webhooks/supplier-portal{"workflow_id": "wf_8b048c8646ab46698ef6", "status": "POLICY_CHECKED", "ai_used": "NO", "failure_reason": null}Signature: v1=hex(HMAC-SHA256(secret, "<timestamp>." + raw_body)). A stale timestamp or a reused signature is rejected with 401 ("replayed request"); X-BridgeOps-Delivery is the idempotency key.
POST/v1/csvUpload a CSV of invoices; one workflow per row
Who: CSV API key (demo-key-csv) or a user who can submit
printf 'supplier_id,invoice_number,total,currency,purchase_order\nSUP-1001,INV-C1,300.00,USD,PO-5001\n' | \ curl -X POST -H "Authorization: Bearer demo-key-csv" -H "Content-Type: text/csv" --data-binary @- \ https://bridgeops.enthernetservice.com/api/v1/csv
{"workflows": [{"workflow_id": "wf_29cf…", "status": "POLICY_CHECKED", "ai_used": "NO", "failure_reason": null}]}Required columns: supplier_id, invoice_number, total, currency, purchase_order. UTF-8, up to 2 MB / 5,000 rows. A bad row fails on its own (for example SCHEMA_FAILED: "amount: must be a decimal number").
POST/v1/ai/eventsRaw output from an AI extractor, treated as untrusted input
Who: AI API key (demo-key-ai)
curl -X POST -H "Authorization: Bearer demo-key-ai" -H "Content-Type: application/json" \
-d '{"extracted":{"supplier_id":"SUP-1001","invoice_id":"INV-55120","amount":"4250.00","currency":"USD","purchase_order":"PO-5001"},"confidence":0.96,"source_document_id":"DOC-7781"}' \
https://bridgeops.enthernetservice.com/api/v1/ai/events{"workflow_id": "wf_0cc3…", "status": "SCHEMA_FAILED", "ai_used": "YES", "failure_reason": "AI output is not valid JSON"} ← if you send chatty text insteadEvery extracted value must appear in the referenced source document (DOC-7781 or DOC-7782), confidence below 0.90 needs an APPROVER, and output with extra keys (for example an injected "action": "POST_DIRECTLY_TO_ERP") is rejected as SCHEMA_FAILED. The workflow is stamped AI USED: YES.
People
POST/v1/manualSubmit an invoice by hand
Who: OWNER, OPERATOR
curl -X POST -H "Authorization: Bearer demo-token-u_ops" -H "Content-Type: application/json" \
-d '{"workflow_type":"invoice","payload":{"supplier_id":"SUP-1001","invoice_id":"INV-T1","amount":"1200.00","currency":"USD","purchase_order":"PO-5001"}}' https://bridgeops.enthernetservice.com/api/v1/manualPOST/v1/workflows/{id}/approveApprove a held invoice
Who: APPROVER, OWNER (never the person who submitted or corrected it)
curl -X POST -H "Authorization: Bearer demo-token-u_finance" -H "Content-Type: application/json" \
-d '{"reason":"Matches PO-5001"}' https://bridgeops.enthernetservice.com/api/v1/workflows/<id>/approve{"workflow_id": "wf_7710…", "status": "APPROVED", "ai_used": "NO", "failure_reason": null}The approval is bound to the payload hash: correcting the payload voids it. An operator gets 403 {"error":"PermissionDenied","detail":"role OPERATOR lacks permission 'approve'"}.
POST/v1/workflows/{id}/rejectReject a held invoice
Who: APPROVER, OWNER
curl -X POST -H "Authorization: Bearer demo-token-u_finance" -H "Content-Type: application/json" \
-d '{"reason":"Duplicate of INV-55120"}' https://bridgeops.enthernetservice.com/api/v1/workflows/<id>/rejectPOST/v1/workflows/{id}/correctFix the payload of a failed workflow and run it again
Who: OPERATOR, OWNER
curl -X POST -H "Authorization: Bearer demo-token-u_ops" -H "Content-Type: application/json" \
-d '{"payload":{"supplier_id":"SUP-1001","invoice_id":"INV-C2","amount":"300.00","currency":"USD","purchase_order":"PO-5001"}}' \
https://bridgeops.enthernetservice.com/api/v1/workflows/<id>/correctPOST/v1/workflows/{id}/requeue · /reverify · /cancelRecovery actions for dead letters, failed verification or cancelling
Who: OPERATOR, OWNER
cancel takes {"reason": "…"}; requeue and reverify take no body.
Reading
GET/v1/meWho the token belongs to
Who: any user token
curl -H "Authorization: Bearer demo-token-u_owner" https://bridgeops.enthernetservice.com/api/v1/me
{"id": "u_owner", "email": "owner@democorp.example", "role": "OWNER", "tenant_id": "democorp", "demo_mode": true, "ai_enabled": true}GET/v1/dashboardCounters, connector health and circuit-breaker state
Who: any user token
curl -H "Authorization: Bearer demo-token-u_view" https://bridgeops.enthernetservice.com/api/v1/dashboard
{"active": 0, "completed": 1, "approval_required": 1, "failed": 1, "retrying": 0, "dead_letter": 0, "needs_human": 2, "total": 4, "ai_assisted": 1,
"connectors": [{"id": "legacy-erp", "type": "http_erp", "circuit": "CLOSED", "healthy": true}, …]}GET/v1/workflows?status=…&limit=100List workflows, newest first
Who: any user token
curl -H "Authorization: Bearer demo-token-u_view" "https://bridgeops.enthernetservice.com/api/v1/workflows?status=APPROVAL_REQUIRED,SCHEMA_FAILED"
status is a comma-separated list of states; limit max 500.
GET/v1/workflows/{id}Everything about one workflow
Who: any user token
curl -H "Authorization: Bearer demo-token-u_view" https://bridgeops.enthernetservice.com/api/v1/workflows/<id>
{"workflow": {"id": "wf_…", "status": "COMPLETED", "business_key": "invoice:SUP-1001|INV-62058", "normalized_payload": {…},
"policy_decision": {"decision": "ALLOW", "facts": {"po_amount": "12000.00", "po_match": true, "supplier_active": true}, "reasons": [...]}},
"normalized_event": {…}, "ai_used": "NO", "approvals": [], "executions": [{"attempt": 1, "status": "SUCCEEDED", "response_reference": "INV-9202"}],
"audit": [{"event_type": "EVENT_RECEIVED", "event_hash": "ae78…", "previous_hash": "2072…"}, …]}GET/v1/audit/verifyRecompute the tenant's audit hash chain
Who: AUDITOR, OWNER
curl -H "Authorization: Bearer demo-token-u_audit" https://bridgeops.enthernetservice.com/api/v1/audit/verify
{"ok": true, "checked": 41, "head": "485af775cb2a…"}GET/healthService health
Who: anyone
curl https://bridgeops.enthernetservice.com/api/health
Demo controls
GET/v1/demo/usersThe demo users
Who: anyone
curl https://bridgeops.enthernetservice.com/api/v1/demo/users
POST/v1/demo/loginGet a demo user's token
Who: anyone
curl -X POST -H "Content-Type: application/json" -d '{"user_id":"u_ops"}' https://bridgeops.enthernetservice.com/api/v1/demo/login{"token": "demo-token-u_ops", "synthetic": true}POST/v1/demo/scenarios/{name}Run a scenario with a fresh synthetic invoice
Who: OPERATOR, OWNER
curl -X POST -H "Authorization: Bearer demo-token-u_ops" https://bridgeops.enthernetservice.com/api/v1/demo/scenarios/B
A = normal invoice, B = above the 5,000 USD threshold, C = AI extraction (C:malformed by default; also C:normal, C:uncertain, C:hallucinated, C:injected), D = ERP outage with retries (switches the mock ERP to down; put it back with erp-mode ok).
POST/v1/demo/erp-modeMake the mock ERP misbehave
Who: OPERATOR, OWNER
curl -X POST -H "Authorization: Bearer demo-token-u_owner" -H "Content-Type: application/json" \
-d '{"mode":"ok"}' https://bridgeops.enthernetservice.com/api/v1/demo/erp-modeModes: ok, down, timeout_after_commit (writes then times out: tests reconcile-before-retry), lie (says yes, writes nothing: tests verification), corrupt, reject.
States & policy
RECEIVED → AUTHENTICATED → NORMALIZED → VALIDATED → POLICY_CHECKED → (APPROVAL_REQUIRED → APPROVED) → EXECUTING → VERIFYING → COMPLETED
Stops that need a human: SCHEMA_FAILED, POLICY_BLOCKED, APPROVAL_REQUIRED, CONNECTOR_FAILED, VERIFICATION_FAILED, DEAD_LETTERED. Retries: RETRY_PENDING (demo delays 10s / 30s / 60s). Final: COMPLETED, CANCELLED, APPROVAL_REJECTED, AUTH_FAILED.
Invoice policy (invoice-default v1)
| Rule | Then | When |
|---|---|---|
| supplier-active | BLOCK | Supplier is unknown or inactive (SUP-1002 is inactive) |
| po-match | BLOCK | Invoice doesn't match an open purchase order on supplier, currency or amount (PO-5001: USD 12,000 · PO-5002: USD 3,000) |
| currency-threshold-defined | BLOCK | Currency has no approval threshold (only USD is configured) |
| high-value | REQUIRE_APPROVAL | Amount above 5,000 USD needs an APPROVER |
| ai-low-confidence | REQUIRE_APPROVAL | AI-extracted and confidence below 0.90 needs an APPROVER |
| (default) | ALLOW | No rule matched: create the payable in the legacy ERP, then verify it |
Missing fields, type mismatches or a missing policy all mean BLOCK. The most restrictive matching rule wins, and the ERP destination comes from the policy, never from the payload.
Errors
Errors look like {"error": "PermissionDenied", "detail": "role OPERATOR lacks permission 'approve'"}.
| 400 | Invalid JSON |
| 401 | Missing, unknown or wrong kind of credential; bad, stale or replayed webhook signature |
| 403 | Role lacks the permission, or separation of duties (you can't approve your own submission) |
| 404 | Not found, or another tenant's workflow |
| 413 | Body too large (256 KB for events, 2 MB for CSV) |
| 422 | Wrong body shape, missing X-BridgeOps-Delivery, unknown scenario or ERP mode |
| 429 | Rate limited by the server |
