SteveSteve

Submission Review

Operate Steve as a headless review queue through the external API.

The submission endpoints let an integrator run manual review outside the Steve UI.

Use them to:

  • list submissions for the authenticated company
  • fetch full detail, including file download URLs and event history
  • inspect and resolve fraud matches
  • approve, reject, or cancel submissions

Public status model

Steve exposes the following submission statuses through the external API:

StatusMeaning
createdSubmission record exists but processing has not started
processingSteve is running extraction and fraud checks
capture_quality_blockedProcessing stopped because the uploaded capture did not meet quality rules
reviewHuman review is required
approvedApproved and waiting for or entering downstream sync
syncedApproved, and the data was actually delivered downstream (derived)
rejectedRejected by a reviewer or integration
cancelledCancelled before approval or sync
failedProcessing failed for a non-review reason

For workflow sessions, GET /api/v1/jobs/{sessionId} tells you when processing is done. GET /api/v1/submissions/{submissionId} is the canonical read model for manual review.

List submissions

curl "$BASE_URL/api/v1/submissions?status=review&workflow=receipt-ocr&limit=20" \
  -H "Authorization: Bearer $STEVE_API_KEY"

Supported query parameters:

ParameterMeaning
workflowFilter to one workflow slug
statusFilter by public submission status
fraudStatusFilter by clean, flagged, or blocked
cursorContinue from a previous page
limitPage size, clamped to 1..100

Example response:

{
  "data": [
    {
      "id": "jx7sub456submissions",
      "submissionStatus": "review",
      "fraudStatus": "flagged",
      "fraudScore": 72.5,
      "workflow": {
        "slug": "receipt-ocr",
        "name": "Receipt OCR",
        "version": 3
      },
      "extractedData": {
        "merchantName": "Biedronka",
        "totalAmount": 123.45
      },
      "confidence": 0.96,
      "createdAt": "2026-03-25T10:00:00.000Z",
      "clientSubmissionId": "order-1711353600000"
    }
  ],
  "pagination": {
    "hasMore": false,
    "nextCursor": null
  }
}

Notes:

  • timestamps are ISO 8601 strings
  • clientSubmissionId is the caller-supplied idempotency key from session creation
  • extractedData may be partial or complete depending on processing state

Get submission detail

curl "$BASE_URL/api/v1/submissions/jx7sub456submissions" \
  -H "Authorization: Bearer $STEVE_API_KEY"

Example response:

{
  "id": "jx7sub456submissions",
  "submissionStatus": "review",
  "fraudStatus": "flagged",
  "fraudScore": 72.5,
  "extractedData": {
    "merchantName": "Biedronka",
    "totalAmount": 123.45,
    "currency": "PLN"
  },
  "confidence": 0.96,
  "fieldConfidences": {
    "merchantName": 0.99,
    "totalAmount": 0.95
  },
  "certainty": 0.94,
  "aiSummary": "Receipt from Biedronka with one flagged fraud match.",
  "failedReason": null,
  "rejectReason": null,
  "cancelReason": null,
  "workflow": {
    "id": "jx7wf001workflows",
    "slug": "receipt-ocr",
    "name": "Receipt OCR",
    "version": 3
  },
  "files": [
    {
      "id": "jx7file001submissionFiles",
      "fileIndex": 0,
      "label": "receipt",
      "mimeType": "image/jpeg",
      "fileSize": 245120,
      "downloadUrl": "https://r2.example.com/files/receipt.jpg?X-Amz-Signature=abc123"
    }
  ],
  "fraudMatches": [
    {
      "id": "jx7fm001submissionFraudMatches",
      "checkType": "visual",
      "score": 72.5,
      "similarity": 0.88,
      "status": "flag",
      "resolution": null,
      "resolvedBy": null,
      "resolvedAt": null
    }
  ],
  "events": [
    {
      "type": "ready_for_review",
      "occurredAt": "2026-03-25T10:01:40.000Z",
      "details": {
        "compliance": 0.96,
        "fraudStatus": "flagged"
      }
    }
  ],
  "clientSubmissionId": "order-1711353600000",
  "metadata": {
    "orderId": "12345"
  },
  "createdAt": "2026-03-25T10:00:00.000Z"
}

Important details:

  • file download URLs are pre-signed and valid for one hour
  • events are newest-first and capped at 100 entries; all verification_* events are excluded. Public outcome events such as rejected remain visible, with internal runId and previousRunId fields omitted from their details.
  • rejectReason and cancelReason are exposed on rejected or cancelled submissions

