OpenCorpo API

Sign and verify AI-generated images with three independent layers of provenance — in a single API call.

The OpenCorpo ComplianceAPI helps you comply with EU AI Act Article 50 and the emerging global standard for AI content disclosure. Every image you sign receives three interlocking layers of provenance that can be independently verified at any time:

🔏
Invisible Watermark
A hidden identifier is encoded directly into the pixel data using LSB steganography on the blue channel — imperceptible to the human eye but machine-readable.
LSB steganography
📜
C2PA Metadata
A cryptographically signed Content Credentials manifest is embedded directly into the image file, binding provenance claims to the exact pixel content.
C2PA v2 / ECDSA-SHA256
🔍
Content Fingerprint
A perceptual hash of the image is recorded in the OpenCorpo registry. Even if metadata is stripped, the image can be identified by its visual fingerprint.
Average hash (64-bit)

All signed images are returned as PNG regardless of the input format. PNG is lossless, which is required to preserve the invisible watermark across downloads and re-uploads.


Authentication

All API requests require an API key passed in the X-API-Key request header.

Obtaining an API Key

Sign in to the OpenCorpo dashboard and navigate to API Keys. Create a new key and copy the full key value — it is only shown once.

Keep your API key secret. Do not expose it in client-side JavaScript or commit it to version control. Always call the API from a server-side environment.

Sending the Key

Include the key in every request as an HTTP header:

HTTP Header
X-API-Key: sk_live_your_api_key_here

Key Prefixes

PrefixTypeDescription
sk_test_stringTest key — sandbox mode, no billing
sk_live_stringLive key — production, usage is billed

Quick Start

Sign your first image in under 60 seconds.

1. Get your API key

Create a free account at compliance-app.opencorpo.com and generate an API key from the dashboard.

2. Sign an image

cURL
Python
Node.js
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/sign" \
  -H "X-API-Key: sk_live_your_key" \
  -F "file=@image.png" \
  -F "generator=Stable Diffusion XL" \
  -F "organization=Acme Inc" \
  --output signed.png
import requests

with open("image.png", "rb") as f:
    resp = requests.post(
        "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/sign",
        headers={"X-API-Key": "sk_live_your_key"},
        files={"file": ("image.png", f, "image/png")},
        data={
            "generator": "Stable Diffusion XL",
            "organization": "Acme Inc",
        },
    )

resp.raise_for_status()
data = resp.json()

print(f"Asset ID: {data['asset_id']}")
print(f"Layers:   {data['layers_applied']}")

# Download the signed PNG
download = requests.get(
    f"https://i2dygqhmdw.eu-west-3.awsapprunner.com{data['download_url']}",
    headers={"X-API-Key": "sk_live_your_key"},
)
with open("signed.png", "wb") as out:
    out.write(download.content)
import FormData from "form-data";
import fs from "fs";
import fetch from "node-fetch";

const form = new FormData();
form.append("file", fs.createReadStream("image.png"), "image.png");
form.append("generator", "Stable Diffusion XL");
form.append("organization", "Acme Inc");

const res = await fetch(
  "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/sign",
  { method: "POST", headers: { "X-API-Key": "sk_live_your_key" }, body: form }
);

const data = await res.json();
console.log("Asset ID:", data.asset_id);
console.log("Layers:  ", data.layers_applied);

3. Verify the signed image

cURL
Python
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify" \
  -H "X-API-Key: sk_live_your_key" \
  -F "file=@signed.png"
import requests

with open("signed.png", "rb") as f:
    resp = requests.post(
        "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify",
        headers={"X-API-Key": "sk_live_your_key"},
        files={"file": ("signed.png", f, "image/png")},
    )

data = resp.json()
print(f"Status:      {data['compliance_status']}")   # compliant
print(f"Watermark:   {data['has_watermark']}")         # True
print(f"C2PA valid:  {data['has_c2pa']}")              # True
print(f"Fingerprint: {data['fingerprint_match']}")     # True

Base URL

All API requests should be made to the following base URL:

Base URL
https://i2dygqhmdw.eu-west-3.awsapprunner.com

All endpoints accept and return JSON unless otherwise noted. File uploads use multipart/form-data. Signed images are returned as binary image/png.


