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
- You POST documents and a
correlationId(a UUID you generate) to our submit endpoint. We return202 Accepted. - We process the documents asynchronously using our OCR and AI pipeline.
- We POST the structured results to your
webhookUrl, with yourcorrelationIdechoed 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
Submit a set of documents for processing. Returns immediately with 202 Accepted; the actual results arrive later via webhook.
Request Headers
| Header | Required | Description |
|---|---|---|
| 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
| Field | Type | Required | Description |
|---|---|---|---|
| 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:
| Header | Description |
|---|---|
| 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
| Field | Type | Description |
|---|---|---|
| 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.
| HTTP | error code | When |
|---|---|---|
| 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)"
}
]
}
EOFPython
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"}), 200Test 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.
| Environment | API 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.