ShippedFlow Partner API

Submit documents for processing and receive results via webhook

API v1 · For integrators

Overview

The Partner API lets external integrators submit shipping documents (invoice, packing list, bill of lading) for automated extraction and discrepancy detection. Processing is asynchronous: you POST documents to /api/v1/partner/submit, and ShippedFlow delivers the extracted structured data to a webhook URL you provide.

How it works

  1. You POST documents and a correlationId (a UUID you generate) to our submit endpoint. We return 202 Accepted.
  2. We process the documents asynchronously using our OCR and AI pipeline.
  3. We POST the structured results to your webhookUrl, with your correlationId echoed back at the top of the payload.

Audience: this API is for backend-to-backend integrations. The general API documentation (form-based submissions, MDC pipeline) lives at the main docs site.

Authentication

Every request to the Partner API must include a Bearer token issued by ShippedFlow:

Authorization: Bearer <your_api_key>

Per-customer billing

You receive a single API key. Each request includes a customerRef field — an opaque string identifying which of your end-customers caused the request. ShippedFlow uses it only for billing aggregation; we never treat it as a credential. You can use any string format you like (UUID, slug, name, etc.) up to 255 characters.

Why one key, not per-customer keys? Authentication identifies you (the integrator); customerRef identifies who pays for the job. Decoupling these means you manage one credential and never have to rotate per-customer secrets.

Submit Endpoint

POST https://api.shippedflow.info/api/v1/partner/submit

Submit a set of documents for processing. Returns immediately with 202 Accepted; the actual results arrive later via webhook.

Request Headers

HeaderRequiredDescription
Authorization required Bearer <api_key>
Content-Type required application/json
Idempotency-Key optional UUID string. If repeated, returns the original response instead of re-processing. Useful for safely retrying after network failures.

Request Body

FieldTypeRequiredDescription
correlationId string (UUID) required UUID v4 you generate. Echoed back in the webhook so you can thread response to request.
customerRef string (1–255 chars) required Your end-customer identifier. Used only for billing aggregation; format is up to you.
webhookUrl string (HTTPS URL) required Where ShippedFlow will POST the processing results.
operationType string optional IMPORT or EXPORT. Advisory hint.
documents array required 1–20 documents to process. Total request body ≤ 50 MB.
documents[].name string (1–255 chars) required Original filename (used in logs and S3 keys).
documents[].type string optional INVOICE, PACKING_LIST, BILL_OF_LADING, or OTHER. Advisory; our pipeline classifies internally.
documents[].filedata string (base64) required File contents, base64-encoded. ≤ 10 MB decoded per file.

Example Request

POST /api/v1/partner/submit HTTP/1.1
Host: api.shippedflow.info
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: 7f3a9c2b-1d4e-4b8a-9c1f-2e5d6a7b8c9d

{
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "customerRef": "futurbano-sl",
  "webhookUrl": "https://your-system.example.com/webhooks/shippedflow",
  "operationType": "IMPORT",
  "documents": [
    {
      "name": "invoice.pdf",
      "type": "INVOICE",
      "filedata": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9MZW5n..."
    },
    {
      "name": "packing-list.pdf",
      "type": "PACKING_LIST",
      "filedata": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9MZW5n..."
    }
  ]
}

Success Response

202 Accepted — request validated and queued for processing.

{
  "status": "accepted",
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "customerRef": "futurbano-sl",
  "jobId": "sf-job-7f3a9c2b1d4e",
  "receivedAt": "2026-05-31T14:23:00Z"
}

Webhook Delivery

When processing completes (success or failure), ShippedFlow POSTs the result to the webhookUrl you provided.

Webhook Request

Method: POST

Headers we send:

HeaderDescription
Content-Type application/json
X-Shippedflow-Signature sha256=<hex> — HMAC-SHA256 of the raw body using your shared secret. You must verify this before trusting the payload.
X-Shippedflow-Delivery-Id UUID, unique per delivery attempt. Use it to deduplicate if you receive retries.
User-Agent Shippedflow-Webhook/1.0

Webhook Body

