Export Center REST API

Audience: Platform integrators, Console/CLI developers, and automation engineers orchestrating export runs. Base routes: /v1/exports/* for generic export profiles/runs and /v1/audit-bundles/* for StellaBundle audit exports behind the StellaOps gateway; requires Authority-issued tokens with export scopes.

This reference describes the Export Center API introduced in Export Center Phase 1 (Epic 10) and extended in Phase 2. Use it alongside the Export Center Architecture and Profiles guides for service-level semantics.

Status: The current ExportCenter host uses PostgreSQL-backed canonical export profile/run/artifact repositories, verification artifact readback, exception report job/content state, incident management, timeline notification persistence, risk-bundle job state, and audit-bundle job state in non-testing runtime. Risk-bundle submissions are materialized by StellaOps.ExportCenter.Worker through the real RiskBundleJob lease path. Audit-bundle submissions are leased by AuditBundleBackgroundWorker, with completed ZIP/index payloads stored through the configured content-addressed payload store and continuity recorded in EvidenceLocker. Export/promotion attestation readback and verify plus simulation export remain explicit 501 problem+json gaps. Legacy /exports routes are removed from the success surface and return 410 Gone; callers must use the typed /v1/exports/* APIs.

Surface familyRuntime service outside TestingCurrent API behavior
Verification readbackPostgresExportVerificationArtifactStoreDurable manifest/signature/artifact readback from Postgres; missing persisted rows return 404 or verification errors
Export attestation readback/verifyUnsupportedExportAttestationService501 problem+json
Promotion attestation readback/verifyUnsupportedPromotionAttestationAssembler501 problem+json
Incident managementPostgresExportIncidentManagerDurable activate/update/resolve/status/readback backed by export_incidents and export_incident_updates
Risk bundle jobsRiskBundleJobHandler + PostgresRiskBundleJobStore + StellaOps.ExportCenter.WorkerDurable PostgreSQL job state/events; API submissions remain pending until the worker leases and materializes them through RiskBundleJob.
Simulation exportUnsupportedSimulationReportExporter501 problem+json with error_code=exportcenter.simulation_exports.not_implemented
Audit bundle generationAuditBundleJobHandler + PostgresAuditBundleJobStore + AuditBundleBackgroundWorkerDurable create/list/status/download/index lifecycle; production processing fails closed when the configured payload store is unavailable and never substitutes placeholder bundle bytes
Exception report generationExceptionReportGenerator + PostgresExceptionReportJobStoreDurable create/list/status/download backed by Postgres report job state
Timeline publicationPostgresExportNotificationSinkDurable channel/event/payload persistence in export_center.export_timeline_notifications; no in-memory buffering
Legacy /exports routesRemoved legacy handler410 Gone; no fake scheduled/list/deleted responses
NIS2 SoA typed profile/run/downloadNis2SoaExportServiceProduction signed bundle after Authority profile lookup and trust-root verification
Assurance export profile discovery/readinessAssuranceExportProfileRegistryTenant-guarded deterministic discovery and fail-closed readiness reason codes

1. Authentication and headers

2. Error model

Errors follow standard HTTP codes with structured payloads:

{
  "code": "ERR_EXPORT_002",
  "message": "Profile not found for tenant acme",
  "details": [],
  "traceId": "01J9N4Y4K2XY8C5V7T2S",
  "timestamp": "2025-10-29T13:42:11Z"
}
CodeDescriptionTypical HTTP statusNotes
ERR_EXPORT_001Validation failure (selectors, configuration)400details enumerates offending fields.
ERR_EXPORT_002Profile missing or not accessible for tenant404Returned on run submission or profile fetch.
ERR_EXPORT_003Concurrency or quota exceeded429Includes retryAfterSeconds in details.
ERR_EXPORT_004Adapter failure (schema mismatch, upstream outage)502Worker logs contain adapter error reason.
ERR_EXPORT_005Signing or KMS error500Run marked failed with errorCode=signing.
ERR_EXPORT_006Distribution failure (HTTP, OCI, object storage)502details lists failing distribution driver.
ERR_EXPORT_007Run canceled or expired409Includes cancel author and timestamp.
ERR_EXPORT_BASE_MISSINGBase manifest for delta exports not found400Specific to mirror:delta.
ERR_EXPORT_EMPTYNo records matched selectors (when allowEmpty=false)422Useful for guard-railled automation.
ERR_EXPORT_QUOTADaily quota exhausted429Always paired with quota headers.

All responses include traceId for correlation with logs and metrics.

3. Profiles endpoints

3.1 List profiles

GET /api/export/profiles?kind=json&variant=raw&page=1&pageSize=20
Scopes: export:read

Returns tenant-scoped profiles. Response headers: X-Total-Count, Link for pagination.

Response

{
  "items": [
    {
      "profileId": "prof-json-raw",
      "name": "Daily JSON Raw",
      "kind": "json",
      "variant": "raw",
      "distribution": ["http", "object"],
      "retention": {"mode": "days", "value": 14},
      "createdAt": "2025-10-23T08:00:00Z",
      "createdBy": "user:ops"
    }
  ],
  "page": 1,
  "pageSize": 20
}

3.2 Get a profile

GET /api/export/profiles/{profileId}
Scopes: export:read

Returns full configuration, including config payload, distribution options, and metadata.

3.3 Create a profile

POST /api/export/profiles
Scopes: export:profile:manage

Request

{
  "profileId": "prof-airgap-mirror",
  "name": "Airgap Mirror Weekly",
  "kind": "mirror",
  "variant": "full",
  "include": ["advisories", "vex", "sboms", "policy"],
  "distribution": ["http", "object"],
  "encryption": {
    "enabled": true,
    "recipientKeys": ["age1tenantkey..."],
    "strict": false
  },
  "retention": {"mode": "days", "value": 30},
  "limits": {
    "maxActiveRuns": 4,
    "maxQueuedRuns": 50,
    "backpressureMode": "reject"
  },
  "approval": {
    "required": false
  }
}

Response 201

{
  "profileId": "prof-airgap-mirror",
  "version": 1,
  "createdAt": "2025-10-29T12:05:22Z",
  "createdBy": "user:ops",
  "status": "active"
}

3.4 Update profile metadata

PATCH /api/export/profiles/{profileId}
Scopes: export:profile:manage

Allows renaming, toggling distribution switches, or updating retention. Structural configuration updates (kind/variant/include) create a new revision; the API returns revisionCreated=true and the new profileId (e.g., prof-airgap-mirror@2).

3.5 Archive profile

POST /api/export/profiles/{profileId}:archive
Scopes: export:profile:manage

Marks profile as inactive; existing runs remain accessible. Use :restore to reactivate.

4. Run management

4.1 Submit an export run

POST /api/export/runs
Scopes: export:run

Request

{
  "profileId": "prof-json-raw",
  "selectors": {
    "tenants": ["acme"],
    "timeWindow": {
      "from": "2025-10-01T00:00:00Z",
      "to": "2025-10-29T00:00:00Z"
    },
    "products": ["registry.example.com/app:*"],
    "sboms": ["sbom:S-1001", "sbom:S-2004"]
  },
  "policySnapshotId": "policy-snap-42",
  "options": {
    "allowEmpty": false,
    "priority": "standard"
  }
}

4.1a Submit a NIS2 Statement of Applicability run

The Console uses the typed NIS2 SoA surface rather than the generic profile-run surface because the run must bind a Policy control-register snapshot, Authority tenant compliance profile, production signing key, and tenant export trust roots before returning a bundle URL.

GET /v1/exports/nis2/soa/profile
Scopes: export:read

POST /v1/exports/nis2/soa/runs
Scopes: export:run

Request

{
  "tenantId": "tenant-a",
  "controlRegisterSnapshot": {
    "schemaVersion": "nis2-control-register-v1",
    "tenantId": "tenant-a",
    "generatedAt": "2026-04-30T12:00:00Z",
    "controls": []
  },
  "since": "2026-04-01",
  "evidenceLockerBundleRef": "evidence-locker://bundles/sha256:...",
  "redactInternalOnly": false,
  "correlationId": "console-nis2-soa-202604"
}

Response 200

{
  "jobId": "c991d88d-94e1-5b11-8f0d-5981493925b3",
  "runId": "c991d88d-94e1-5b11-8f0d-5981493925b3",
  "profileId": "nis2:statement-of-applicability",
  "tenantId": "tenant-a",
  "status": "completed",
  "progress": {
    "totalItems": 13,
    "processedItems": 13,
    "failedItems": 0,
    "totalSizeBytes": 2048,
    "percentComplete": 100
  },
  "signedBundleUrl": "/v1/exports/nis2/soa/runs/c991d88d-94e1-5b11-8f0d-5981493925b3/bundle",
  "exportUrl": "/v1/exports/nis2/soa/runs/c991d88d-94e1-5b11-8f0d-5981493925b3/bundle",
  "bundleHash": "sha256:...",
  "soaExportHash": "sha256:...",
  "controlRegisterSnapshotHash": "sha256:...",
  "evidenceLockerBundleRef": "evidence-locker://bundles/sha256:...",
  "verification": {
    "isValid": true,
    "payloadTypeId": "nis2-soa-v1",
    "schemaPinId": "nis2-soa-v1",
    "verifiedKeyId": "tenant-export-key",
    "verifiedTrustRootId": "tenant-export-root-2026",
    "errors": []
  }
}

GET /v1/exports/nis2/soa/runs/{runId} returns the persisted run status and GET /v1/exports/nis2/soa/runs/{runId}/bundle streams the signed bundle as application/vnd.stellaops.nis2.soa.bundle.v1+json. Tenant mismatches fail before signing or Authority profile lookup. Missing Authority profile, signing key, storage root, or tenant export trust roots fail closed.

4.1b Discover Assurance export profiles

The Assurance registry is the generic discovery/readiness surface for optional NIS2 and CRA export modules. DORA profile ids are documented as route-owned targets, but they are not current ExportCenter built-ins until the owning Notify/Findings/EvidenceLocker readiness contracts are wired. The registry does not start export runs and does not replace adapter-specific or legacy typed routes.

GET /v1/exports/assurance/profiles
GET /v1/exports/assurance/profiles?frameworkId=cra
GET /v1/exports/assurance/profiles/{profileId}
GET /v1/exports/assurance/profiles/{profileId}/readiness
Scopes: export:read

All routes use the same tenant guard behavior as the typed NIS2 SoA route. Missing tenant context returns 400 tenant_not_found; unknown profiles return 404 assurance_export_profile_not_found.

Profile response fields

FieldMeaning
profileIdStable generic profile id, for example nis2.statement-of-applicability or cra.technical-file. The legacy typed NIS2 SoA endpoint may still return nis2:statement-of-applicability.
frameworkIdnis2 or cra.
packIdAssurance pack id, for example nis2 or cra.technical-documentation.
adapterIdAdapter or typed endpoint owner such as cra:tech-file, cra:conformity-dossier, exportcenter.nis2.soa, or exportcenter.nis2.effectiveness-report.
schemaIdPayload contract id such as nis2-soa-v1, nis2-effectiveness-report-v1, cra-tech-file-v1, or conformity-dossier-v1.
signingRequiredtrue when production export success requires a signed artifact.
supportsOfflineVerificationtrue when the exported artifact includes verifier metadata and trust-root expectations for offline verification.
readinessPrerequisitesStable prerequisite ids from assurance-setup-prerequisites-v1.
compatibilityRoutesExisting typed API or CLI routes that continue to work.

Readiness response

{
  "schemaVersion": "assurance-evidence-export-v1",
  "profileId": "cra.technical-file",
  "tenantId": "tenant-a",
  "frameworkId": "cra",
  "packId": "cra.technical-documentation",
  "exportType": "technical-file",
  "state": "blocked",
  "signedExportReady": false,
  "livePublicationReady": false,
  "reasonCodes": [
    "signing-key-missing",
    "storage-root-missing",
    "tenant-export-trust-roots-missing"
  ],
  "secretValuesExposed": false
}

Readiness is fail-closed. NIS2 SoA reports authority-profile-url-missing, storage-root-missing, signing-key-missing, signing-provider-missing, and tenant-export-trust-roots-missing. CRA signed exports report storage-root-missing, signing-key-missing, signing-provider-missing, and tenant-export-trust-roots-missing; CRA live-publication preflight is reported separately through security-mailbox-unverified, intake-key-expired-or-missing, rotation-roles-missing, and support-lifecycle-metadata-missing. A CRA profile can be state=export-ready while livePublicationReady=false.

Response 202

{
  "runId": "run-20251029-01",
  "status": "pending",
  "profileId": "prof-json-raw",
  "createdAt": "2025-10-29T12:12:11Z",
  "createdBy": "user:ops",
  "selectors": { "...": "..." },
  "links": {
    "self": "/api/export/runs/run-20251029-01",
    "events": "/api/export/runs/run-20251029-01/events"
  },
  "quotas": {
    "maxActiveRuns": 4,
    "maxQueuedRuns": 50,
    "backpressureMode": "reject"
  },
  "approval": {
    "required": false
  }
}

4.2 List runs

GET /api/export/runs?status=active&profileId=prof-json-raw&page=1&pageSize=10
Scopes: export:read

Returns latest runs with pagination. Each item includes summary counts, duration, and last event.

4.3 Get run status

GET /api/export/runs/{runId}
Scopes: export:read

Response fields:

FieldDescription
statuspending, running, success, failed, canceled.
progressObject with adapters, bytesWritten, recordsProcessed.
errorCodePopulated when status=failed (signing, distribution, etc).
policySnapshotIdReturned for policy-aware profiles.
distributionsList of available distribution descriptors (type, location, sha256, expiresAt).
rerunHashSHA-256 over sorted contents[*].digest; used for determinism checks.
integrityExpected HTTP headers (Digest, X-Stella-Signature, X-Stella-Immutability) and OCI annotations (io.stellaops.export.*).
quotasActive limits/backpressure settings returned with the run.
approvalCross-tenant approval ticket when selectors span multiple tenants/wildcards.

4.4 Cancel a run

POST /api/export/runs/{runId}:cancel
Scopes: export:run

Body optional ({"reason": "Aborted due to incident INC-123"}). Returns 202 and pushes run.canceled event.

5. Events and telemetry

5.1 Server-sent events

GET /api/export/runs/{runId}/events
Scopes: export:read
Accept: text/event-stream

Event payload example:

event: run.progress
data: {"runId":"run-20251029-01","phase":"adapter","adapter":"json","records":1024,"bytes":7340032,"timestamp":"2025-10-29T12:13:15Z"}

Event types:

EventMeaning
run.acceptedPlanner accepted job and queued with Orchestrator.
run.progressPeriodic updates with phase, adapter, counts.
run.distributionDistribution driver finished (includes descriptor).
run.signedSigning completed successfully.
run.succeededRun marked success.
run.failedRun failed; payload includes errorCode.
run.canceledRun canceled; includes canceledBy.

SSE heartbeats (: ping) keep long-lived connections alive and should be ignored by clients.

5.2 Audit events

GET /api/export/runs/{runId}/events?format=audit returns the same event stream in newline-delimited JSON for offline ingestion.

6. Download endpoints

6.1 Bundle download

GET /api/export/runs/{runId}/download
Scopes: export:download

Streams the primary bundle (tarball, zip, or profile-specific layout). Headers:

Supports HTTP range requests for resume functionality. If no bundle exists yet, responds 409 with ERR_EXPORT_007.

6.2 Manifest download

GET /api/export/runs/{runId}/manifest
Scopes: export:download

Returns signed export.json. To fetch the detached signature, append ?signature=true.

6.3 Provenance download

GET /api/export/runs/{runId}/provenance
Scopes: export:download

Returns signed provenance.json. Supports ?signature=true. Provenance includes attestation subject digests, policy snapshot ids, adapter versions, and KMS key identifiers.

6.4 Distribution descriptors

GET /api/export/runs/{runId}/distributions
Scopes: export:read

Lists all registered distribution targets (HTTP, OCI, object storage). Each item includes type, location, sha256, sizeBytes, and expiresAt.

7. Webhook hand-off

Exports can notify external systems once a run succeeds by registering an HTTP webhook:

POST /api/export/webhooks
Scopes: export:profile:manage

Payload includes targetUrl, events (e.g., run.succeeded), and optional secret for HMAC signatures. Webhook deliveries sign payloads with X-Stella-Signature header (sha256=...). Retries follow exponential backoff with dead-letter capture in export_events.

8. Observability

Imposed rule: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.