STAGING ENVIRONMENT

Dashboard

System Components

Today

requests

This Week

requests

Success Rate

Avg Duration

successful requests

Signing Container

Activity (30 days)

By Format

Projects

ProjectRequestsSuccessErrorsSignsVerifiesAvg msLast Used

Recent Errors

API Keys

Create API Key

Comma-separated. Leave empty to allow all origins (server-to-server).

Leave empty for no expiration

Max file size (MB, 1โ€“200, default 100)

Revoke API Key

You are about to revoke the key for . This action cannot be undone. All API calls using this key will immediately fail.

Edit API Key

Project:

Comma-separated. Empty = allow all origins.

Empty = no expiration.

Max file size (MB)

Key created successfully

Copy it now โ€” it won't be shown again.

ProjectStatusExpiresRequestsSuccessErrorsLast UsedCreated

Audit Log

TimeOpFormatStatusProjectClient IPSizeDurationErrorRequest ID

Integration Guide

Quick Start

Base URL: https://c2pa.mirrorstage.net

All endpoints use the /v1/ prefix.

Every request (except /v1/health) requires authentication:

Authorization: Bearer <API_KEY>

Each project gets a dedicated API key. Create one from the page. The key is shown only once at creation.

Endpoints

GET /v1/health No auth required

Check service status.

// Response 200 { "status": "ok", "version": "1.0" }
POST /v1/warmup Optional

Pre-warms the signing container. Cold starts are handled transparently by the gateway, but this endpoint lets you pre-warm before batch operations for guaranteed low latency. Safe to call when already warm.

// Response 200 { "status": "warm" }
POST /v1/sign

Embeds a C2PA manifest into a media file. Returns the signed file as binary download.

Important: The file must be a valid, complete media file. Placeholders, empty files, or files <100 bytes will be rejected.

Request: multipart/form-data

FieldTypeRequiredDescription
fileFile*Media file (direct upload)
file_urlString*HTTPS URL to fetch file from (alternative to file)
manifestStringYesJSON manifest (see below)
formatStringYesimage/jpeg image/png image/webp image/tiff image/avif video/mp4 audio/mpeg audio/wav audio/flac audio/mp4

* Provide either file (upload) or file_url (remote fetch), not both. HTTPS only. Max file size is per-key (default 100MB, up to 200MB). If origin restrictions are set, the URL hostname must match an allowed origin.

Response 200

  • Content-Type: application/octet-stream โ€” signed file binary
  • X-Request-Id โ€” unique request ID (log this for debugging)
  • X-Duration-Ms โ€” processing time
POST /v1/verify

Reads and validates the C2PA manifest embedded in a signed file. Performs full validation: signature check, hash binding, certificate chain, OCSP revocation.

Send the signed file as Content-Type: application/octet-stream in the request body.

// Response 200 { "format": "image/jpeg", // auto-detected MIME type "validation_state": "Valid", // "Invalid" | "Valid" | "Trusted" "valid": true, // false if Invalid "trusted": false, // true only with C2PA Trust List cert "manifest": { ... }, "signer": "BitForFun", "timestamp": null, "validation_errors": ["signingCredential.untrusted: signing certificate untrusted"], "validation_warnings": [] }

Validation States:

  • Invalid โ€” signature mismatch, tampered file, or broken manifest
  • Valid โ€” signature and hashes correct, but cert not on C2PA Trust List
  • Trusted โ€” fully verified with a trusted C2PA certificate

Error Codes (sign & verify)

CodeDescription
400Invalid request (missing fields, bad JSON, unsupported format, magic bytes mismatch, no C2PA data on verify)
401Missing or invalid API key
413File too large (exceeds per-key limit, default 100MB, max 200MB).
429Rate limit exceeded (per IP or per project). Includes Retry-After header.
502Signing service error (rare โ€” cold starts are handled transparently by the gateway).

Manifest JSON