Sign Image

Apply all three compliance layers to an AI-generated image and receive a signed PNG with embedded provenance data.

POST /v1/sign

Request

Send a multipart/form-data request with the image file and optional provenance metadata.

Form Fields

FieldTypeDescription
file required file The image to sign. Accepted formats: JPEG, PNG, WebP. Maximum size: 20 MB.
generator optional string Name of the AI model or tool that generated the image. e.g. Stable Diffusion XL, DALL·E 3, Midjourney v6. Embedded in the C2PA manifest.
generator_version optional string Version of the AI model. e.g. 1.0.0. Embedded in the C2PA manifest.
organization optional string Organization name to attribute in the C2PA CreativeWork assertion. e.g. Acme Inc.

Response 200 OK

JSON
{
  "asset_id": "a3f1c9b2e041",
  "status": "signed",
  "layers_applied": [
    "c2pa_metadata",
    "invisible_watermark",
    "content_fingerprint"
  ],
  "compliance": {
    "eu_ai_act_article_50": true
  },
  "download_url": "/v1/download/a3f1c9b2e041",
  "report_url": "/v1/report/a3f1c9b2e041",
  "verify_url": "/v1/verify/a3f1c9b2e041",
  "processing_ms": 843,
  "file_size": 2471038
}

Response Fields

FieldTypeDescription
asset_idstringUnique identifier for this signed asset. Use this to download or report on the image.
statusstringAlways "signed" on success.
layers_appliedstring[]List of the compliance layers successfully applied to the image.
compliance.eu_ai_act_article_50booleanWhether the image meets the EU AI Act Article 50 disclosure requirements.
download_urlstringPath to download the signed PNG. Prepend the base URL.
report_urlstringPath to retrieve the full compliance report as JSON.
processing_msintegerTotal server-side processing time in milliseconds.
file_sizeintegerSize of the signed output file in bytes.

Full Example

cURL
Python
Node.js
PHP
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/sign" \
  -H "X-API-Key: sk_live_your_api_key" \
  -F "file=@portrait.jpg" \
  -F "generator=Midjourney v6" \
  -F "generator_version=6.0" \
  -F "organization=Acme Media" \
  -o response.json
import requests

API_KEY = "sk_live_your_api_key"
BASE    = "https://i2dygqhmdw.eu-west-3.awsapprunner.com"

def sign_image(path: str, generator: str, org: str) -> dict:
    with open(path, "rb") as f:
        mime = "image/png" if path.endswith(".png") else "image/jpeg"
        resp = requests.post(
            f"{BASE}/v1/sign",
            headers={"X-API-Key": API_KEY},
            files={"file": (path, f, mime)},
            data={"generator": generator, "organization": org},
            timeout=60,
        )
    resp.raise_for_status()
    return resp.json()

result = sign_image("portrait.jpg", "Midjourney v6", "Acme Media")
print(result)
import FormData from "form-data";
import fs from "fs";
import fetch from "node-fetch";

const API_KEY = "sk_live_your_api_key";
const BASE    = "https://i2dygqhmdw.eu-west-3.awsapprunner.com";

async function signImage(filePath, generator, organization) {
  const form = new FormData();
  form.append("file", fs.createReadStream(filePath));
  form.append("generator", generator);
  form.append("organization", organization);

  const res = await fetch(`${BASE}/v1/sign`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, ...form.getHeaders() },
    body: form,
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.detail ?? `HTTP ${res.status}`);
  }

  return res.json();
}

const result = await signImage("portrait.jpg", "Midjourney v6", "Acme Media");
console.log(result);
<?php
$apiKey = 'sk_live_your_api_key';
$base   = 'https://i2dygqhmdw.eu-west-3.awsapprunner.com';

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL            => "$base/v1/sign",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $apiKey"],
    CURLOPT_POSTFIELDS     => [
        'file'         => new CURLFile('portrait.jpg', 'image/jpeg'),
        'generator'    => 'Midjourney v6',
        'organization' => 'Acme Media',
    ],
]);

$body   = curl_exec($curl);
$result = json_decode($body, true);
echo $result['asset_id'];
?>

Verify Image

