SteveSteve

Quick Start

End-to-end guide for submitting files into Steve workflows through the external API.

This guide walks the production flow:

  1. discover accessible workflows
  2. create a workflow session
  3. upload files directly to Cloudflare R2
  4. submit the session for processing
  5. track completion by webhook or polling
  6. fetch the submission and, if needed, take a review action

Before you start

You need:

  • a company-scoped API key created by a super_admin
  • at least one workflow assigned to that company in the admin app
  • the Convex site host for your Steve deployment

Steve API keys are long-lived server secrets. Do not embed them in browsers, mobile apps, or desktop binaries.

export BASE_URL="https://your-deployment.convex.site"
export STEVE_API_KEY="aok_your_key_here"

Optional: Use the TypeScript SDK

Steve ships a first-party TypeScript client in packages/steve-sdk.

import { SteveClient } from "@steve/sdk";
 
const client = new SteveClient({
  baseUrl: process.env.BASE_URL!,
  apiKey: process.env.STEVE_API_KEY!,
});
 
const workflows = await client.listWorkflows();

Step 1: List workflows

Use the workflow list endpoint to discover what your company is allowed to integrate with and which workflow version is currently published.

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

Example response:

{
  "data": [
    {
      "id": "jx7wf001workflows",
      "slug": "receipt-ocr",
      "name": "Receipt OCR",
      "description": "Extract receipt data from uploaded images.",
      "icon": "receipt",
      "color": "#0B6E4F",
      "publishedVersion": 3,
      "contractUrl": "/api/v1/workflows/receipt-ocr/versions/3",
      "upload": {
        "minFiles": 1,
        "maxFiles": 5,
        "acceptedFormats": ["image/jpeg", "image/png", "image/webp"],
        "fileLabels": ["receipt"],
        "maxFileSizeMb": 10
      }
    }
  ],
  "pagination": {
    "hasMore": false,
    "nextCursor": null
  }
}

Use the upload block as the source of truth for:

  • how many files to send
  • which MIME types Steve accepts
  • whether the workflow expects labeled file slots
  • the maximum file size

To fetch the currently published summary for one workflow:

GET /api/v1/workflows/{slug}

To fetch the immutable upload and output contract for a specific version:

GET /api/v1/workflows/{slug}/versions/{version}

Example contract response:

{
  "workflow": {
    "id": "jx7wf001workflows",
    "slug": "receipt-ocr",
    "name": "Receipt OCR",
    "description": "Extract receipt data from uploaded images.",
    "icon": "receipt",
    "color": "#0B6E4F"
  },
  "version": 3,
  "upload": {
    "minFiles": 1,
    "maxFiles": 5,
    "acceptedFormats": ["image/jpeg", "image/png", "image/webp"],
    "fileLabels": ["receipt"],
    "maxFileSizeMb": 10
  },
  "outputSchema": [
    {
      "name": "merchantName",
      "label": "Merchant Name",
      "required": true,
      "type": "string"
    },
    {
      "name": "totalAmount",
      "label": "Total Amount",
      "required": true,
      "type": "currency",
      "currencyCode": "PLN"
    }
  ],
  "outputJsonSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "merchantName": { "type": "string" },
      "totalAmount": { "type": "number", "x-currencyCode": "PLN" }
    },
    "required": ["merchantName", "totalAmount"]
  }
}

Use outputJsonSchema when you want machine validation. Use outputSchema when you want a simple typed field list for UI or mapping code.

Step 2: Create a workflow session

Creating a session reserves file slots, creates the linked submission record, and returns pre-signed R2 upload URLs.

curl "$BASE_URL/api/v1/workflows/receipt-ocr/sessions" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fileCount": 1,
    "contentTypes": ["image/jpeg"],
    "clientSubmissionId": "order-1711353600000",
    "webhookUrl": "https://integrator.example.com/webhooks/steve",
    "metadata": {
      "orderId": "12345"
    }
  }'

Important request fields:

FieldRequiredNotes
fileCountYesMust stay within the workflow's minFiles..maxFiles range
contentTypesYesOne MIME type per file slot
clientSubmissionIdYesIdempotency key for the caller
webhookUrlNoPublic HTTPS endpoint for production notifications
metadataNoEchoed back in jobs, submission detail, and webhook deliveries
externalUserNoEnd-user attribution ({ id?, name?, email? }) surfaced as "Submitted by" in the Steve admin UI. Omit to attribute to the API key.

Example response:

{
  "sessionId": "jx7ses789session",
  "submissionId": "jx7sub456submissions",
  "state": "pending",
  "submissionStatus": "created",
  "workflow": {
    "slug": "receipt-ocr",
    "version": 3
  },
  "uploadUrls": [
    {
      "fileIndex": 0,
      "uploadUrl": "https://r2.example.com/upload?X-Amz-Signature=abc123",
      "r2Url": "https://pub-r2.example.com/submission-files/jx7sub456/0-receipt.jpg",
      "contentType": "image/jpeg",
      "label": "receipt"
    }
  ],
  "expiresAt": "2026-03-25T10:30:00.000Z",
  "uploadUrlsExpireAt": "2026-03-25T10:15:00.000Z"
}

Operational notes:

  • upload URLs expire after 15 minutes
  • the session expires after 30 minutes
  • Steve currently exposes no public upload-URL refresh endpoint; if the pre-signs expire, create a new session
  • if you retry create session with the same clientSubmissionId while the first session is still pending, Steve returns the existing session with 200 OK

Step 3: Upload files directly to R2

Upload each file to its uploadUrl using PUT. The Content-Type header must match the MIME type you declared in contentTypes.

curl "$UPLOAD_URL" \
  -X PUT \
  -H "Content-Type: image/jpeg" \
  --data-binary @receipt.jpg

Steve does not proxy the file bytes. The upload goes straight to Cloudflare R2.

Step 4: Submit the session

Once every required file is uploaded, submit the session.

curl "$BASE_URL/api/v1/workflows/receipt-ocr/sessions/jx7ses789session/submit" \
  -X POST \
  -H "Authorization: Bearer $STEVE_API_KEY"

Response:

{
  "sessionId": "jx7ses789session",
  "submissionId": "jx7sub456submissions",
  "state": "submitted",
  "submissionStatus": "processing",
  "workflow": {
    "slug": "receipt-ocr",
    "version": 3
  }
}

Steve verifies the files exist in R2 before it queues the session for processing. If required files are missing, the endpoint returns 422 with field-level errors for the missing file slots.

Step 5: Track completion

Use webhooks for production latency. Use polling as a fallback and for recovery.

Webhook deliveries

If you supplied a webhookUrl, Steve sends a signed POST when the session completes, fails, or when later submission actions change state.

See Webhooks for:

  • delivery headers
  • signature verification
  • retry timing
  • event types

Polling

Poll the job endpoint every 5 to 10 seconds:

curl "$BASE_URL/api/v1/jobs/jx7ses789session" \
  -H "Authorization: Bearer $STEVE_API_KEY"

Example workflow-session response:

{
  "sessionId": "jx7ses789session",
  "submissionId": "jx7sub456submissions",
  "workflow": {
    "slug": "receipt-ocr",
    "version": 3
  },
  "companyId": "jx7abc123company",
  "state": "completed",
  "submissionStatus": "review",
  "result": {
    "extractedData": {
      "merchantName": "Biedronka",
      "totalAmount": 123.45,
      "currency": "PLN"
    },
    "confidence": 0.96,
    "fraudStatus": "flagged"
  },
  "failedReason": null,
  "clientSubmissionId": "order-1711353600000",
  "metadata": {
    "orderId": "12345"
  },
  "processedAt": "2026-03-25T10:05:30.000Z",
  "webhookDeliveryStatus": "delivered"
}

Interpret the fields like this:

  • state is the session lifecycle: pending, submitted, completed, failed, or expired
  • submissionStatus is the current business outcome when known: for example processing, review, approved, rejected, or cancelled
  • result is the workflow-specific processing payload
  • result should be interpreted against the workflow version contract you fetched earlier
  • webhookDeliveryStatus tells you whether Steve has delivered the configured webhook yet

Stop polling when state reaches completed, failed, or expired.

Step 6: Read the submission and take action if needed

When a completed session lands in submissionStatus: "review", fetch the submission and decide what to do next.

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

If the submission is in review:

  • approve it with POST /api/v1/submissions/{submissionId}/approve
  • reject it with POST /api/v1/submissions/{submissionId}/reject
  • cancel it with POST /api/v1/submissions/{submissionId}/cancel
  • resolve any fraud matches before approval

See Submission Review for the full review flow.

Status model

Session status

StatusMeaningTerminal
pendingSession created, waiting for uploads and submitNo
submittedSession accepted for processingNo
completedProcessing finishedYes
failedProcessing failedYes
expiredSession aged out before submitYes

Submission status

StatusMeaning
createdSubmission record exists but processing has not started
processingSession was submitted and Steve is processing it
reviewHuman decision required
approvedApproved and eligible for downstream sync
syncedApproved and synced downstream
rejectedRejected by reviewer
cancelledCancelled by API call
failedProcessing failed for a non-review reason

Limits that matter in production

LimitValue
Pending sessions per API key20
Session TTL30 minutes
Upload URL TTL15 minutes
Workflow list default/max page size20 / 100
Metadata size4 KB serialized
Metadata nesting depth2 levels

Next pages