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.Workerthrough the realRiskBundleJoblease path. Audit-bundle submissions are leased byAuditBundleBackgroundWorker, 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 explicit501 problem+jsongaps. Legacy/exportsroutes are removed from the success surface and return410 Gone; callers must use the typed/v1/exports/*APIs.
| Surface family | Runtime service outside Testing | Current API behavior |
|---|---|---|
| Verification readback | PostgresExportVerificationArtifactStore | Durable manifest/signature/artifact readback from Postgres; missing persisted rows return 404 or verification errors |
| Export attestation readback/verify | UnsupportedExportAttestationService | 501 problem+json |
| Promotion attestation readback/verify | UnsupportedPromotionAttestationAssembler | 501 problem+json |
| Incident management | PostgresExportIncidentManager | Durable activate/update/resolve/status/readback backed by export_incidents and export_incident_updates |
| Risk bundle jobs | RiskBundleJobHandler + PostgresRiskBundleJobStore + StellaOps.ExportCenter.Worker | Durable PostgreSQL job state/events; API submissions remain pending until the worker leases and materializes them through RiskBundleJob. |
| Simulation export | UnsupportedSimulationReportExporter | 501 problem+json with error_code=exportcenter.simulation_exports.not_implemented |
| Audit bundle generation | AuditBundleJobHandler + PostgresAuditBundleJobStore + AuditBundleBackgroundWorker | Durable 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 generation | ExceptionReportGenerator + PostgresExceptionReportJobStore | Durable create/list/status/download backed by Postgres report job state |
| Timeline publication | PostgresExportNotificationSink | Durable channel/event/payload persistence in export_center.export_timeline_notifications; no in-memory buffering |
Legacy /exports routes | Removed legacy handler | 410 Gone; no fake scheduled/list/deleted responses |
| NIS2 SoA typed profile/run/download | Nis2SoaExportService | Production signed bundle after Authority profile lookup and trust-root verification |
| Assurance export profile discovery/readiness | AssuranceExportProfileRegistry | Tenant-guarded deterministic discovery and fail-closed readiness reason codes |
1. Authentication and headers
- Authorization: Bearer tokens in
Authorization: Bearer <token>paired with DPoP proof. The canonical scope claims are:export.viewerfor read-only profile, run, artifact, and audit-bundle access.export.operatorfor profile create/update, run start/cancel, and audit-bundle creation.export.adminfor profile archival and administrative export controls.
- Tenant context: Provide
X-StellaOps-TenantIdwhen the token carries multiple tenants; defaults to token tenant otherwise. - Idempotency: Mutating endpoints accept
Idempotency-Key(UUID). Retrying with the same key returns the original result. - Rate limits and quotas: Responses include
X-Stella-Quota-Limit,X-Stella-Quota-Remaining, andX-Stella-Quota-Reset. Exceeding quotas returns429 Too Many RequestswithERR_EXPORT_QUOTA. - Integrity headers (downloads):
Digest: sha-256=<base64>,X-Stella-Signature: dsse-b64=<payload>, andX-Stella-Immutability: trueaccompany bundle/manifest downloads; clients must validate before use. - Content negotiation: Requests and responses use
application/json; charset=utf-8unless otherwise stated. Downloads stream binary content with profile-specific media types. - SSE: Event streams set
Content-Type: text/event-streamand keep connections alive with comment heartbeats every 15 seconds.
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"
}
| Code | Description | Typical HTTP status | Notes |
|---|---|---|---|
ERR_EXPORT_001 | Validation failure (selectors, configuration) | 400 | details enumerates offending fields. |
ERR_EXPORT_002 | Profile missing or not accessible for tenant | 404 | Returned on run submission or profile fetch. |
ERR_EXPORT_003 | Concurrency or quota exceeded | 429 | Includes retryAfterSeconds in details. |
ERR_EXPORT_004 | Adapter failure (schema mismatch, upstream outage) | 502 | Worker logs contain adapter error reason. |
ERR_EXPORT_005 | Signing or KMS error | 500 | Run marked failed with errorCode=signing. |
ERR_EXPORT_006 | Distribution failure (HTTP, OCI, object storage) | 502 | details lists failing distribution driver. |
ERR_EXPORT_007 | Run canceled or expired | 409 | Includes cancel author and timestamp. |
ERR_EXPORT_BASE_MISSING | Base manifest for delta exports not found | 400 | Specific to mirror:delta. |
ERR_EXPORT_EMPTY | No records matched selectors (when allowEmpty=false) | 422 | Useful for guard-railled automation. |
ERR_EXPORT_QUOTA | Daily quota exhausted | 429 | Always 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
| Field | Meaning |
|---|---|
profileId | Stable 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. |
frameworkId | nis2 or cra. |
packId | Assurance pack id, for example nis2 or cra.technical-documentation. |
adapterId | Adapter or typed endpoint owner such as cra:tech-file, cra:conformity-dossier, exportcenter.nis2.soa, or exportcenter.nis2.effectiveness-report. |
schemaId | Payload contract id such as nis2-soa-v1, nis2-effectiveness-report-v1, cra-tech-file-v1, or conformity-dossier-v1. |
signingRequired | true when production export success requires a signed artifact. |
supportsOfflineVerification | true when the exported artifact includes verifier metadata and trust-root expectations for offline verification. |
readinessPrerequisites | Stable prerequisite ids from assurance-setup-prerequisites-v1. |
compatibilityRoutes | Existing 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:
| Field | Description |
|---|---|
status | pending, running, success, failed, canceled. |
progress | Object with adapters, bytesWritten, recordsProcessed. |
errorCode | Populated when status=failed (signing, distribution, etc). |
policySnapshotId | Returned for policy-aware profiles. |
distributions | List of available distribution descriptors (type, location, sha256, expiresAt). |
rerunHash | SHA-256 over sorted contents[*].digest; used for determinism checks. |
integrity | Expected HTTP headers (Digest, X-Stella-Signature, X-Stella-Immutability) and OCI annotations (io.stellaops.export.*). |
quotas | Active limits/backpressure settings returned with the run. |
approval | Cross-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:
| Event | Meaning |
|---|---|
run.accepted | Planner accepted job and queued with Orchestrator. |
run.progress | Periodic updates with phase, adapter, counts. |
run.distribution | Distribution driver finished (includes descriptor). |
run.signed | Signing completed successfully. |
run.succeeded | Run marked success. |
run.failed | Run failed; payload includes errorCode. |
run.canceled | Run 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:
Content-Disposition: attachment; filename="export-run-20251029-01.tar.zst"Digest: sha-256=<base64>(EC5)X-Stella-Signature: dsse-b64:<payload>(EC3/EC5)X-Stella-Immutability: trueX-Export-Size: 73482019X-Export-Encryption: age(when mirror encryption enabled)
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.
- Integrity annotations are mirrored in response headers (
Digest,X-Stella-Signature,X-Stella-Immutability) and in the manifestintegrityblock to keep rerun-hash deterministic.
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
- Metrics endpoint:
/metrics(service-local) exposes Prometheus metrics listed in Architecture. - Tracing: When
traceparentheader is provided, worker spans join the calling trace. - Run lookup by trace: Use
GET /api/export/runs?traceId={id}when troubleshooting distributed traces.
9. Related documentation
- Export Center Overview
- Export Center Architecture
- Export Center Profiles
- Export Center CLI Guide (companion document)
- Aggregation-Only Contract reference
Imposed rule: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