Check whether an image carries valid OpenCorpo provenance data. Works on any image — returns detection results for each compliance layer independently.

POST /v1/verify

Request

Send a multipart/form-data request with the image to verify.

FieldTypeDescription
file required file The image to verify. Accepted: JPEG, PNG, WebP. Maximum size: 20 MB.

Response 200 OK

JSON — Compliant
JSON — Non-compliant
{
  "compliance_status": "compliant",
  "has_watermark": true,
  "watermark_id": "OPENCORPO:a3f1c9b2e041",
  "has_c2pa": true,
  "c2pa_details": {
    "claim_generator": "OpenCorpo ComplianceAPI/0.1.0",
    "title": "portrait.jpg",
    "assertions_count": 2
  },
  "fingerprint_match": true,
  "matched_asset_id": "a3f1c9b2e041"
}
{
  "compliance_status": "non-compliant",
  "has_watermark": false,
  "watermark_id": null,
  "has_c2pa": false,
  "c2pa_details": null,
  "c2pa_error": "No C2PA manifest found in this file",
  "fingerprint_match": false,
  "matched_asset_id": null
}

Response Fields

FieldTypeDescription
compliance_statusstring"compliant" all 3 layers detected · "partial" 1–2 layers detected · "non-compliant" no layers detected
has_watermarkbooleanWhether an OpenCorpo invisible watermark was found in the pixel data.
watermark_idstring | nullThe identifier embedded in the watermark, in the format OPENCORPO:{asset_id}.
has_c2pabooleanWhether a valid C2PA Content Credentials manifest was found.
c2pa_detailsobject | nullParsed manifest fields: claim_generator, title, assertions_count.
c2pa_errorstring | nullTechnical detail when C2PA detection fails. Useful for diagnosing why a manifest is absent or invalid.
fingerprint_matchbooleanWhether the image's perceptual hash matches a record in the OpenCorpo registry.
matched_asset_idstring | nullThe asset ID of the matching registry entry, if a fingerprint match was found.

The fingerprint check compares the image against assets signed by your account only. An image signed by a different OpenCorpo user will return fingerprint_match: false even if it is otherwise fully compliant.

Full Example

cURL
Python
Node.js
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify" \
  -H "X-API-Key: sk_live_your_api_key" \
  -F "file=@signed.png"
import requests

def verify_image(path: str) -> dict:
    with open(path, "rb") as f:
        resp = requests.post(
            "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify",
            headers={"X-API-Key": "sk_live_your_api_key"},
            files={"file": (path, f, "image/png")},
            timeout=30,
        )
    resp.raise_for_status()
    return resp.json()

result = verify_image("signed.png")

if result["compliance_status"] == "compliant":
    print("✓ Fully compliant — all 3 layers verified")
    print(f"  Signed by: {result['c2pa_details']['claim_generator']}")
    print(f"  Asset ID:  {result['matched_asset_id']}")
elif result["compliance_status"] == "partial":
    print("⚠ Partial compliance")
else:
    print("✗ Not compliant — no provenance data found")
import FormData from "form-data";
import fs from "fs";
import fetch from "node-fetch";

const form = new FormData();
form.append("file", fs.createReadStream("signed.png"));

const res = await fetch(
  "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify",
  {
    method: "POST",
    headers: { "X-API-Key": "sk_live_your_api_key", ...form.getHeaders() },
    body: form,
  }
);

const data = await res.json();
console.log("Status:", data.compliance_status);
console.log("Watermark:", data.has_watermark);
console.log("C2PA:", data.has_c2pa);
console.log("Fingerprint:", data.fingerprint_match);

Download Signed Image

Retrieve the signed PNG file for a previously signed asset.

GET /v1/download/{asset_id}

Path Parameters

ParameterTypeDescription
asset_id required string The asset ID returned by /v1/sign.

Response

Returns the signed PNG file as a binary stream with Content-Type: image/png and a Content-Disposition: attachment header.

cURL
Python
curl "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/download/a3f1c9b2e041" \
  -H "X-API-Key: sk_live_your_api_key" \
  --output signed.png
import requests

resp = requests.get(
    "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/download/a3f1c9b2e041",
    headers={"X-API-Key": "sk_live_your_api_key"},
    stream=True,
)
resp.raise_for_status()

