Solution Flow
Sync vs. async patterns, idempotency, retries, error propagation, concurrency.
Sequence (full pipeline)
sequenceDiagram
autonumber
participant Mail as Mailbox
participant D365 as D365 CS
participant Orch as Orchestrator
participant APIM as Azure APIM
participant FAC as Faction APIs
participant Rep as Sales Rep
participant K8 as K8 (ERP)
Mail->>D365: Inbound email
D365->>Orch: Case created event
Orch->>APIM: POST /v1/intent/classify
APIM->>FAC: forward (auth, correlation_id)
FAC-->>APIM: 200 { intent, confidence, rationale }
APIM-->>Orch: 200
alt intent == quote
par parallel calls
Orch->>APIM: POST /v1/extract/quote
APIM->>FAC: forward
FAC-->>APIM: 200 { line_items, delivery, ... }
APIM-->>Orch: 200
and
Orch->>APIM: POST /v1/match/customer
APIM->>FAC: forward
FAC-->>APIM: 200 { customer_id, ship_to, ... }
APIM-->>Orch: 200
end
Orch->>APIM: POST /v1/match/product (uses extracted line_items)
APIM->>FAC: forward
FAC-->>APIM: 200 { matches, alternatives, unmatched }
APIM-->>Orch: 200
Orch->>D365: Update case with enriched fields
D365->>Rep: Pre-populated draft quote
Rep->>D365: Review, edit, confirm
D365->>K8: Create quote (existing flow)
else intent != quote
Orch->>D365: Route case per non-quote rules
endSync vs. async
Faction supports both. The orchestrator chooses per call based on payload.
| Module | Default mode | Async trigger |
|---|---|---|
| Intent Classifier | Sync | Payload over 10 MB |
| Quote Info Extractor | Sync | Payload over 10 MB or scanned PDFs detected |
| Customer Matcher | Sync | Never (always sync) |
| Product Matcher | Sync | Line-item count over 200 |
Async polling flow
POST /v1/extract/quote
Prefer: respond-async
HTTP/1.1 202 Accepted
{ "job_id": "job_01J...", "status_url": "/v1/jobs/job_01J..." }
GET /v1/jobs/job_01J...
HTTP/1.1 200 OK
{ "status": "processing" }
GET /v1/jobs/job_01J...
HTTP/1.1 200 OK
{ "status": "complete", "result": { ... } }Async webhook flow
POST /v1/extract/quote
Prefer: respond-async
Faction-Callback-Url: https://example.com/api/faction/callbacksFaction calls back with the result when ready. Callback delivery is retried up to 5 times with exponential backoff.
Idempotency
Every call accepts an optional Idempotency-Key header. If a key is reused within 24 hours with the same payload, Faction returns the original response without re-processing.
POST /v1/match/product
Idempotency-Key: case-CRM-2026-04-29-00417-product-match-v1
X-Correlation-Id: 8f3c-b21Reusing a key with a different payload returns 409 Conflict.
Retry semantics
| Failure | Recommended retry behavior |
|---|---|
| 5xx response | Retry with exponential backoff, up to 3 attempts. Reuse Idempotency-Key. |
| 429 rate-limited | Honor Retry-After header. |
| Timeout (no response) | Retry with same Idempotency-Key. |
| 4xx (other than 429) | Do not retry. Surface to operations team. |
Error propagation
Faction returns structured errors. The orchestrator can surface them to D365 as case notes or route them to a dead-letter queue.
{
"error_code": "EXTRACTION_PARTIAL_FAILURE",
"message": "2 of 3 attachments processed; 1 attachment was password-protected and skipped.",
"correlation_id": "8f3c-b21",
"details": {
"skipped_attachments": [
{ "filename": "rfq_v2.pdf", "reason": "password_protected" }
]
}
}A partial-success response (HTTP 422 or 200 with degraded results) is preferred over a hard failure where possible. The orchestrator decides whether to proceed or escalate.
Concurrent calls
The four modules can be called concurrently for the same case_id. Faction maintains case-scoped context internally during the request window so concurrent calls share state where useful (e.g., extracted line items can inform product matching even if both are called in parallel). Context is not persisted beyond the request window.
Cancellation and timeouts
| Scenario | Behavior |
|---|---|
| Client closes connection during sync call | Server completes the work but result is discarded. Repeating the call with the same Idempotency-Key returns the cached result. |
| Async job marked stale (no poll for 60 min) | Job remains queryable for 24 hours; result is purged after that. |
| Per-tenant request timeout | Configurable. Default 30 s for sync calls; raise to 90 s for extraction over large attachments. |