FieldTypeDescription
correlationId string Echoed from your submit request — your thread identifier.
customerRef string Echoed from your submit request.
status string success, error, or partial.
jobId string Same value as in our submit response. Useful for support.
completedAt string (ISO 8601) When processing finished on our side.
summary object Aggregated totals (currency, total amount, weights, packages). See the results JSON schema for full structure.
invoice_details object Header-level metadata (exporter, consignee, vessel, ports, container numbers).
data array One entry per line item: quantities, prices, TARIC code, classification confidence.
discrepancies array AI-detected mismatches between documents. Closed list of Type values: Consolidation mismatch, Sum mismatch, Quantity mismatch, Missing data, Other. Treat any unknown future value as Other.
error object Present only when status = error. Contains {code, message}.

Missing or unknown values: JSON null is used across every field — both string fields and numeric fields — when the source documents don't carry the data. Type stability is preserved per field (e.g. summary.total_amount is number | null, invoice_details.vessel is string | null). Use the canonical value === null check rather than empty-string or string-sentinel comparisons.

Error Codes (when status = "error")

When processing fails permanently, the webhook arrives with status: "error" and an error object containing a code and a human-readable message. The code is a closed list — treat any unknown future value as internal_error.

{
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "customerRef": "your-ref",
  "status": "error",
  "jobId": "sf-job-550e8400",
  "completedAt": "2026-06-14T10:23:00.000Z",
  "error": {
    "code": "submission_too_large",
    "message": "Combined document size exceeds processing limit. Please split the submission into smaller batches."
  }
}
error.codeWhenRecommended partner action
submission_too_large The combined documents exceeded our processing budget during analysis (input is larger than the model can handle). Split the submission into smaller batches and resubmit.
document_unreadable One or more documents could not be parsed: corrupt PDF, OCR failure, or the model failed to extract structured content. Verify file integrity (the PDF opens cleanly outside our system) and resubmit. If the file is fine, contact support.
incomplete_submission The submission is missing required document types for the declared operationType. IMPORT requires Invoice + Packing List + Bill of Lading. EXPORT requires Invoice + Packing List. Include all required document types in a single submission and resubmit.
documents_mismatch The submitted documents appear to belong to different shipments — large inconsistencies were detected between invoice and packing list totals (quantities, weights, packages, or volume) that go beyond normal rounding. Verify that the invoice and packing list belong to the same shipment and resubmit.
unsupported_content The submission contains content we cannot currently process (unsupported language, document type, or layout). Contact support — we'll triage whether the case fits the current scope or needs to land in our backlog.
processing_timeout Processing exceeded the maximum allowed time (transient issue or unusually slow upstream). Resubmit. If the problem persists for the same submission, contact support.
internal_error Catch-all for any unmapped failure on our side. Resubmit later. If the problem persists, contact support with the correlationId so we can trace it.

Privacy of error details: the message field is a stable, generic description tied to the code. We never expose internal details (model names, token counts, stack traces) to partners. Match on code for automation, surface message to end users.

Expected Response

Your endpoint should return any 2xx status within 30 seconds. Response body is ignored.

Retry policy: on non-2xx or timeout, we retry at 5 minutes, 30 minutes, and 2 hours. After the third failure we mark the submission as ERROR and stop. Use X-Shippedflow-Delivery-Id to deduplicate retries.

Verifying the HMAC Signature

The signature proves the webhook came from ShippedFlow and the body wasn't modified in transit. Compute HMAC-SHA256 of the raw request body using your shared secret, and compare to the value in X-Shippedflow-Signature:

Python (Flask)

import hmac
import hashlib
from flask import request, abort

SHIPPEDFLOW_HMAC_SECRET = "your-shared-secret-here"

def verify_shippedflow_signature():
    received = request.headers.get("X-Shippedflow-Signature", "")
    if not received.startswith("sha256="):
        abort(401)
    expected = "sha256=" + hmac.new(
        SHIPPEDFLOW_HMAC_SECRET.encode(),
        request.get_data(),   # raw body, NOT request.get_json()
        hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(received, expected):
        abort(401)

Node.js (Express)

const crypto = require("crypto");

const SHIPPEDFLOW_HMAC_SECRET = "your-shared-secret-here";

function verifyShippedflowSignature(req) {
  const received = req.header("X-Shippedflow-Signature") || "";
  if (!received.startsWith("sha256=")) return false;
  const expected = "sha256=" + crypto
    .createHmac("sha256", SHIPPEDFLOW_HMAC_SECRET)
    .update(req.rawBody)       // raw body buffer, NOT req.body parsed
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(received),
    Buffer.from(expected)
  );
}