with open("signed.png", "wb") as f:
    for chunk in resp.iter_content(chunk_size=8192):
        f.write(chunk)

Compliance Report

Retrieve a full compliance report for a signed asset, including a checklist against EU AI Act Article 50 requirements.

GET /v1/report/{asset_id}

Path Parameters

ParameterTypeDescription
asset_id required string The asset ID returned by /v1/sign.

Response 200 OK

JSON
{
  "report_id": "rpt_a3f1c9b2e041",
  "asset_id": "a3f1c9b2e041",
  "generated_at": "2025-11-14T09:22:04Z",
  "asset": {
    "asset_id": "a3f1c9b2e041",
    "filename": "portrait.jpg",
    "original_content_type": "image/jpeg",
    "content_type": "image/png",
    "generator": "Midjourney v6",
    "organization": "Acme Media",
    "signed_at": "2025-11-14T09:21:58Z",
    "file_size": 2471038,
    "processing_ms": 843
  },
  "compliance_checklist": {
    "article_50_2_machine_readable_marking": {
      "status": "pass",
      "method": "C2PA v2 manifest with signed provenance chain"
    },
    "article_50_2_watermarking": {
      "status": "pass",
      "method": "Invisible spatial-domain watermark embedded in image"
    },
    "article_50_2_content_fingerprint": {
      "status": "pass",
      "method": "Perceptual hash stored in content registry"
    },
    "code_of_practice_multi_layer": {
      "status": "pass",
      "note": "3 of 3 layers implemented per EU Code of Practice draft"
    },
    "digital_source_type": {
      "status": "pass",
      "value": "trainedAlgorithmicMedia (IPTC vocabulary)"
    }
  }
}

Health Check

A simple health check endpoint for uptime monitoring and load balancer probes. Does not require authentication.

GET /health
Response
{
  "status": "ok",
  "timestamp": "2025-11-14T09:22:04.123456Z"
}

The Three Compliance Layers

OpenCorpo applies three independent, complementary layers of provenance to every signed image. Each layer can survive scenarios the others cannot.

LayerSurvives metadata strip?Survives re-save as PNG?Survives screenshot?
Invisible Watermark
C2PA Metadata
Perceptual Fingerprint

🔏 Invisible Watermark

The watermark is embedded using LSB (Least Significant Bit) steganography on the blue channel of the image. The technique modifies the lowest-order bit of selected pixel values to encode a sequence of bits representing the message OPENCORPO:{asset_id}.

How it works

  1. The image is converted to RGB and the blue channel is extracted as a flat array.
  2. A deterministic set of pixel positions is selected using a SHA-256 seed derived from the server's watermark key.
  3. The ASCII message is encoded as bits and written one bit per pixel into the LSB of each selected blue pixel.
  4. A null terminator byte marks the end of the message.

Why PNG output?

LSB watermarks are destroyed by JPEG compression, which modifies pixel values as part of DCT encoding. All signed images are therefore output as lossless PNG to preserve the watermark across subsequent downloads and re-uploads.

The watermark will not survive screenshot capture, re-encoding to JPEG, or significant image manipulation (resizing, cropping, colour grading). It is designed to survive lossless round-trips and normal PNG re-saves.

📜 C2PA Metadata

The C2PA (Coalition for Content Provenance and Authenticity) standard defines a cryptographically signed manifest format for embedding provenance data directly into a media file. OpenCorpo uses C2PA v2 with ECDSA-SHA256 signing.

What is embedded

  • c2pa.actions — Records the c2pa.created action with the AI generator name, version, and IPTC trainedAlgorithmicMedia digital source type.
  • stds.schema-org.CreativeWork — Records the author organization using Schema.org vocabulary.

Certificate chain

OpenCorpo generates a test CA and end-entity certificate pair using P-256 (secp256r1) elliptic curve keys. In production environments, certificates signed by a trusted CA registered with the C2PA Trust List should be used for full cross-platform verification.

Viewing Content Credentials

C2PA-signed images can be inspected using the Content Credentials viewer at contentcredentials.org/verify, or any C2PA-compatible tool.

🔍 Content Fingerprint