Fraud match workflow

If a submission is flagged or blocked, inspect the linked fraud matches:

curl "$BASE_URL/api/v1/submissions/jx7sub456submissions/fraud-matches" \
  -H "Authorization: Bearer $STEVE_API_KEY"

Response shape:

{
  "data": [
    {
      "id": "jx7fm001submissionFraudMatches",
      "checkType": "visual",
      "score": 72.5,
      "similarity": 0.88,
      "status": "flag",
      "resolution": null,
      "resolvedAt": null,
      "targetSubmission": {
        "id": "jx7sub200submissions",
        "createdAt": "2026-03-25T09:43:20.000Z"
      }
    }
  ],
  "pagination": {
    "hasMore": false,
    "nextCursor": null
  }
}

Resolve one match:

curl "$BASE_URL/api/v1/submissions/jx7sub456submissions/fraud-matches/jx7fm001submissionFraudMatches/resolve" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resolution":"false_positive"}'

Allowed resolutions:

  • false_positive
  • confirmed_fraud

Approvals are blocked while unresolved fraud matches remain.

Reading checkType

checkType is either visual (perceptual-hash image duplicate) or data (duplicate extracted content). Data duplicates are produced two ways, and both report checkType: "data":

  • an exact match on the workflow's configured identity fields, which always arrives as score: 100, similarity: 1.0, status: "block"
  • a similarity match scored against the workflow's thresholds, which can be flag or block

Treat similarity: 1.0 with status: "block" as a certain duplicate rather than a high score.

Approve, reject, and cancel

Approve

Approves a submission that is currently in review.

Approval returns 409 while product matching is not finished for a workflow that matches products: a line still waits for a reviewer, matching is still running, or it failed. When the organization's product catalog is switched off, approval does not refuse for this reason; it skips the unfinished matching and approves.

Approval returns 409 when verification checks did not pass; a reviewer must approve with a reason. This includes failed, inconclusive, unavailable, stale, or unrun checks on an enabled verification stage. Checks skipped because fraud blocked processing still need to run after the fraud match is resolved. A reviewer can use Run checks in Steve and approve after a current pass, or approve with an audited reason. The external approve endpoint does not accept an override reason; send these cases to a Steve reviewer.

curl "$BASE_URL/api/v1/submissions/jx7sub456submissions/approve" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY"

Response:

{
  "submissionId": "jx7sub456submissions",
  "workflow": {
    "slug": "receipt-ocr",
    "version": 3
  },
  "submissionStatus": "approved"
}

approved is the terminal status: a submission that reaches it is finished, and for an account with no downstream delivery configured that is the last word you will see.

Where delivery IS configured, approval triggers Steve's downstream sync path and later reads may show synced. It is owned by the delivery ledger, not by review actions, and it requires an actual delivery to have gone out — every target the submission owed, delivered. Anything short of that stays approved: a workflow that declares no delivery targets, a delivery still outstanding, or one skipped for a fixable reason (no customer email on file, or the integration not configured yet) — the last of which stays approved until someone fixes the cause and the delivery is retried.

Reject

Reject requires a human-readable reason.

curl "$BASE_URL/api/v1/submissions/jx7sub456submissions/reject" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Document is illegible and cannot be verified."}'

Cancel

Cancel is available until the submission reaches approved or synced. The reason is optional.

curl "$BASE_URL/api/v1/submissions/jx7sub456/cancel" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Duplicate submission from the same customer."}'

Operational guidance

  • Use GET /api/v1/jobs/{sessionId} to know when a submission is ready for review.
  • Use GET /api/v1/submissions/{submissionId} as the canonical source of truth after any action.
  • If you need file bytes, fetch them immediately after retrieving submission detail because download URLs expire.
  • POST review actions share the write rate-limit bucket. See Error Handling.
  • Webhooks are useful for notification, but the safest way to render a review UI is still to refetch the submission after each event.

Process a blocked capture anyway

GET /api/v1/submissions/{submissionId} includes files[].captureQuality: null when unmeasured, otherwise verdict (ok, warn, or blocked), reasons, processedAnyway, and checkedAt. Each reason includes code, metric, the rounded measured value, threshold, and blocking. A processed-anyway file keeps the measured verdict; processedAnyway: true records the bypass.

POST /api/v1/submissions/{submissionId}/process-anyway
Authorization: Bearer aok_<key>

Send no body. This authorizes one run with the same files and workflow version. It returns 202 with the existing submission action response and current submissionStatus. Repeating the request while processing, or after that override finished without another capture-quality block, returns 202 without starting another run. A completed override blocked again can start another run.