The manifest defines provenance metadata embedded in the file. Use this template for AI-generated content (EU AI Act compliant):

{ "claim_generator": "MirrorStage/1.0", "title": "Generated Content", "assertions": [ { "label": "c2pa.actions", "data": { "actions": [{ "action": "c2pa.created", "softwareAgent": { "name": "MirrorStage", "version": "1.0" }, "parameters": { "description": "AI-generated content" }, // REQUIRED for EU AI Act compliance "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" }] } }, { "label": "stds.schema-org.CreativeWork", "data": { "@type": "CreativeWork", "author": [{ "@type": "Organization", "name": "MirrorStage" }] } } ] }

Key fields:

  • claim_generator โ€” identifies your application and version
  • title โ€” human-readable content title
  • action: c2pa.created โ€” indicates content was created (not edited)
  • digitalSourceType: trainedAlgorithmicMedia โ€” IPTC code for AI-generated. Required for EU AI Act.
  • stds.schema-org.CreativeWork โ€” attribution metadata (author organization)

Code Examples

Sign an image

curl -X POST https://c2pa.mirrorstage.net/v1/sign \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  -F 'manifest={"claim_generator":"MirrorStage/1.0","title":"My Image","assertions":[{"label":"c2pa.actions","data":{"actions":[{"action":"c2pa.created","digitalSourceType":"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"}]}}]}' \
  -F "format=image/jpeg" \
  --output my-image-signed.jpg

Sign from a remote URL

curl -X POST https://c2pa.mirrorstage.net/v1/sign \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file_url=https://cdn.example.com/my-image.jpg" \
  -F 'manifest={"claim_generator":"MirrorStage/1.0","title":"My Image","assertions":[{"label":"c2pa.actions","data":{"actions":[{"action":"c2pa.created","digitalSourceType":"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"}]}}]}' \
  -F "format=image/jpeg" \
  --output my-image-signed.jpg

Verify a signed file

curl -X POST https://c2pa.mirrorstage.net/v1/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @my-image-signed.jpg
const C2PA_URL = "https://c2pa.mirrorstage.net/v1";
const API_KEY = process.env.C2PA_API_KEY!;

interface VerifyResult {
  validation_state: "Invalid" | "Valid" | "Trusted";
  valid: boolean;
  trusted: boolean;
  manifest: Record<string, unknown> | null;
  signer: string | null;
  timestamp: string | null;
  validation_errors: string[];
  validation_warnings: string[];
}

function buildManifest(title: string) {
  return {
    claim_generator: "MirrorStage/1.0",
    title,
    assertions: [
      {
        label: "c2pa.actions",
        data: { actions: [{
          action: "c2pa.created",
          softwareAgent: { name: "MirrorStage", version: "1.0" },
          digitalSourceType: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
        }] },
      },
      {
        label: "stds.schema-org.CreativeWork",
        data: { "@type": "CreativeWork", author: [{ "@type": "Organization", name: "MirrorStage" }] },
      },
    ],
  };
}

async function signFile(file: Buffer, filename: string, format: string): Promise<Buffer> {
  const form = new FormData();
  form.append("file", new Blob([file]), filename);
  form.append("manifest", JSON.stringify(buildManifest(filename)));
  form.append("format", format);

  const res = await fetch(`${C2PA_URL}/sign`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: form,
  });
  if (!res.ok) throw new Error(`Sign failed: ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

async function verifyFile(file: Buffer): Promise<VerifyResult> {
  const res = await fetch(`${C2PA_URL}/verify`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/octet-stream" },
    body: file,
  });
  if (!res.ok) throw new Error(`Verify failed: ${res.status}`);
  return res.json();
}
import json, os, requests

C2PA_URL = "https://c2pa.mirrorstage.net/v1"
API_KEY = os.environ["C2PA_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def build_manifest(title="Generated Content"):
    return {
        "claim_generator": "MirrorStage/1.0",
        "title": title,
        "assertions": [
            {
                "label": "c2pa.actions",
                "data": {"actions": [{
                    "action": "c2pa.created",
                    "softwareAgent": {"name": "MirrorStage", "version": "1.0"},
                    "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
                }]}
            },
            {
                "label": "stds.schema-org.CreativeWork",
                "data": {"@type": "CreativeWork", "author": [{"@type": "Organization", "name": "MirrorStage"}]}
            }
        ]
    }

def sign_file(file_path: str, fmt: str, title: str = "Generated Content") -> bytes:
    with open(file_path, "rb") as f:
        resp = requests.post(f"{C2PA_URL}/sign", headers=HEADERS,
            files={"file": f}, data={"manifest": json.dumps(build_manifest(title)), "format": fmt})
    resp.raise_for_status()
    return resp.content

def verify_file(file_path: str) -> dict:
    with open(file_path, "rb") as f:
        resp = requests.post(f"{C2PA_URL}/verify",
            headers={**HEADERS, "Content-Type": "application/octet-stream"}, data=f.read())
    resp.raise_for_status()
    return resp.json()  # { validation_state, valid, trusted, manifest, ... }

# Usage
signed = sign_file("photo.jpg", "image/jpeg", "My AI Photo")
open("photo_signed.jpg", "wb").write(signed)
result = verify_file("photo_signed.jpg")
print(f"State: {result['validation_state']}, Trusted: {result['trusted']}")
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

public class C2paClient
{
    private readonly HttpClient _http;
    private const string BaseUrl = "https://c2pa.mirrorstage.net/v1";

    public C2paClient(string apiKey)
    {
        _http = new HttpClient();
        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", apiKey);
    }

    public async Task<byte[]> SignFileAsync(
        string filePath, string format, string title = "Generated Content")
    {
        var manifest = new
        {
            claim_generator = "MirrorStage/1.0",
            title,
            assertions = new object[]
            {
                new {
                    label = "c2pa.actions",
                    data = new { actions = new[] { new {
                        action = "c2pa.created",
                        softwareAgent = new { name = "MirrorStage", version = "1.0" },
                        digitalSourceType = "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
                    } } }
                },
                new {
                    label = "stds.schema-org.CreativeWork",
                    data = (object)new Dictionary<string, object> {
                        ["@type"] = "CreativeWork",
                        ["author"] = new[] { new Dictionary<string, string> {
                            ["@type"] = "Organization", ["name"] = "MirrorStage"
                        } }
                    }
                }
            }
        };

        using var content = new MultipartFormDataContent();
        content.Add(new ByteArrayContent(File.ReadAllBytes(filePath)),
            "file", Path.GetFileName(filePath));
        content.Add(new StringContent(
            JsonSerializer.Serialize(manifest), Encoding.UTF8), "manifest");
        content.Add(new StringContent(format), "format");

        var resp = await _http.PostAsync($"{BaseUrl}/sign", content);
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadAsByteArrayAsync();
    }

    public async Task<VerifyResult> VerifyFileAsync(string filePath)
    {
        var bytes = File.ReadAllBytes(filePath);
        var content = new ByteArrayContent(bytes);
        content.Headers.ContentType =
            new MediaTypeHeaderValue("application/octet-stream");

        var resp = await _http.PostAsync($"{BaseUrl}/verify", content);
        resp.EnsureSuccessStatusCode();
        var json = await resp.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<VerifyResult>(json)!;
    }
}

public record VerifyResult(
    string validation_state,
    bool valid,
    bool trusted,
    JsonElement? manifest,
    string? signer,
    string? timestamp,
    string[] validation_errors,
    string[] validation_warnings
);

// Usage
var client = new C2paClient(Environment.GetEnvironmentVariable("C2PA_API_KEY")!);
var signed = await client.SignFileAsync("photo.jpg", "image/jpeg", "My AI Photo");
File.WriteAllBytes("photo_signed.jpg", signed);
var result = await client.VerifyFileAsync("photo_signed.jpg");
Console.WriteLine($"State: {result.validation_state}, Trusted: {result.trusted}");

Supported Formats

FormatMIME TypeExtension
JPEGimage/jpeg.jpg
PNGimage/png.png
WebPimage/webp.webp
TIFFimage/tiff.tiff
AVIFimage/avif.avif
MP4 Videovideo/mp4.mp4
MP3audio/mpeg.mp3
WAVaudio/wav.wav
FLACaudio/flac.flac
M4Aaudio/mp4.m4a

Integration Checklist

  • Store API key securely (env var / secrets manager, never in code)
  • Sign every AI-generated media before delivering to end users
  • Use correct format MIME type matching actual file
  • Include digitalSourceType: trainedAlgorithmicMedia (EU AI Act)
  • Include both c2pa.actions and CreativeWork assertions
  • Handle 429 (rate limit) with exponential backoff
  • Handle 502 with retry (rare โ€” cold starts handled transparently, first request may take 10-20s)
  • Log X-Request-Id from responses for debugging
  • Verify files after signing as sanity check during development
  • Test signed files at contentcredentials.org/verify

Key Rotation

API keys should be rotated periodically (recommended every 6 months) or immediately if compromised.

1

Create new key

Use the same project_id for audit log continuity. Set expiration and allowed origins.

2

Update your application

Replace the API key in your environment variables or secrets manager. Never hardcode keys.

3

Grace period (48h)

Both old and new keys work simultaneously. Monitor the Audit Log to confirm traffic moves to the new key.

4

Revoke old key

Once confirmed, revoke the old key from the page with a reason.

If a key is compromised: Revoke immediately, create a replacement, and contact [email protected]. Check the Audit Log for unauthorized usage.

Rate Limits

  • 30 requests/minute per IP (configurable per key)
  • Max file size: 100MB default (up to 200MB per key)
  • First request after idle: 10-20s (cold start handled transparently)
  • Use /v1/warmup before bursts for guaranteed low latency

Notes

  • signingCredential.untrusted is expected during development (self-signed cert). Signature integrity is valid.
  • Production certs will be issued by Trufo TCA (C2PA Trust List)
  • Verify signed files at contentcredentials.org/verify

C2PA Compliance & Documentation

This service implements the C2PA Content Credentials Specification v2.1 and is undergoing the C2PA Conformance Program for Generator Products.

Conformance Status

Organization Validation (OV)Approved
Product Validation (PV)Pending
Certificate AuthorityTrufo TCA
Signing AlgorithmPS256 (RSA) โ†’ ES256 (ECDSA P-256)

Operator

CompanyBitForFun SRL
ProductMirrorStage C2PA Gateway
Security Contact[email protected]
InfrastructureCloudflare Workers + Containers

Security & Compliance Documents

Security Architecture

System design, trust boundaries, deployment topology, TLS/encryption

Key Management Plan

Key generation, storage, rotation, destruction, access control

Certificate Management

CA chain, enrollment, renewal, revocation, TSA integration

Threat Model

STRIDE analysis, C2PA-specific threats, risk matrix, mitigations

Incident Response Plan

SEV levels, key compromise procedure, 72h notification SLA

Audit & Compliance

GDPR, EU AI Act, data retention, audit log schema

Full documentation available upon request to [email protected]. Required for C2PA Conformance Program Appendix C submission.

Verification Best Practices

1

Always verify after signing

Call /v1/verify on the signed file to confirm the manifest was embedded correctly.

2

Check validation_state in response

Valid = signature correct (expected with self-signed cert). Trusted = fully trusted chain (after Trufo cert integration).

3

Preserve the signed file intact

Any modification (resize, crop, re-encode) will invalidate the C2PA manifest. Serve the signed file as-is.

4

Report anomalies

If verify returns Invalid on a file you just signed, contact [email protected] immediately.

Release History