SHIP Claim Submission Workflow Contract
Status: Active
Version: v1
Last updated: 2026-04-08
This document is the normative contract for the SHIP claim submission workflow. Claim submissions have a distinct state machine from findings and are documented separately from docs/api/ship-finding-contract-v1.md — a submission aggregates one or more findings into a packet that can be sent to a carrier, and its state transitions independently of the underlying findings’ workflow states.
Routes in scope
All routes below are callable with an integration-key Bearer JWT obtained via POST /api/auth/token/integration.
| Method | Route | Purpose |
|---|---|---|
| POST | /api/ship/claims/submissions | Create a new submission in DRAFT |
| GET | /api/ship/claims/submissions | List submissions with optional filters |
| GET | /api/ship/claims/submissions/:submissionId | Get submission detail |
| POST | /api/ship/claims/submissions/:submissionId/generate-packet | Generate the claim packet; DRAFT → READY |
| POST | /api/ship/claims/submissions/:submissionId/submit | Submit to carrier; READY → SUBMITTED |
| POST | /api/ship/claims/submissions/:submissionId/acknowledge | Record carrier receipt; SUBMITTED → ACKNOWLEDGED |
| POST | /api/ship/claims/submissions/:submissionId/confirm-credit | Record carrier credit confirmation; callable from SUBMITTED, ACKNOWLEDGED, or FAILED |
State machine
DRAFT ──generate-packet──▶ READY ──submit──▶ SUBMITTED ──acknowledge──▶ ACKNOWLEDGED
│ │ │ │ │
│ │ │ │ ▼
│ │ ├──confirm-credit──▶ CREDIT_CONFIRMED
│ │ │ │ │
│ │ │ │ ▼
└──────────────────────── (failure at any step can transition to FAILED) CLOSED
│ ▲
│ (operator close)
│
confirm-credit (retry)confirm-credit is callable from three states: SUBMITTED, ACKNOWLEDGED, and FAILED. The outcome depends on per-finding credit confirmation results (see Confirm-credit outcomes below).
State definitions:
DRAFT— submission created but packet not yet generated. Findings can still be added or removed by re-creating the submission (creation is the add path; modify by delete-and-recreate is the simplest pattern).READY— claim packet has been generated and is ready to submit.packetUrlis available.SUBMITTED— packet has been sent to the carrier.submittedAtis set.confirm-creditis callable from this state.ACKNOWLEDGED— the carrier confirmed receipt.acknowledgedAtis set.confirm-creditis callable from this state.CREDIT_CONFIRMED— carrier credited the claim.creditConfirmedAtis set. Credit confirmation on a submission is separate from per-findingCREDITED— the submission-level credit groups the contested amount at the carrier level while the per-finding credit records the billing-critical individual credit (seeship-finding-contract-v1.md).CLOSED— terminal administrative state; submission will not be reopened.FAILED— failure at packet generation, submission, or credit confirmation.confirm-creditis callable from this state as a retry path. Inspect the submission detail for the reason.
The canonical state set is defined by the ShipClaimSubmissionStatus enum in apps/api/prisma/schema.prisma and enforced by apps/api/src/lib/ship/claim-submissions.ts.
Relationship to the finding workflow
- A submission is created from a non-empty set of
findingIds. Each finding must pass the claim eligibility check (seeship-finding-contract-v1.md, “Claim eligibility matrix”):- Only findings with
claimEligibility: ELIGIBLEcan be bundled. Ineligible findings are rejected with400 INVALID_REQUESTand ainvalidFindingStatesdetail array naming each finding + itsclaimBlockerReason. - Eligible workflow states:
DISPUTED,SUBMITTED,CARRIER_REVIEW. - Ineligible states:
OPEN(must dispute first),DISMISSED/CREDITED/REJECTED(workflow resolved), carrier-blocked findings, and findings already linked to an active (non-terminal) claim submission. - A finding already linked to an active submission (status not in
CREDIT_CONFIRMED/CLOSED/FAILED) returns409 ACTION_NOT_ALLOWEDwith aduplicateLinksdetail array naming the conflicting submission.
- Only findings with
- Creating a submission does not automatically transition the underlying findings. The finding workflow and the submission workflow are orthogonal — a caller that wants an end-to-end “dispute and submit” flow must call the finding dispute/submit endpoints separately.
- Per-finding
CREDITEDtransitions are still the billing-critical writes. A submission-levelCREDIT_CONFIRMEDdoes not replace the per-finding credit endpoint — it records the outer carrier response, while the per-finding credit endpoint is the billing-input-of-record. - Each finding in a submission response includes the same explanation fields (
actionability,headline,claimEligibility,claimBlockerReason, etc.) as described inship-finding-contract-v1.md.
Happy path
POST /api/ship/claims/submissionswith{ carrier, findingIds[], notes? }. Response is the submission inDRAFT, wrapped in the{ submission }envelope described below.POST /api/ship/claims/submissions/:submissionId/generate-packet. Response envelope includessubmission.packetUrlandsubmission.status === 'READY'.POST /api/ship/claims/submissions/:submissionId/submitwith{ "externalReference": "..." }.externalReferenceis required and must be a non-empty string (a carrier-facing reference, e.g. the carrier’s claim ticket ID). Omitting it or sending an empty string returns400 INVALID_REQUEST. Status becomesSUBMITTED.- When the carrier confirms receipt:
POST /api/ship/claims/submissions/:submissionId/acknowledgewith{ "externalReference": "..." }.externalReferenceis required on acknowledge too (a non-empty string; omission returns400 INVALID_REQUEST). Status becomesACKNOWLEDGED. - When the carrier credits the claim:
POST /api/ship/claims/submissions/:submissionId/confirm-creditwith{ confirmation: { source, referenceId, confirmedAt, notes?, artifactUrl? }, amountsByFinding?, reason? }. The resultingsubmission.statusdepends on per-finding outcomes (see Confirm-credit outcomes).
After CREDIT_CONFIRMED, record the billing-critical per-finding credits via POST /api/ship/findings/:findingId/credit for each finding in the submission (see ship-finding-contract-v1.md).
Confirm-credit outcomes
confirm-credit is callable from SUBMITTED, ACKNOWLEDGED, or FAILED. The handler attempts to confirm credit for each finding linked to the submission. The resulting submission status depends on the per-finding results:
| Per-finding result | Submission outcome | Notes |
|---|---|---|
| All findings credited successfully | CREDIT_CONFIRMED | creditConfirmedAt is set. Terminal success. |
| All findings failed | FAILED | failedAt and failureReason are set. Caller may retry by calling confirm-credit again. |
| Partial (some succeed, some fail) | Stays at current status | If called from SUBMITTED, stays SUBMITTED. If called from ACKNOWLEDGED or FAILED, stays ACKNOWLEDGED. |
Retry path: Because FAILED is a valid entry state for confirm-credit, callers can retry after a previous failure without resetting the submission. This is the intended recovery mechanism for transient credit-confirmation failures.
Response envelope
Every claim submission mutation (POST /api/ship/claims/submissions, .../generate-packet, .../submit, .../acknowledge, .../confirm-credit) and the single-submission detail GET return the submission nested under a submission key — never as a bare object:
{
"submission": {
"id": "7c1e3e2c-22d4-4b79-9d1a-4a8b3f5d6c77",
"status": "SUBMITTED",
"carrier": "demo-carrier",
"findingIds": ["3b1b5c4c-...-..."],
"packetUrl": "https://...",
"submittedAt": "2026-04-08T17:03:22.511Z",
"acknowledgedAt": null,
"creditConfirmedAt": null,
"externalReference": "DEMO-REF-0001",
"createdAt": "2026-04-08T17:02:10.100Z",
"updatedAt": "2026-04-08T17:03:22.511Z"
}
}The list endpoint GET /api/ship/claims/submissions uses a distinct envelope keyed on submissions (plural): { submissions: [...], total, limit, offset, hasMore }. It does not use an items key.
Example submit call:
curl -X POST \
"https://api.rgl8r.com/api/ship/claims/submissions/$SUBMISSION_ID/submit" \
-H "Authorization: Bearer $BEARER_JWT" \
-H "Content-Type: application/json" \
-d '{"externalReference":"DEMO-REF-0001"}'Example acknowledge call:
curl -X POST \
"https://api.rgl8r.com/api/ship/claims/submissions/$SUBMISSION_ID/acknowledge" \
-H "Authorization: Bearer $BEARER_JWT" \
-H "Content-Type: application/json" \
-d '{"externalReference":"DEMO-REF-0001-ACK"}'Error envelopes
All errors follow the canonical public envelope. Endpoint-specific errors:
| Code | HTTP | When |
|---|---|---|
INVALID_REQUEST | 400 | Missing carrier/findingIds on create, missing or empty externalReference on submit/acknowledge, invalid confirmation shape on confirm-credit |
INVALID_TOKEN | 401 | JWT expired or revoked |
NOT_FOUND | 404 | Submission does not belong to the tenant, or referenced findings not found |
ACTION_NOT_ALLOWED | 409 | Transition not allowed from current state, carrier policy blocks the action, or findings are in an invalid combination of states |
RATE_LIMITED | 429 | Write bucket |
INTERNAL_ERROR | 500 | Unexpected failure |
Common 409 subcodes surfaced in details:
carrier_blocked— the tenant’s carrier policy (e.g. manual-import-only) blocks the action.invalid_transition— current state is not a valid source for the requested transition.invalid_finding_state— one or more findings are not eligible to bundle into a claim.missing_findings—findingIdslist is empty after filtering to the tenant.
Retry guidance
- Create submission (
POST /api/ship/claims/submissions) is not idempotent — retries may create duplicate submissions. Prefer to poll byfindingIdsfirst if the caller cannot remember whether the previous attempt succeeded. - Transition calls (
generate-packet,submit,acknowledge) are safe to retry when the previous attempt left the submission in an intermediate state; a retry into the same state returnsACTION_NOT_ALLOWEDwhich the caller should treat as success. confirm-creditis explicitly callable fromFAILEDas a retry path. If the previous attempt transitioned toFAILED, callingconfirm-creditagain will re-attempt per-finding credit confirmation. If it already reachedCREDIT_CONFIRMED, a retry returnsACTION_NOT_ALLOWED.- Retry on
429usingdetails.retryAfterSeconds; do not retryACTION_NOT_ALLOWEDwithout re-reading state.
Cross-references
- OpenAPI artifact:
docs/api/openapi/rgl8r-public-api-v1.2.0.yaml - Public API contract pack:
docs/api/public-api-contract-v1.md - Finding workflow contract (upstream of submissions):
docs/api/ship-finding-contract-v1.md - Quickstart and agent kit:
docs/developers/quickstart.md,docs/api/agent-integration-kit-v1.md - Sample fixture:
docs/api/examples/fixtures/ship-invoice-sample.csv - Example scripts:
docs/api/examples/rgl8r_ship_quickstart.ts,docs/api/examples/rgl8r_ship_quickstart.py