Rejected (error returned)
Background (after reply sent)
flowchart TD
A([" 📨 CargoWise sends XML\n POST /api/cargowisedata/receive "]):::start
A --> B{{"🔐 Is the caller\nauthenticated?\nBearer or Basic"}}:::decision
B -- "❌ No token\nor wrong token" --> C(["🚫 401 Unauthorised\nReason explained\nin response body"]):::fail
B -- "✅ Valid token" --> D{{"📋 Are required headers\npresent?\nRecipientID + EDIClientName"}}:::decision
D -- "❌ Missing\nheaders" --> E(["🚫 400 Bad Request"]):::fail
D -- "✅ Headers OK" --> F{{"📂 Is this sender\nregistered?\ntblMessageConfig"}}:::decision
F -- "❌ Unknown sender" --> G(["🚫 403 Forbidden\nSender not registered\nContact admin to add them"]):::fail
G --> UE(["📧 Alert email sent\nto admin — throttled\n1 email per sender per hour"]):::notify
F -- "✅ Found" --> H{{"🟢 Is the sender\nactive?\nStatus = true"}}:::decision
H -- "❌ Disabled" --> I(["🚫 403 Forbidden\nThis sender has\nbeen turned off"]):::fail
H -- "✅ Active" --> J{{"🔀 Is routing\nenabled?\ntblMessageFilter"}}:::decision
J -- "⚠️ Routing OFF" --> K(["💾 Save file to disk\n✅ Return OK to CargoWise\n⏭️ No forwarding — disk only"]):::diskonly
J -- "✅ Routing ON" --> L(["💾 Save file to disk\n✅ Return OK to CargoWise"]):::success
L --> M(["⚙️ Background task starts\nA fresh database connection\nis created just for this"]):::bg
M --> N{{"📋 Any active\ndelivery routes?\ntblRoutes status = true"}}:::decision
N -- "❌ No routes\nconfigured" --> O(["📝 Log: no routes found\nFile stays on disk"]):::warn
N -- "✅ Routes found" --> P(["🚀 Send to ALL routes\nat the same time"]):::bg
P --> RETRY(["🔄 Each route: up to 3 attempts\nBack-off: 5 s then 10 s between tries\nConfig errors skip retries immediately"]):::bg
RETRY --> Q(["📁 FTP / SFTP\nUpload to\nremote server"]):::transport
RETRY --> R(["🌐 HTTP / HTTPS\nPOST XML to\nAPI endpoint"]):::transport
RETRY --> S(["🔑 OUTBOUNDNEXT\nGet OAuth2 token first\nthen POST XML"]):::transport
RETRY --> T(["🧼 SOAP / ENVELOP\nSOAP POST with\nSOAPAction header"]):::transport
RETRY --> U(["🗂️ LOCAL\nCopy file to\nlocal folder"]):::transport
Q & R & S & T & U --> V(["📊 Record delivery count\ntblMessageCount +1\nper successful route"]):::bg
V --> W(["📝 Log results\nSuccess ✅ or Failure ❌\nper route"]):::bg
W --> NA{{"❌ Any routes still\nfailed after 3 attempts?"}}:::decision
NA -- "✅ All OK" --> NB(["✅ Done"]):::success
NA -- "❌ Has failures" --> RF(["🗄️ Record each failed route\nto tblFailedMessage\nconfig ID · filename · error · datetime"]):::fail
RF --> ALLQ{{"📦 Did ALL routes fail?\n(nothing delivered)"}}:::decision
ALLQ -- "✅ Some routes\ndelivered OK" --> NC
ALLQ -- "❌ Total failure" --> FMOVE(["📁 Copy file to FailedFolder\nfor review & reprocessing\nfailed_path saved to DB"]):::fail
FMOVE --> NC
NC{{"📋 Notification tracker\nexists in tblNotificationTracker?"}}:::decision
NC -- "First time\n(no row yet)" --> ND(["📧 Send plain-English\nalert email now\n+ Create tracker row"]):::notify
NC -- "Row found" --> NE{{"⏰ Throttle window\nelapsed?\nHourly / Daily / Minutes"}}:::decision
NE -- "❌ Still within\nwindow" --> NF(["🔇 Suppress email\nAvoid flooding inbox"]):::warn
NE -- "✅ Window elapsed" --> NG(["📧 Send plain-English\nalert email\n+ Update last-sent time"]):::notify
classDef start fill:#1e293b,stroke:#6366f1,stroke-width:3px,color:#e2e8f0,font-weight:bold
classDef decision fill:#1e293b,stroke:#6366f1,stroke-width:2px,color:#c4b5fd
classDef fail fill:#450a0a,stroke:#f87171,stroke-width:2px,color:#fca5a5
classDef success fill:#052e16,stroke:#34d399,stroke-width:2px,color:#86efac
classDef diskonly fill:#451a03,stroke:#fbbf24,stroke-width:2px,color:#fde68a
classDef bg fill:#1c1917,stroke:#fbbf24,stroke-width:2px,color:#fde68a,stroke-dasharray:5 4
classDef transport fill:#0c1a35,stroke:#38bdf8,stroke-width:2px,color:#7dd3fc
classDef warn fill:#1c1917,stroke:#94a3b8,stroke-width:1px,color:#94a3b8
classDef notify fill:#1a0a2e,stroke:#a78bfa,stroke-width:2px,color:#ddd6fe
📬 Delivery Route Types
FTP / FTPS
Uploads the file to a remote FTP server. Always uses encrypted FTPS (TLS) — plain FTP is never sent. Username + password login.
Port 21
SFTP
Uploads the file over a secure SSH connection. Different from FTPS — uses SSH protocol, not TLS. Username + password login.
Port 22
HTTP / HTTPS
Sends the XML file as a POST request to a web API endpoint. Supports Basic Auth (username + password) or a static bearer token.
REST API
OUTBOUNDNEXT
Like HTTP/HTTPS but uses OAuth 2.0 — first contacts a login server to get a temporary access token, then sends the file. Token is reused until it expires (auto-refreshed 60 seconds early).
OAuth 2.0
SOAP
Sends the XML as a SOAP 1.1 message to a web service. Adds a SOAPAction header that tells the server which operation to run. Supports Basic Auth, bearer token, or SOAPAction-only (no auth).
text/xml
ENVELOP
Same as SOAP but uses the newer SOAP 1.2 content type. For systems that require application/soap+xml instead of text/xml.
application/soap+xml
LOCAL
Copies the file to a folder on the same server (or a mapped network drive). No login needed — just a destination folder path.
File Copy
📁 Failed Folder
When every route fails after 3 attempts, the original XML file is copied here for review and manual reprocessing. The path is saved in tblFailedMessage.failed_path.
All routes failed
If only some routes fail, the file is not copied — it was already delivered by the successful routes.
🔄 Retry Policy
Every route is tried up to 3 times before being marked as failed. A short wait is added between attempts to give the destination time to recover.
Attempt 1
Wait 5 s → Attempt 2
Wait 10 s → Attempt 3
Configuration errors (e.g. unknown route type) skip retries immediately — they won't fix themselves.
🔐 SOAP / ENVELOP — Auth Selection Logic
| Password set? | Token set? | SOAPAction set? | What gets sent |
| ✅ Yes | ❌ No | — | Basic Auth (username:password base64) |
| ✅ Yes | ✅ Yes | — | Bearer Token (Authorization: Bearer) |
| ❌ No | — | ✅ Yes | SOAPAction header only (no auth) |
| ❌ No | ✅ Yes | ❌ No | Bearer Token |
| ❌ No | ❌ No | ❌ No | No auth (unauthenticated) |
📧 Error Notification — How It Works
EaglesNest sends a plain-English alert email in two situations:
- Delivery failure — when a file cannot be delivered to any route after 3 attempts.
- Unknown sender — when the API receives a file from a sender that has no matching row in
tblMessageConfig.
Throttle tracked in
tblNotificationTracker. SMTP settings in
appsettings.json → Notification.
| occurrence_type | occurrence_no | Meaning |
| Hourly (default) | 1 | At most one email per hour |
| Hourly | 4 | At most one email every 4 hours |
| Daily | 1 | At most one email per day |
| Minutes | 30 | At most one email every 30 minutes |
| First failure ever | Always sent immediately — tracker row is created on the spot |
Endpoint: POST /api/cargowisedata/send
The caller authenticates with Basic Auth. The authenticated username is used as the partitionKey.
EaglesNest derives the SenderID from the first 6 characters of the username (username[..6]),
fetches a Bearer token from eAdapterNext.Token using the full username as the partitionKey,
then forwards the XML to https://{SenderID}services.wisegrid.net/eAdaptorNext.
If no Azure Table record exists for the username, an alert email is sent to the admin.
OAuth2 / Token acquisition
flowchart TD
A([" 📤 Caller POSTs XML\n POST /api/cargowisedata/send "]):::start
A --> B{{"🔐 Basic Auth\nAuthenticated?"}}:::decision
B -- "❌ Invalid / missing" --> C(["🚫 401 Unauthorised"]):::fail
B -- "✅ Valid" --> BA(["👤 username = User.Identity.Name\n(Basic Auth credential)"]):::step
BA --> BB{{"❌ username\n< 6 chars?"}}:::decision
BB -- "Too short" --> BC(["🚫 400 Bad Request\nUsername must be ≥ 6 chars\n(first 6 used as SenderID)"]):::fail
BB -- "✅ OK" --> D(["📖 Read raw XML body\nfrom HTTP request stream"]):::step
D --> E{{"❌ Body read\nfailed?"}}:::decision
E -- "Exception" --> F(["🚫 500 Internal Error\nFailed to read request body"]):::fail
E -- "✅ OK" --> EE{{"❌ Body\nempty?"}}:::decision
EE -- "Empty" --> EF(["🚫 400 Bad Request\nRequest body is empty"]):::fail
EE -- "✅ Has content" --> G(["🔑 Derive SenderID = username[..6]\nBuild TargetUrl:\nhttps://{SenderID}services.wisegrid.net/eAdaptorNext"]):::step
G --> K(["🎫 POST {TokenApiBaseUrl}/api/token\nbody: { partitionKey: '{username}' }\nContent-Type: application/json"]):::ext
K --> L{{"📡 Token API\nreachable?"}}:::decision
L -- "❌ Network error\nor timeout" --> M(["🚫 502 Bad Gateway\nCannot reach Token API\neAdapterNext.Token is down"]):::fail
L -- "✅ Responded" --> N{{"✅ Token API\nsuccess=true?"}}:::decision
N -- "❌ Config not found\n(no Azure Table row\nfor this username)" --> NA(["📧 Admin alert email sent\nThrottled: 1/hour per username"]):::notify
NA --> O(["🚫 502 Bad Gateway\nNo Token config for this user.\nAdmin has been notified."]):::fail
N -- "❌ Other error\n(Azure AD / token issue)" --> O2(["🚫 502 Bad Gateway\nToken API returned error"]):::fail
N -- "✅ success=true" --> P{{"🗄️ Token source?"}}:::decision
P -- "🟡 CACHED\n(still valid in\nAzure Table Storage)" --> Q(["⚡ Return cached token\nTokenSource: Cached\nno Azure AD call needed"]):::cache
P -- "🆕 NEW\n(expired or\nfirst time)" --> R(["🔑 Token API called Azure AD\nOAuth2 client_credentials\nToken cached in Azure Table"]):::auth
Q & R --> S(["📤 POST XML to CargoWise\nhttps://{SenderID}services.wisegrid.net/eAdaptorNext\nAuthorization: Bearer {token}\nContent-Type: application/xml"]):::ext
S --> T{{"📡 CargoWise\nendpoint reachable?"}}:::decision
T -- "❌ Network error\nor timeout" --> TNA(["📧 Admin alert email\nNotifySendFailureAsync\nThrottled: 1/hour per username"]):::notify
TNA --> U(["🚫 502 Bad Gateway\nNetwork error to CargoWise\nEndpoint may be down"]):::fail
T -- "✅ Response received" --> V{{"📊 CargoWise\nHTTP Status"}}:::decision
V -- "✅ 2xx" --> W(["✅ 200 OK → success=true\nCargowiseSendResponse:\ntraceParent · sentAt · cwStatusCode\ncwResponse · tokenSource · targetUrl"]):::success
V -- "⚠️ Non-2xx\n4xx / 5xx" --> VNA(["📧 Admin alert email\nNotifySendFailureAsync\nThrottled: 1/hour per username"]):::notify
VNA --> X(["⚠️ 200 OK → success=false\nCargoWise rejected the XML\ncwStatusCode + cwResponse body returned"]):::warn
classDef start fill:#1e293b,stroke:#6366f1,stroke-width:3px,color:#e2e8f0,font-weight:bold
classDef decision fill:#1e293b,stroke:#6366f1,stroke-width:2px,color:#c4b5fd
classDef fail fill:#450a0a,stroke:#f87171,stroke-width:2px,color:#fca5a5
classDef success fill:#052e16,stroke:#34d399,stroke-width:2px,color:#86efac
classDef step fill:#0f1a2e,stroke:#38bdf8,stroke-width:2px,color:#7dd3fc
classDef ext fill:#1c1005,stroke:#fb923c,stroke-width:2px,color:#fdba74
classDef cache fill:#1c1917,stroke:#fbbf24,stroke-width:2px,color:#fde68a
classDef auth fill:#1a0a2e,stroke:#a78bfa,stroke-width:2px,color:#ddd6fe
classDef warn fill:#451a03,stroke:#fbbf24,stroke-width:1px,color:#fde68a
classDef notify fill:#0c1a0c,stroke:#4ade80,stroke-width:2px,color:#86efac
🎫 Token API Lookup — How It Works
eAdapterNext.Token (POST /api/token) manages OAuth2 tokens for outbound calls.
The authenticated username is used as the partitionKey — it must match a row
in Azure Table Storage containing the OAuth2 credentials (TenantId, ClientId, certificate PEM, private key PEM, Scope).
If no row is found, a 502 is returned and an admin alert email is sent (throttled — 1/hour per username).
Tokens are cached in the same Azure Table row (AccessToken, ExpiresAtUtc).
A 5-minute buffer is applied — the token is refreshed 5 minutes before it actually expires to avoid using a stale token.
| Scenario | Token API Action | TokenSource in response |
| First request / token expired | OAuth2 client_credentials → Azure AD → store in table | New |
| Within 5-min buffer before expiry | OAuth2 refresh → Azure AD → update table | New |
| Token still valid (> 5 min remaining) | Return stored AccessToken directly | Cached |
| Username not in Azure Table | Return success=false, message: "Config not found" — admin alert email sent | ❌ 502 from eAdapterNext.Auth |
| Azure AD refuses credentials | Return success=false with Azure error detail | ❌ 502 from eAdapterNext.Auth |
⚠️ Send Endpoint — Error Reference
| HTTP Status | success field | Cause | Fix |
| 400 | false | Username shorter than 6 characters, or empty request body | Ensure Basic Auth username is ≥ 6 chars and XML body is present |
| 401 | false | Missing or invalid Basic Auth credentials | Supply a valid Authorization: Basic ... header |
| 500 | false | Failed to read the HTTP request body | Ensure the XML body is sent correctly and the connection is stable |
| 502 | false | No Azure Table config for username (admin notified), Token API unreachable, or CargoWise network failure | Add the username row in eAdapterNext.Token Azure Table; check eAdapterSettings:TokenApiBaseUrl |
| 200 + success=false | false | CargoWise returned non-2xx (e.g. 400 or 500) — admin alert sent | Check cwResponse body for CargoWise error detail |
| 200 + success=true | true | XML delivered successfully | — |