Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Easy2257 Partner Productions API
version: '2026-07-27'
description: 'The Easy2257 Partner API lets your platform outsource 18 U.S.C. § 2257 compliance in a single integration: performer ID verification, per-depiction content logging, and 7-year record retention, with Easy2257 named Custodian of Records. No endpoint in this API returns a performer''s legal name, date of birth, government ID image, or Easy2257 user id. A platform integrating it never touches a government ID, so it cannot leak one. There is no sandbox: only `ez_live_` keys are accepted, and every call writes a production record.'
contact:
name: Easy2257 Developer Support
email: api@easy2257.com
url: https://docs.easy2257.com
servers:
- url: https://easy2257.com
description: Production, and the only environment. There is no sandbox host.
tags:
- name: Productions
description: Query documentation status and verify RS256 compliance certificates for professionally produced content.
paths:
/api/v1/productions:
get:
summary: List productions by performer email
description: 'Every production a given performer is documented in, scoped to productions linked to your partner account. This is the right-to-removal lookup: when a performer asks to be removed, one call gives you every piece of content on your platform they appear in.'
tags:
- Productions
security:
- BearerAuth: []
parameters:
- schema:
type: string
format: email
description: Required. Matched case-insensitively.
example: performer@example.com
required: true
name: performerEmail
in: query
- schema:
type: string
enum:
- pending_documentation
- documented
- revoked
- takedown_pending
- takedown_complete
description: Filter by documentation status. Defaults to `documented`, so pass an explicit value to see anything else.
required: false
name: status
in: query
- schema:
type: string
description: 1 to 200. Default 50.
example: '50'
required: false
name: limit
in: query
- schema:
type: string
description: '`nextCursor` from the previous page.'
required: false
name: cursor
in: query
responses:
'200':
description: Production list. An empty array means no match, which is not an error.
content:
application/json:
schema:
$ref: '#/components/schemas/ProductionListResponse'
'400':
description: '`performerEmail` missing. `code` is `missing_parameter`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No `Authorization: Bearer` header, or a key that is unknown, revoked, or expired. The body carries only `error`, never a `code`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'Rejected before the handler: a test-mode key (`TEST_MODE_UNAVAILABLE`) or a partner account that is not ACTIVE. Never used for cross-partner access, which returns 404.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded: 300 requests per 60 seconds per API key, shared across all `/api/v1` endpoints (requests without a `Bearer` key fall back to 60 per 60 seconds per client IP, and a per-address flood ceiling of 1200 per 60 seconds also applies). Honor `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitError'
'500':
description: Server error. Retry with exponential backoff.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getApiV1Productions
x-operation-id-source: derived
/api/v1/productions/{productionId}:
get:
summary: Get production status
description: 'Current compliance status of one production, plus the custodian identity you can print on a records statement. Scoped to productions linked to your partner account: anything else returns 404.'
tags:
- Productions
security:
- BearerAuth: []
parameters:
- schema:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
required: true
name: productionId
in: path
responses:
'200':
description: Production found.
content:
application/json:
schema:
$ref: '#/components/schemas/ProductionResponse'
'400':
description: 'Invalid request: a required field is missing or malformed.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No `Authorization: Bearer` header, or a key that is unknown, revoked, or expired. The body carries only `error`, never a `code`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'Rejected before the handler: a test-mode key (`TEST_MODE_UNAVAILABLE`) or a partner account that is not ACTIVE. Never used for cross-partner access, which returns 404.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: No such production, or it is not linked to your partner account.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded: 300 requests per 60 seconds per API key, shared across all `/api/v1` endpoints (requests without a `Bearer` key fall back to 60 per 60 seconds per client IP, and a per-address flood ceiling of 1200 per 60 seconds also applies). Honor `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitError'
'500':
description: Server error. Retry with exponential backoff.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getApiV1ProductionsByProductionId
x-operation-id-source: derived
/api/v1/productions/{productionId}/verify-certificate:
post:
summary: Verify a compliance certificate
description: 'Validates a stored JWT compliance certificate against Easy2257''s records: RS256 signature, expiry, revocation, and that the certificate belongs to the production in the URL. Use it for a nightly sweep or a dashboard check. For real-time signals prefer the `production.takedown_requested` webhook and the daily revocation list. For an offline check with no API call at all, verify the JWT against the public JWKS at `https://easy2257.com/.well-known/jwks.json`.'
tags:
- Productions
security:
- BearerAuth: []
parameters:
- schema:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
required: true
name: productionId
in: path
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyCertificateBody'
responses:
'200':
description: 'The check ran. Read `valid`: this status is returned for good and bad certificates alike.'
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyCertificateResponse'
'400':
description: '`certificate` missing (`missing_field`), or the certificate verified but belongs to a different production (`production_id_mismatch`, returned as `{ valid: false, reason }`).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No `Authorization: Bearer` header, or a key that is unknown, revoked, or expired. The body carries only `error`, never a `code`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'Rejected before the handler: a test-mode key (`TEST_MODE_UNAVAILABLE`) or a partner account that is not ACTIVE. Never used for cross-partner access, which returns 404.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded: 300 requests per 60 seconds per API key, shared across all `/api/v1` endpoints (requests without a `Bearer` key fall back to 60 per 60 seconds per client IP, and a per-address flood ceiling of 1200 per 60 seconds also applies). Honor `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitError'
'500':
description: Server error. Retry with exponential backoff.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: postApiV1ProductionsByProductionIdVerifyCertificate
x-operation-id-source: derived
/api/v1/productions/{productionId}/certificates:
get:
summary: List a production's certificates
description: 'Every certificate minted for a linked production, newest first, with lifecycle status and — for certificates issued since storage began — the signed JWT itself. Use it to re-fetch a certificate you missed or lost, to pick up the replacement after a renewal, and to see at a glance which certificate is `CURRENT`. Certificates issued before storage began return `jwt: null`; those arrive with the next annual renewal. Scoped to productions linked to your partner account: anything else returns 404.'
tags:
- Productions
security:
- BearerAuth: []
parameters:
- schema:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
required: true
name: productionId
in: path
responses:
'200':
description: Certificate list, newest first.
content:
application/json:
schema:
$ref: '#/components/schemas/CertificatesResponse'
'400':
description: 'Invalid request: a required field is missing or malformed.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No `Authorization: Bearer` header, or a key that is unknown, revoked, or expired. The body carries only `error`, never a `code`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'Rejected before the handler: a test-mode key (`TEST_MODE_UNAVAILABLE`) or a partner account that is not ACTIVE. Never used for cross-partner access, which returns 404.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: No such production, or it is not linked to your partner account.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded: 300 requests per 60 seconds per API key, shared across all `/api/v1` endpoints (requests without a `Bearer` key fall back to 60 per 60 seconds per client IP, and a per-address flood ceiling of 1200 per 60 seconds also applies). Honor `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitError'
'500':
description: Server error. Retry with exponential backoff.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getApiV1ProductionsByProductionIdCertificates
x-operation-id-source: derived
/api/v1/productions/revoked-certificates:
get:
summary: Certificate revocation list
description: 'Certificates revoked since a timestamp, scoped to productions linked to your partner account. Run it on a daily cron. Every entry is a takedown signal: a reason starting `removal_request:` means take the content down. Annual renewals are not revocations and no longer enter this list (entries recorded before that split carry `type: supersession` and their content stays up). Advance your watermark from the top-level `retrievedAt`, and page with `nextCursor`.'
tags:
- Productions
security:
- BearerAuth: []
parameters:
- schema:
type: string
format: date-time
description: ISO 8601. Defaults to 30 days ago. Use the `retrievedAt` from your last successful sync.
example: '2026-04-01T00:00:00Z'
required: false
name: since
in: query
- schema:
type: string
description: 1 to 10000. Default 1000.
example: '1000'
required: false
name: limit
in: query
- schema:
type: string
description: '`nextCursor` from the previous page, passed back verbatim.'
required: false
name: cursor
in: query
responses:
'200':
description: Revocation list.
content:
application/json:
schema:
$ref: '#/components/schemas/CrlResponse'
'400':
description: '`since` is not a valid date. `code` is `invalid_parameter`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No `Authorization: Bearer` header, or a key that is unknown, revoked, or expired. The body carries only `error`, never a `code`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'Rejected before the handler: a test-mode key (`TEST_MODE_UNAVAILABLE`) or a partner account that is not ACTIVE. Never used for cross-partner access, which returns 404.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded: 300 requests per 60 seconds per API key, shared across all `/api/v1` endpoints (requests without a `Bearer` key fall back to 60 per 60 seconds per client IP, and a per-address flood ceiling of 1200 per 60 seconds also applies). Honor `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitError'
'500':
description: Server error. Retry with exponential backoff.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getApiV1ProductionsRevokedCertificates
x-operation-id-source: derived
components:
schemas:
RateLimitError:
type: object
properties:
error:
type: string
example: Too many requests. Please slow down.
retryAfter:
type: integer
description: Seconds until the window frees up. Mirrors the `Retry-After` header.
example: 14
limit:
type: integer
description: Requests allowed per window.
example: 60
remaining:
type: integer
example: 0
reset:
type: integer
description: Epoch milliseconds at which the sliding window releases the oldest request.
example: 1714000060000
required:
- error
- limit
- remaining
- reset
CertificatesResponse:
type: object
properties:
productionId:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
retrievedAt:
type: string
format: date-time
example: '2026-08-15T18:00:00Z'
certificates:
type: array
items:
type: object
properties:
jwtId:
type: string
description: The certificate's `jti` claim — its stable identifier across this API.
example: 0b4b8f4e-6a7c-4a1e-9f0e-2f1d7c3a5b9e
kid:
type: string
description: Signing key id, resolvable against the public JWKS.
example: ez2257-ad88852c
issuedAt:
type: string
format: date-time
example: '2026-08-15T18:00:00Z'
expiresAt:
type: string
format: date-time
example: '2027-08-15T18:00:00Z'
status:
type: string
enum:
- CURRENT
- SUPERSEDED
- REVOKED
description: Lifecycle state. `CURRENT` is the one live certificate. `SUPERSEDED` means an annual renewal replaced it; the content stays up. `REVOKED` means the certified record was pulled; take the content down.
revokedAt:
type:
- string
- 'null'
format: date-time
description: Set on revoked certificates. Also present on certificates recorded as superseded before supersession was split out of revocation; `status` is the field to branch on.
supersededAt:
type:
- string
- 'null'
format: date-time
description: Set when an annual renewal replaced this certificate. Null on records that predate the supersession split; `status` is the field to branch on.
supersededByJwtId:
type:
- string
- 'null'
description: '`jwtId` of the replacing certificate, when superseded.'
jwt:
type:
- string
- 'null'
description: The signed certificate itself. `null` on certificates issued before certificates were stored; those were delivered once in their webhook and arrive again with the next annual renewal.
required:
- jwtId
- kid
- issuedAt
- expiresAt
- status
- revokedAt
- supersededAt
- supersededByJwtId
- jwt
description: Every certificate minted for the production, newest first.
required:
- productionId
- retrievedAt
- certificates
ProductionListResponse:
type: object
properties:
productions:
type: array
items:
type: object
properties:
id:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
sceneTitle:
type:
- string
- 'null'
description: The production title.
example: April 2026 Scene
status:
type: string
enum:
- pending_documentation
- documented
- revoked
- takedown_pending
- takedown_complete
description: '`pending_documentation`: records not yet complete. `documented`: records on file and a certificate issued. `revoked`: certificate revoked by an actioned takedown. `takedown_pending`: removal request received, under review. `takedown_complete`: content removed, records sealed.'
documentedAt:
type:
- string
- 'null'
format: date-time
required:
- id
- sceneTitle
- status
- documentedAt
nextCursor:
type:
- string
- 'null'
description: Pass back verbatim as `cursor`. null on the last page.
example: cmd3p8w1k0002qz8h9j4r7v5s
required:
- productions
- nextCursor
VerifyCertificateBody:
type: object
properties:
certificate:
type: string
description: The RS256 JWT compliance certificate Easy2257 issued, as delivered in the `production.documented` webhook.
example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImV6MjI1Ny1hZDg4ODUyYyJ9...
required:
- certificate
CrlResponse:
type: object
properties:
retrievedAt:
type: string
format: date-time
description: 'Server time at which this response was built. This is the watermark: pass it as `since` on your next sync. There is no top-level `revokedAt`, and the per-item `revokedAt` values belong to individual certificates, so advancing a watermark off one of those will make your sync stall or skip.'
example: '2026-04-24T18:00:00Z'
revocations:
type: array
items:
type: object
properties:
productionId:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
revokedAt:
type: string
format: date-time
description: When this certificate was revoked.
example: '2026-04-24T18:00:00Z'
reason:
type: string
description: Free-form string, not a fixed enum. `removal_request:<id>` means an admin actioned a removal request, so pull the content. Match that one on the `removal_request:` prefix, since the id varies. `unspecified` means no reason was recorded. `annual_renewal` appears only on entries recorded before renewals were split out of this feed; the content stays up for those.
example: removal_request:cmremreq0000000000000001
type:
type: string
enum:
- revocation
- supersession
description: 'What the entry is. `revocation` means the certified record was pulled: take the content down. `supersession` means an annual renewal replaced the certificate and the content stays up; it appears only on entries recorded before renewals stopped being written to this feed. New renewals never enter the list — they arrive as `production.certificate_renewed` webhooks and in `GET /api/v1/productions/{productionId}/certificates`.'
example: revocation
required:
- productionId
- revokedAt
- reason
- type
nextCursor:
type:
- string
- 'null'
description: Opaque page cursor derived from the certificate record's `jwtId`. Pass it back verbatim as `cursor`. It is not a `productionId`, and ordering is stable on (revokedAt, jwtId) so pages never skip or repeat. null on the last page.
example: 0b4b8f4e-6a7c-4a1e-9f0e-2f1d7c3a5b9e
required:
- retrievedAt
- revocations
- nextCursor
ProductionResponse:
type: object
properties:
id:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
status:
type: string
enum:
- pending_documentation
- documented
- revoked
- takedown_pending
- takedown_complete
description: '`pending_documentation`: records not yet complete. `documented`: records on file and a certificate issued. `revoked`: certificate revoked by an actioned takedown. `takedown_pending`: removal request received, under review. `takedown_complete`: content removed, records sealed.'
performerCount:
type: integer
description: Number of talent participants across the production's scenes.
example: 3
documentedAt:
type:
- string
- 'null'
format: date-time
example: '2026-04-24T18:00:00Z'
custodianName:
type: string
description: Custodian of Records legal entity, exactly as published on the Easy2257 records statement. Render it, do not parse it.
custodianAddress:
type: string
description: Custodian mailing address on one line. Empty string when no address is on file.
takedownContact:
type: string
format: email
example: takedown@easy2257.com
lastVerifiedAt:
type: string
format: date-time
description: Timestamp of this lookup, not of a stored verification event. Useful as an audit breadcrumb for when you last checked.
required:
- id
- status
- performerCount
- documentedAt
- custodianName
- custodianAddress
- takedownContact
- lastVerifiedAt
Error:
type: object
properties:
error:
type: string
description: 'Human-readable message. Switch on HTTP status and on `code`, never on this string: it is not part of the API contract.'
example: email, externalUserId, and callbackUrl are required
code:
type: string
description: Machine-readable error code. Present on the test-key rejection (`TEST_MODE_UNAVAILABLE`), consent conflicts, and most collab-scene and upload-session validation errors. Never present on `401` authentication rejections, and absent on the inactive-partner `403` and on some older solo-account validation errors, which is why status is the primary signal.
example: missing_parameter
field:
type: string
description: The offending request field. Present on collab-scene and upload-session validation errors.
example: contentType
required:
- error
VerifyCertificateResponse:
type: object
properties:
valid:
type: boolean
description: The only field you should branch on. A 200 response says the check ran, not that the certificate is good.
productionId:
type: string
example: cmd3p8w1k0002qz8h9j4r7v5s
issuedAt:
type: string
format: date-time
description: Present when valid is true.
expiresAt:
type: string
format: date-time
description: Present when valid is true.
currentStatus:
type: string
description: Current production documentation status. Present when valid is true. `unknown` if the production row has since been removed.
example: documented
reason:
type: string
enum:
- certificate_revoked
- certificate_superseded
- certificate_expired
- certificate_not_found
- signature_mismatch
- invalid_certificate
- no_public_key
- public_key_not_configured
- production_id_mismatch
description: 'Machine-readable failure reason, present when valid is false. `certificate_superseded` means a newer certificate replaced this one, either at annual renewal or when the producer reissued the sealed package after a correction: swap in the replacement (from the `production.certificate_renewed` webhook, the newer `production.documented` delivery, or `GET /api/v1/productions/{productionId}/certificates`); the content stays up. `certificate_revoked` means the certified record was pulled: take the content down. `production_id_mismatch` arrives with HTTP `400`: the certificate verified, but it belongs to a different production than the one in the URL. `no_public_key` and `public_key_not_configured` are Easy2257-side signing key problems, so retry rather than treating the certificate as bad.'
required:
- valid
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: 'Partner API key from partner.easy2257.com/api-keys. Only `ez_live_` keys are accepted. A key beginning with `ez_test_` is rejected with `403` and code `TEST_MODE_UNAVAILABLE` before any handler runs: there is no sandbox, and every handler writes production records, provisions real users, and sends real email. New partner accounts start with a test key, so create a live key before your first call.'