Important: verify against the raw request body bytes, not the re-serialized JSON. Most frameworks parse and re-serialize JSON, which can change whitespace and break the signature.

Error Responses

All error responses share this shape:

{
  "error": "validation_failed",
  "message": "documents[2].filedata is not valid base64",
  "correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

The correlationId is echoed when present so you can still match errors to requests.

HTTPerror codeWhen
400 invalid_request Malformed JSON or missing required field.
401 unauthorized Missing or invalid API key.
409 duplicate_correlation_id This correlationId was already accepted. The original jobId is returned in the body so you can keep using it.
413 payload_too_large A file exceeded 10 MB (decoded), the request exceeded 50 MB total, or you sent more than 20 documents.
415 unsupported_file_type A file's content isn't a supported format (currently PDF only).
422 validation_failed A field is present but malformed (bad UUID, non-HTTPS webhook, invalid base64, etc.).
429 rate_limited You're sending requests too quickly. Retry with backoff.
500 internal_error Something went wrong on our side. Safe to retry after a short delay.

Examples

cURL

curl -X POST https://api.shippedflow.info/api/v1/partner/submit \
  -H "Authorization: Bearer $SHIPPEDFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @- <<EOF
{
  "correlationId": "$(uuidgen)",
  "customerRef": "futurbano-sl",
  "webhookUrl": "https://your-system.example.com/webhooks/shippedflow",
  "operationType": "IMPORT",
  "documents": [
    {
      "name": "invoice.pdf",
      "type": "INVOICE",
      "filedata": "$(base64 -i invoice.pdf)"
    }
  ]
}
EOF

Python

import base64
import os
import uuid
import requests

API_KEY = os.environ["SHIPPEDFLOW_API_KEY"]
API_BASE = "https://api.shippedflow.info"

def submit(pdf_paths, customer_ref, webhook_url):
    documents = []
    for path in pdf_paths:
        with open(path, "rb") as f:
            documents.append({
                "name": os.path.basename(path),
                "type": "INVOICE",        # or PACKING_LIST / BILL_OF_LADING
                "filedata": base64.b64encode(f.read()).decode("ascii"),
            })

    correlation_id = str(uuid.uuid4())
    response = requests.post(
        f"{API_BASE}/api/v1/partner/submit",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "correlationId": correlation_id,
            "customerRef": customer_ref,
            "webhookUrl": webhook_url,
            "operationType": "IMPORT",
            "documents": documents,
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    result = submit(
        ["invoice.pdf", "packing-list.pdf"],
        customer_ref="futurbano-sl",
        webhook_url="https://your-system.example.com/webhooks/shippedflow",
    )
    print(f"Accepted: jobId={result['jobId']} correlationId={result['correlationId']}")

Webhook receiver (Python / Flask)

import hmac
import hashlib
import os
from flask import Flask, request, jsonify, abort

app = Flask(__name__)
SECRET = os.environ["SHIPPEDFLOW_HMAC_SECRET"].encode()

@app.route("/webhooks/shippedflow", methods=["POST"])
def shippedflow_webhook():
    # 1. Verify signature against raw body
    received = request.headers.get("X-Shippedflow-Signature", "")
    expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(received, expected):
        abort(401)

    # 2. Deduplicate by delivery id (in case of retries)
    delivery_id = request.headers.get("X-Shippedflow-Delivery-Id")
    if already_processed(delivery_id):
        return jsonify({"status": "duplicate_ignored"}), 200

    # 3. Process payload
    payload = request.get_json()
    save_result(payload["correlationId"], payload)

    return jsonify({"status": "ok"}), 200

Test Environment

Use the preprod environment for integration development and testing. It runs the same code as production but against a separate database and S3 bucket.

EnvironmentAPI base URL
Production https://api.shippedflow.info
Preprod (test) https://test-api.shippedflow.info

You'll receive separate API keys and HMAC secrets for each environment.

Need credentials or have questions? Contact your ShippedFlow account contact to provision API keys, webhook secrets, and any environment-specific configuration.