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://test-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 (sum mismatches, missing data, value mismatches).
error object Present only when status = error. Contains {code, message}.

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://test-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://test-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.