A perceptual hash (average hash, 64-bit) is computed from the watermarked image and stored in the OpenCorpo registry at signing time.

Algorithm

  1. The image is converted to grayscale and resized to 8×8 pixels.
  2. The average pixel value is computed.
  3. Each pixel is compared to the average — 1 if above, 0 if below — producing a 64-bit binary string.

During verification, the same hash is computed from the uploaded image and compared against every asset in the account's registry. If a match is found, the matched_asset_id is returned.

The perceptual fingerprint is robust to minor colour shifts and lossless transformations. It will even match a screenshot of the signed image in many cases, making it the most persistent of the three layers.


Error Reference

The API returns standard HTTP status codes. Error responses include a detail field with a human-readable message.

Error Response
{
  "detail": "Unsupported type: image/gif. Allowed: {'image/jpeg', 'image/png', 'image/webp'}"
}
StatusCodeCause & Resolution
400 Bad Request Invalid file format, file too large (>20 MB), or missing required fields. Check the detail field for the specific reason.
401 Unauthorized The X-API-Key header is missing. Add your API key to the request.
403 Forbidden The provided API key is invalid or has been revoked. Check the key in your dashboard.
404 Not Found The asset_id does not exist in the registry, or the signed file was not found on the server.
422 Unprocessable Entity The request body could not be parsed. Ensure you are sending multipart/form-data with the correct field names.
500 Internal Server Error An unexpected server error occurred. This is likely transient — retry after a few seconds. If the issue persists, contact support.

Rate Limits & Plans

Usage is metered per account. Limits apply to the total number of /v1/sign and /v1/verify calls per calendar month.

Plan Monthly Sign Calls Monthly Verify Calls API Keys Support
Trial 100 100 1 Community
Pro 5,000 10,000 5 Email
Business Unlimited Unlimited Unlimited Priority

Rate limit headers are included in every response:

HeaderDescription
X-RateLimit-LimitYour monthly call limit for this endpoint type.
X-RateLimit-RemainingCalls remaining this calendar month.
X-RateLimit-ResetUnix timestamp of when the limit resets (first of next month, UTC).

When you exceed your monthly limit, the API returns 429 Too Many Requests. Upgrade your plan to increase limits.


Supported Formats
FormatMIME TypeUploadSigned Output
JPEGimage/jpegConverted to PNG
PNGimage/pngPNG
WebPimage/webpConverted to PNG
GIFimage/gif
TIFFimage/tiff

All signed output is PNG regardless of input format. This is required to preserve the invisible watermark, which is destroyed by lossy compression such as JPEG encoding.


Python — Full Example

A complete Python script that signs an image, verifies it, and downloads the signed file.

opencorpo_example.py
"""
OpenCorpo ComplianceAPI — Python example
pip install requests
"""
import requests
import sys

API_KEY = "sk_live_your_api_key"
BASE    = "https://i2dygqhmdw.eu-west-3.awsapprunner.com"
HEADERS = {"X-API-Key": API_KEY}


def sign(image_path: str) -> dict:
    """Sign an image and return the API response."""
    print(f"Signing {image_path}...")
    with open(image_path, "rb") as f:
        mime = "image/png" if image_path.endswith(".png") else "image/jpeg"
        resp = requests.post(
            f"{BASE}/v1/sign",
            headers=HEADERS,
            files={"file": (image_path, f, mime)},
            data={"generator": "My AI App", "organization": "Acme Inc"},
            timeout=60,
        )
    resp.raise_for_status()
    return resp.json()


def download(asset_id: str, output_path: str):
    """Download the signed PNG for an asset."""
    print(f"Downloading signed image → {output_path}")
    resp = requests.get(f"{BASE}/v1/download/{asset_id}", headers=HEADERS, stream=True)
    resp.raise_for_status()
    with open(output_path, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)


def verify(image_path: str) -> dict:
    """Verify an image and return the compliance result."""
    print(f"Verifying {image_path}...")
    with open(image_path, "rb") as f:
        resp = requests.post(
            f"{BASE}/v1/verify",
            headers=HEADERS,
            files={"file": (image_path, f, "image/png")},
            timeout=30,
        )
    resp.raise_for_status()
    return resp.json()