The endpoint requires current workflow access. It returns 403 if the module is off, the workflow is inactive, or its company grant was revoked; 404 for a missing or other-company submission; 409 if there is no eligible block or recorded measurement, or delivery history exists.

A failed API job returns to submitted and its old result is cleared. Poll the same session ID or receive a new webhook with a generation suffix in its event ID. This authorization does not cast calibration votes, approve the result, or bypass other processing checks.

Only this endpoint reopens the job. When someone clicks Process anyway in the Steve admin panel, the submission is processed again, but the API job stays failed and no new session webhook is sent. Read the outcome with GET /api/v1/submissions/{submissionId}. If your integration needs the webhook, trigger the rerun through this endpoint instead of the panel.

Replace a photo with a new capture

Files cannot be added to a submission after its session was submitted, so a retake is a new session and a new submission. Duplicate checks compare the new submission with earlier ones, by image and by the extracted identity fields (for example receipt number and date). Cancelled, rejected, and failed submissions are not compared.

To replace a capture without the retake being flagged as a duplicate of the original:

  1. Cancel the original with POST /api/v1/submissions/{submissionId}/cancel, for example with the reason "Replaced by a new photo".
  2. Create a new session with the new photo.

A submission that is already approved or synced cannot be cancelled. A retake of it is correctly flagged. A duplicate match never rejects a submission on its own: the submission goes to review with fraudStatus set. If the match is a false positive, resolve it as false_positive and approve the submission.

A multi-page PDF remains one submission. Each extracted field can carry an absolute page citation; item fields can carry one page per item. OCR-text and page-image reads are compared independently. Disagreement, missing citations, unavailable or truncated reads and failed pages require human review, even at full model confidence or after passing verification. Machine evidence remains immutable when a reviewer edits a field. The original PDF and page images/markdown expire under the workflow's media retention policy; extracted values and evidence remain. Both Classic and Deck show a page viewer for multi-page files, including when extraction failed before evidence was saved.

In Deck, the viewer opens on the page of the next field to check and turns to the next field's page as you settle each one, until you turn a page yourself. Under the page, a strip shows every page; a page with fields still to check carries their count, and a page that could not be turned into an image says so. Use the arrows, the arrow keys or the strip to move, zoom and rotate to read small print, and full screen to page through the whole document. Under the page, the viewer lists the fields read from it; choose one to go to its row. The PDF button opens the original document. In a duplicate comparison, a multi-page PDF is shown by its first page.

In Classic, use Previous, Next or the page list to move through the document. A failed page is marked in the list. Download original PDF opens the original document.

Choose a field's page number to open its source page; line items have a citation for each item. A missing or zero page number is not a citation. In Deck, each clean value shows its pages beside it, and a page number is highlighted while that page is on screen. Flagged fields appear in Classic's Failed group and Deck's To check rows, including fields the workflow does not allow you to edit. Each shows a plain-English reason and every value the reads found, with its pages; a value both reads gave is shown once. If OCR itself found conflicting values, each one is shown with its own page.

A sentence above the fields explains any incomplete reads or failed pages. In Deck, the value stored now is marked; Use this value on another one saves it in one click, and Confirm keeps the stored value. You can still type a different value. A non-editable flagged field shows the values without Use this value; Confirm records that you checked it. These actions record the review without replacing the original machine evidence.

In Deck, a line-items field to check shows its lines as a table under the row, so you see what Confirm keeps. Edit line items replaces the table with the editor, and Cancel brings the table back. The lines stay shown after you confirm or save them, and a line-items field you cannot edit shows them the same way. In the editor, text cells wrap and grow with long text: Enter saves the lines, and Shift+Enter starts a new line in a cell.

When the workflow version a submission was checked against has reference images, Deck shows them under the evidence, each with its label. They are kept apart from the submission's photos: they are not counted with them, and they open in their own viewer, where you can zoom in to compare. They come from that version, not from later edits to the workflow. A reference image that cannot be loaded says so.

When the workflow chooses references by product key, Deck shows only the references that were sent with this submission, not the whole library. A sentence above them names the product key, and each label says whether it matched by exact key, key prefix or as a shared reference. When no reference matched and the AI analysis was skipped, Deck shows the reason under the summary and above the references, for example that no product key was sent, and no references. The summary asks you to fill in the fields from the photos, and each field reads as not analysed rather than as read or missing from the photo. When only shared references were used, the same line under the summary says so. See Reference images.

On this page