if __name__ == "__main__":
    image = sys.argv[1] if len(sys.argv) > 1 else "image.jpg"

    # 1. Sign
    result = sign(image)
    asset_id = result["asset_id"]
    print(f"  Asset ID : {asset_id}")
    print(f"  Layers   : {', '.join(result['layers_applied'])}")

    # 2. Download
    signed_path = f"signed_{asset_id}.png"
    download(asset_id, signed_path)

    # 3. Verify the signed file
    v = verify(signed_path)
    status = v["compliance_status"]
    layers_ok = sum([v["has_watermark"], v["has_c2pa"], v["fingerprint_match"]])
    print(f"\nVerification result: {status.upper()} ({layers_ok}/3 layers)")
    print(f"  Watermark   : {'✓' if v['has_watermark'] else '✗'} {v.get('watermark_id','')}")
    print(f"  C2PA        : {'✓' if v['has_c2pa'] else '✗'}")
    print(f"  Fingerprint : {'✓' if v['fingerprint_match'] else '✗'}")
Node.js — Full Example

A complete Node.js script using the native fetch API (Node 18+).

opencorpo.mjs
/**
 * OpenCorpo ComplianceAPI — Node.js example
 * Node 18+ (native fetch) or: npm install node-fetch form-data
 */
import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";

const API_KEY = "sk_live_your_api_key";
const BASE    = "https://i2dygqhmdw.eu-west-3.awsapprunner.com";

async function sign(imagePath) {
  const { default: FormData } = await import("form-data");
  const form = new FormData();
  form.append("file", createReadStream(imagePath));
  form.append("generator", "My AI App");
  form.append("organization", "Acme Inc");

  const res = await fetch(`${BASE}/v1/sign`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, ...form.getHeaders() },
    body: form,
  });

  if (!res.ok) throw new Error(`Sign failed: ${res.status}`);
  return res.json();
}

async function download(assetId, outputPath) {
  const res = await fetch(`${BASE}/v1/download/${assetId}`, {
    headers: { "X-API-Key": API_KEY },
  });
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);
  await pipeline(res.body, createWriteStream(outputPath));
}

async function verify(imagePath) {
  const { default: FormData } = await import("form-data");
  const form = new FormData();
  form.append("file", createReadStream(imagePath));

  const res = await fetch(`${BASE}/v1/verify`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, ...form.getHeaders() },
    body: form,
  });

  if (!res.ok) throw new Error(`Verify failed: ${res.status}`);
  return res.json();
}

// --- Run ---
const result = await sign("image.jpg");
console.log("Asset ID:", result.asset_id);
console.log("Layers:  ", result.layers_applied.join(", "));

const signedPath = `signed_${result.asset_id}.png`;
await download(result.asset_id, signedPath);
console.log("Downloaded →", signedPath);

const v = await verify(signedPath);
console.log("\nCompliance:", v.compliance_status.toUpperCase());
console.log("  Watermark  :", v.has_watermark ? "✓" : "✗");
console.log("  C2PA       :", v.has_c2pa      ? "✓" : "✗");
console.log("  Fingerprint:", v.fingerprint_match ? "✓" : "✗");
cURL — Reference Sheet

All API operations as copy-paste cURL commands.

All Endpoints
# ── Sign an image ──────────────────────────────────────────────────────────
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/sign" \
  -H "X-API-Key: sk_live_your_key" \
  -F "file=@image.jpg" \
  -F "generator=Stable Diffusion XL" \
  -F "organization=Acme Inc"

# ── Verify an image ─────────────────────────────────────────────────────────
curl -X POST "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/verify" \
  -H "X-API-Key: sk_live_your_key" \
  -F "file=@signed.png"

# ── Download a signed image ─────────────────────────────────────────────────
curl "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/download/a3f1c9b2e041" \
  -H "X-API-Key: sk_live_your_key" \
  --output signed.png

# ── Get compliance report ───────────────────────────────────────────────────
curl "https://i2dygqhmdw.eu-west-3.awsapprunner.com/v1/report/a3f1c9b2e041" \
  -H "X-API-Key: sk_live_your_key"

# ── Health check (no auth required) ────────────────────────────────────────
curl "https://i2dygqhmdw.eu-west-3.awsapprunner.com/health"