Reference
Schema (SDL)
The whole API as GraphQL SDL. The descriptions inside are the implementation's own notes; the resource pages are the customer-facing documentation.
Download it from /schema.graphql — a stable URL, so a
build step can fetch it directly:
curl -o schema.graphql https://docs.schema.fm/schema.graphqlUse it to generate typed clients (graphql-codegen, gql.tada,
graphql-client for Rust, Apollo iOS/Kotlin) or to validate your queries in
CI before they ever reach the API. It is generated from the server's code,
and the server's test suite fails if this copy drifts from it.
input AddOrgMemberInput {
"""
Their email address, which is also their login. Stored verbatim;
uniqueness is case-insensitive, so `Alice@` and `alice@` are one person.
"""
email: String!
"""
How to show them in a list. Optional — an address alone is a usable
account, and guessing a name from the local part gets it wrong.
"""
displayName: String
"""
`owner`, `admin` or `member`. Defaults to **member**, which is the least
privileged of the three: an unstated role should never be the powerful
one.
"""
role: String
}
input AddResourceInput {
releasePk: Int!
isrc: String
"""
DDEX type — `MusicalWorkSoundRecording` for a track,
`FrontCoverImage` for artwork. Cover art is a resource like any other;
nothing about this path treats it specially.
"""
resourceType: String!
"""
The AVS kind to intern `resourceType` under. Defaults to
`SoundRecordingType`; artwork needs `ImageType`.
"""
resourceTypeKind: String! = "SoundRecordingType"
title: String
durationSeconds: Int
"""
Track number. Appended after the current last when omitted.
"""
sequence: Int
}
"""
A member just added, and the one and only appearance of their password.
Same contract as an API key and a delivery account: shown once, stored as an
argon2id hash, never recoverable.
"""
type AddedMember {
userPk: Int!
email: String!
role: String!
"""
`null` when the address already belonged to a //SCHEMA user. They joined
with the credential they already had, which this organization neither
sees nor replaces.
"""
password: String
"""
False when an existing user joined rather than a new account being made.
Worth showing: "we made them an account" and "we gave an existing
account access to yours" are different sentences.
"""
created: Boolean!
}
"""
A fault in this catalog that only the customer can fix.
"""
type Alert {
alertPk: Int!
kind: AlertKind!
severity: AlertSeverity!
"""
One line. Quotes sender-supplied text (filenames): render it as text.
"""
title: String!
"""
What //SCHEMA saw, why it matters, and what to do. Quotes
sender-supplied text: render it as text.
"""
description: String!
"""
The thing that most likely needs correcting.
"""
fix: AlertTarget!
"""
Everything else worth a link.
"""
related: [AlertTarget!]!
"""
When it was first raised.
"""
raisedAt: DateTime!
"""
When the condition was last observed. One alert per condition: seeing
it again moves this and `occurrences`, and adds no alert.
"""
lastSeenAt: DateTime!
occurrences: Int!
"""
Whether the signed-in person has read it. Always `false` for an API key.
"""
read: Boolean!
"""
Whether `dismissAlert` may close it. A blocking alert is closed by its
fix, except where dismissing is itself a decision //SCHEMA can act on.
"""
dismissable: Boolean!
resolvedAt: DateTime
resolution: AlertResolution
"""
The note given when it was dismissed.
"""
dismissNote: String
"""
The delivery that raised it, when there was one.
"""
delivery: Message
}
"""
One page of alerts.
"""
type AlertConnection {
nodes: [Alert!]!
"""
Pass as `after` for the next page. `null` on the last page.
"""
endCursor: String
hasNextPage: Boolean!
}
"""
The inbox's headline numbers. Open alerts only.
"""
type AlertCounts {
open: Int!
"""
Open and blocking: the console's nav badge.
"""
blocking: Int!
"""
Open and not read by the signed-in person. Equal to `open` for an API
key, which has no read state.
"""
unread: Int!
}
"""
What kind of fault.
"""
enum AlertKind {
"""
A delivered file was refused at intake — today, a lossy audio master.
"""
FILE_REJECTED
"""
A file's bytes contradict the hash its delivery declared.
"""
HASH_MISMATCH
"""
A console delivery could not tell which waiting file its message names.
"""
AMBIGUOUS_CLAIM
}
"""
How an alert closed.
"""
enum AlertResolution {
"""
The condition cleared.
"""
FIXED
"""
Someone decided it needs no fix.
"""
DISMISSED
"""
Its subject was deleted.
"""
SUPERSEDED
}
"""
Whether something is held because of it.
"""
enum AlertSeverity {
"""
Something is held (a release is `pending`) until it is fixed.
"""
BLOCKING
"""
Nothing is held, but the catalog is wrong or will degrade.
"""
WARNING
}
"""
Which alerts to list.
"""
enum AlertStatus {
"""
Not resolved: blocking first, then most recently seen.
"""
OPEN
"""
Fixed, dismissed or superseded: most recently resolved first.
"""
RESOLVED
}
"""
Something an alert links to.
"""
type AlertTarget {
kind: AlertTargetKind!
releasePk: Int
resourcePk: Int
messagePk: Int
"""
A name for the link, e.g. a track's title and ISRC. Sender-supplied
text: render it as text.
"""
label: String!
}
"""
What an `AlertTarget` points at.
"""
enum AlertTargetKind {
"""
A release: `releasePk`.
"""
RELEASE
"""
A track on a release: `releasePk` and `resourcePk`.
"""
TRACK
"""
A delivered message: `messagePk`.
"""
DELIVERY
"""
The catalog's deliveries, for a fault with no subject yet.
"""
DELIVERIES
}
"""
An API key as the console lists it. Carries no secret and cannot — the
plaintext existed once, when it was minted.
"""
type ApiKey {
apiKeyPk: Int!
catalogPk: Int!
name: String!
"""
The leading, non-secret segment. Enough to tell two keys apart, far too
little to help anyone holding it.
"""
tokenPrefix: String!
scopes: [String!]!
createdAt: DateTime!
expiresAt: DateTime
"""
Best-effort and lagging: the auth path deliberately does not write on
every request, so this answers "is this key dormant" rather than "when
exactly was it last used".
"""
lastUsedAt: DateTime
revokedAt: DateTime
}
"""
One cover at one of your organization's sizes.
"""
type Artwork {
"""
The label asked for.
"""
size: String!
"""
What is rendered for it.
"""
spec: String!
status: EncodingStatus!
"""
The rendition's dimensions once `READY`; before that, what they will be
(`null` until the cover has been measured).
"""
width: Int
height: Int
"""
`jpeg` or `png`.
"""
format: String!
contentType: String!
"""
`null` until `READY`.
"""
byteSize: Int
"""
A short-lived signed URL for the rendition; `null` unless `READY`.
Minted, and logged, only when selected.
"""
url(
"""
Lifetime in seconds; capped by the store.
"""
ttlSeconds: Int
): String
}
"""
How the cover is fitted to a size's bounds.
"""
enum ArtworkCrop {
"""
Exactly the size: scaled to cover it, center-cropped.
"""
FILL
"""
Within the size, aspect kept, nothing cropped.
"""
FIT
}
"""
What a rendition is written as.
"""
enum ArtworkFormat {
JPEG
PNG
}
"""
One of the organization's sizes.
"""
type ArtworkSize {
artworkSizePk: Int!
"""
What `artwork(size:)` takes.
"""
label: String!
width: Int!
height: Int!
crop: ArtworkCrop!
format: ArtworkFormat!
"""
JPEG quality, 1–100. `null` for PNG.
"""
quality: Int
"""
Whether a smaller cover is scaled up to the size.
"""
upscale: Boolean!
"""
What is rendered: `img-fill-600x600-jpeg-q85-v1`. Sizes with the same
spec share renditions, in this organization or any other.
"""
spec: String!
"""
When this size started rendering its current spec: when it was created,
or when its dimensions, crop, format or quality last changed (which
gives it a new `artworkSizePk`). A rename keeps it.
"""
configuredFrom: DateTime!
updatedAt: DateTime!
}
"""
The bounds a size must fall within — asked rather than hardcoded, so the
console's form offers exactly what will be accepted.
"""
type ArtworkSizeLimits {
minPx: Int!
maxPx: Int!
maxSizes: Int!
maxLabelLength: Int!
defaultJpegQuality: Int!
}
"""
Where invoices are sent. Read with `BillingRead`.
"""
type BillingContact {
legalName: String!
billingEmail: String!
postalAddress: String
currency: String!
paymentTermsDays: Int!
"""
Whether a payment method can be managed yet. False means onboarding is
incomplete on //SCHEMA's side, not that the customer did anything wrong.
"""
paymentReady: Boolean!
}
"""
One charge, as it will appear on the invoice.
"""
type BillingLine {
"""
`platform_fee`, `storage_overage`, `ingest`, `migration`, `credit`,
`adjustment`.
"""
kind: String!
description: String!
"""
Scaled by 10^6 — `1.5` is `1500000`. Integers all the way to the screen,
because a float here is a rounding disagreement with the invoice.
"""
quantityMicros: Int!
unit: String!
unitPriceMicros: Int!
amountCents: Int!
}
"""
Spend so far this period, and what it is heading for.
"""
type BillingPeriod {
periodStart: String!
periodEnd: String!
currency: String!
"""
Accrued so far — the days that have actually happened.
"""
toDateCents: Int!
"""
What the period is heading for if it continues like this.
"""
projectedCents: Int!
"""
A plausible band around the projection, widened where delivery volume
has been uneven. Equal on both sides once the period is complete.
"""
projectedLowCents: Int!
projectedHighCents: Int!
"""
One sentence naming the method, so the number is never presented bare.
"""
confidence: String!
"""
True once every day has been observed. **Still not an invoice** — that
comes from a frozen rollup.
"""
isFinal: Boolean!
"""
When today's share was measured. `toDateCents` is as of this instant.
"""
asOf: DateTime!
"""
The projected charges, broken out. Matches the invoice's own line kinds
exactly: if a line will appear on the invoice it appears here, and
nowhere else does a charge get introduced.
"""
lines: [BillingLine!]!
}
"""
A delivered release that cannot go live yet, and why.
"""
type BlockedRelease {
releasePk: Int!
upc: String
title: String
updatedAt: DateTime!
"""
Everything in the tracklist, artwork included.
"""
resources: Int!
"""
Still waiting for their bytes.
"""
unbacked: Int!
"""
Front covers backed by bytes. Zero blocks release.
"""
covers: Int!
"""
What is blocking it, in sentences. Assembled here rather than in SQL so
the counts stay comparable and the wording stays changeable.
"""
problems: [String!]!
}
type Catalog {
catalogPk: Int!
slug: String!
displayName: String!
orgPk: Int!
orgSlug: String!
orgDisplayName: String!
"""
This organization's own SFTP hostname — `bandcamp.ftp.schema.fm`.
`null` for an org that only ingests through the API.
Per-DSP as of `0006`. It was previously one service-level string shared
by every deployment, which stopped being true the moment more than one
DSP could buy the software.
"""
sftpHost: String
"""
The port that host listens on — 22 in production, 2222 in the dev
compose stack. Installation config, not per-DSP, so it is not stored
beside the hostname.
"""
sftpPort: Int!
"""
Where this organization's SFTP endpoint stands: `ready`, `provisioning`
(requested, not reachable yet — a new org's first few minutes),
`delayed` (an attempt failed and is being retried), `off` (it existed
and has been turned off — a canceled trial, a suspension), or `none`.
`sftpHost` stays `null` until `ready`, so without this a console cannot
tell "being set up" from "your plan has no endpoint". `failed` is
reported as `delayed` and `last_error` is not exposed: the cause is
infrastructure the customer cannot act on, and it retries on its own.
"""
sftpEndpointStatus: String!
}
"""
A copyright notice — the © line.
The **song**, not the recording: ℗ covers the master, © covers the
composition, and a label routinely holds one and not the other. Kept as its
own type rather than a `kind` on [`PLine`] so a client cannot conflate them
by forgetting a filter (`0009`).
Qualified by language, script and territory exactly as a [`PLine`] is.
"""
type Cline {
"""
`CLineText`, e.g. `(C) 1994 EMI Music Japan Inc.`
"""
value: String!
"""
`Year`, verbatim as delivered. A **string** for the same reason
[`PLine::year`] is.
"""
year: String
"""
`CLineCompany`. Usually absent — most senders put the company only
inside `value`.
"""
company: String
language: String
script: String
territory: String
isDefault: Boolean!
}
"""
Where a clip sits in the recording.
"""
type Clip {
startMs: Int!
durationMs: Int!
}
"""
One ℗ or © notice, for `plines` or `clines`.
One input type serves both because the fields are identical; which right
it states is decided by the list it is in, and the two are stored apart.
"""
input CopyrightLineInput {
"""
The notice as it should be displayed, e.g. `(P) 2026 Example Records`.
Stored verbatim.
"""
text: String!
"""
The year, as text. Not validated as a number, matching what a
delivered notice may carry.
"""
year: String
"""
The rights-holder, when it is not only inside `text`.
"""
company: String
"""
BCP-47 tag (`en`, `ja-Jpan`), when the notice is in a particular
language. The script subtag is extracted exactly as it is from a
delivery.
"""
languageAndScriptCode: String
"""
Leave unset unless the notice really is territory-qualified. `null` is
**not** the same as `Worldwide`.
"""
territory: String
"""
The default among several. At most one per list; when none is marked,
the first is.
"""
isDefault: Boolean! = false
}
input CreateArtworkSizeInput {
"""
A short slug: a–z, 0–9, `-` and `_`, up to 40 characters, starting with
a letter or digit. Unique in the organization.
"""
label: String!
width: Int!
height: Int!
crop: ArtworkCrop!
format: ArtworkFormat!
"""
JPEG only, 1–100; defaults to 85. Leave it out for PNG.
"""
quality: Int
"""
Scale a smaller cover up to the size. Defaults to false.
"""
upscale: Boolean! = false
}
input CreatePartyInput {
"""
Required. A party with no name identifies nobody, and the DAL refuses to
mint one on either ingress path.
"""
name: String!
"""
DDEX party identifier, if the customer has one. Parties **are** deduped
by DPID within a catalog, because a DPID is an identifier rather than a
label — so passing the same one twice returns the same party.
"""
dpid: String
"""
Interned under `PartyNameType`. `FullName` unless you say otherwise.
"""
nameType: String! = "FullName"
}
input CreateReleaseInput {
"""
Optional **while drafting only**. `releases_has_identity` requires a UPC
or a GRid to leave `draft`, so this is a deferral rather than a
relaxation — a release with neither has no identity and would multiply on
every redelivery. The one exception is an organization //SCHEMA has
exempted because it does not deliver DDEX (`organizations.require_upc`,
`0041`): its releases may publish with neither.
"""
upc: String
grid: String
title: String
"""
`Album`, `Single`, … Interned verbatim; an unknown value is accepted and
marked `observed` rather than rejected, exactly as a delivered one is.
"""
releaseType: String
"""
The release's ℗ notices — who owns the sound recordings.
Leave it out to keep what is there. Pass a list to **replace** them
all; `[]` removes every one. Not required to publish: a release can
legitimately carry none.
"""
plines: [CopyrightLineInput!]
"""
The release's © notices — who owns the compositions, artwork and text.
Same rule as `plines`: absent keeps, a list replaces, `[]` clears.
"""
clines: [CopyrightLineInput!]
"""
The label(s) the release came out on. Same rule as `plines`.
Each names a party made with `createParty`, not a string, so that two
spellings of one label stay one label and `releases(labelPartyPk:)`
finds everything on it.
"""
labels: [LabelInput!]
}
input CreateWebhookEndpointInput {
"""
Where to send. `https://` on the default port; never a private,
loopback or link-local address.
"""
url: String!
description: String
"""
At least one, from `webhookEventTypes`.
"""
eventTypes: [String!]!
"""
Only events from these catalogs of the organization's. Omit for every
catalog, including ones created later.
"""
catalogPks: [Int!]
}
"""
A new endpoint, and the one and only appearance of its signing secret.
"""
type CreatedWebhookEndpoint {
endpoint: WebhookEndpoint!
"""
`whsec_…`. Copy it now — it cannot be read back. Verify each delivery's
`webhook-signature` with it.
"""
secret: String!
}
"""
One party credited in one role.
"""
type Credit {
party: Party!
"""
Verbatim DDEX role — `MainArtist`, `StudioProducer`, `Composer`, …
"""
role: String
"""
Which vocabulary the role came from: `DisplayArtistRole` for billing,
`ResourceContributorRole` for the people who worked on the recording.
Exposed because the two cannot be told apart by the role string alone.
`MainArtist` and `FeaturedArtist` exist only in the first;
`ResourceContributorRole` has `FoleyArtist` and `MakeUpArtist`, so a
client filtering for performers by spelling gets the make-up department.
"""
roleKind: String
"""
`null` means the credit was not territory-qualified, which is NOT the
same as an explicit `Worldwide`.
"""
territory: String
"""
Billing order, when the sender expressed one.
"""
sequence: Int
}
input CreditInput {
partyPk: Int!
"""
Verbatim DDEX role — `MainArtist`, `Producer`, `Composer`, … Not
`Label`: a label is set with `labels` on the release.
"""
role: String!
"""
The AVS kind to intern `role` under. Release-level credits are
`DisplayArtistRole`; track-level ones are usually
`ResourceContributorRole`.
"""
roleKind: String! = "DisplayArtistRole"
"""
Leave unset unless the credit really is territory-qualified. `null` is
**not** the same as `Worldwide` — `0001` decision 6 keeps the two
distinguishable so //SCHEMA can regenerate what was asserted.
"""
territory: String
"""
Billing order.
"""
sequence: Int
}
"""
Implement the DateTime<Utc> scalar
The input/output is a string in RFC3339 format.
"""
scalar DateTime
"""
A deal's availability window — `DealTerms/ValidityPeriod`, DDEX's own
concept of when a commercial model may sell this release.
Distinct from [`Release::available_from`]: that field is this *catalog's*
publish gate, set once a release clears its own activation rules; this is
what the label itself asserted, per deal, and a release can carry several.
"""
type Deal {
"""
`CommercialModelType`, e.g. `SubscriptionModel`, `AdvertisementSupportedModel`.
"""
commercialModel: String
"""
`UseType`, e.g. `Stream`, `PermanentDownload`.
"""
useType: String
start: DateTime
end: DateTime
}
"""
What happened to a file delivered through the console.
The four outcomes are `MatchOutcome`'s, passed through rather than
flattened, because they mean genuinely different things to the operator who
just dropped the file and each wants different words:
- `awaiting` — delivered with no `deliveryNamespace`: stored, and waiting for
its message. A console message delivered within 24 hours that names this
file claims it (`deliverMessage`'s `claimedFiles`).
- `promoted` — it matched an expectation and the track now has its bytes.
- `orphan` — stored, but nothing is waiting for it. Not an error: the file
may simply have arrived before its message. Worth saying out loud, though,
because the likelier cause in a console is a mistyped path.
- `duplicate` — these exact bytes were already landed at this exact path.
- `hashMismatch` — it matched, but contradicts the `<HashSum>` the message
declared. The resource is marked failed and NOT promoted. A corrected
re-upload clears it without operator action.
"""
type DeliveredFile {
deliveryNamespace: String!
relativeUri: String!
"""
`promoted` | `awaiting` | `orphan` | `duplicate` | `hashMismatch`.
"""
outcome: String!
"""
Set for `promoted` and `hashMismatch` — the resource this satisfied.
"""
resourcePk: Int
}
"""
What happened to a DDEX message delivered through the console.
"""
type DeliveredMessage {
messagePk: Int!
"""
Where its media must be delivered to match. The console reuses it via
`outstandingFiles`; nobody has to read it.
"""
deliveryNamespace: String!
"""
Resources referenced whose bytes have not arrived. Zero means the
delivery is already complete — every file was either included or already
in the store from a previous delivery.
"""
awaitingFiles: Int!
"""
The dedup gate fired: this exact message was already ingested and
nothing changed. The happy path for a re-drop, not an error.
"""
duplicate: Boolean!
"""
Files delivered through the console BEFORE this message — waiting
because nothing expected them yet — that it claimed by filename and
matched. `awaitingFiles` already excludes them.
"""
claimedFiles: Int!
"""
Waiting filenames this message would have claimed but could not
unambiguously: more than one waiting file of that name, or one that two
of its entries would both want. Deliver those again now that the message
exists, and they route normally.
"""
ambiguousFiles: [String!]!
"""
For a `PurgeReleaseMessage`, what became of each release it named;
empty for any other message. A purge creates nothing and only ever
withdraws, so this is the whole of what it did.
"""
purged: [PurgedRelease!]!
}
"""
An SFTP login this organization has issued to one of its senders.
Carries no secret. The password exists in plaintext exactly once, in the
[`super::mutation::IssuedAccount`] returned by the mutation that created it;
afterwards only `hasPassword` remains, and a lost one is reset rather than
recovered.
"""
type DeliveryAccount {
deliveryAccountPk: Int!
"""
The SFTP login. Unique on this org's endpoint, not globally.
"""
username: String!
"""
Who it was issued to — 'The Orchard', 'Sony Music UK'. The DSP's own
label for a partner //SCHEMA has no record of.
"""
displayName: String!
hasPassword: Boolean!
"""
Live keys only. Zero with `hasPassword: false` means the account cannot
authenticate at all — created dormant, awaiting a key.
"""
keyCount: Int!
createdAt: DateTime!
"""
Last time something arrived on this account, best-effort.
"""
lastSeenAt: DateTime
"""
When access was withdrawn. Revoked accounts stay listed on purpose — the
support question is "did we ever grant this, and when did it end".
"""
revokedAt: DateTime
}
"""
A public key authorized on a delivery account, shown by fingerprint.
The key itself is not returned. It is not secret, but it is also not useful
to the console, and a field nobody reads is a field that eventually leaks
something adjacent to it.
"""
type DeliveryKey {
deliveryAccountKeyPk: Int!
"""
OpenSSH's `SHA256:…` form, so it can be compared against what
`ssh-keygen -lf` prints on the sender's machine.
"""
fingerprint: String!
createdAt: DateTime!
revokedAt: DateTime
}
"""
A file refused at intake.
"""
type DeliveryRejection {
rejectionPk: Int!
deliveryNamespace: String!
relativeUri: String!
"""
`null` when it was refused before being stored (SFTP refuses first).
"""
sha256: String
byteSize: Int
"""
What the header says it is: `mp3`, `aac`…
"""
detectedFormat: String!
reason: String!
rejectedAt: DateTime!
}
"""
A party as the authoring surface sees it.
"""
type DraftParty {
partyPk: Int!
name: String!
dpid: String
}
"""
A resource as the authoring surface sees it.
Deliberately not [`super::data::Resource`]: that type carries credits,
localized titles and ℗ notices assembled by the read loader, none of which
authoring has produced yet. Returning it would mean answering questions the
caller has not asked and this path cannot yet answer.
"""
type DraftResource {
"""
The handle to upload against.
"""
resourcePk: Int!
isrc: String
"""
`incoming` until bytes land, then `analyzing` → `stored`.
"""
state: String!
}
"""
One file of a resource's audio, in one codec and quality — the delivered
original among them.
"""
type Encoding {
"""
The stored profile: `original`, `aac-256-v1`, `aac-128-v1+clip30-v1`.
"""
profile: String!
"""
The encoding half of it — what `url(encoding:)` takes, pinned —
`original` for the delivered bytes.
"""
encoding: String!
"""
`null` for full length.
"""
clip: Clip
"""
These are the bytes the sender delivered.
"""
original: Boolean!
status: EncodingStatus!
container: String
codec: String
"""
`null` when not yet known: a master not yet analyzed.
"""
lossless: Boolean
bitrateKbps: Int
sampleRateHz: Int
bitDepth: Int
channels: Int
durationMs: Int
byteSize: Int
"""
Lowercase hex SHA-256 of the file: verify what you downloaded.
"""
sha256: String
}
"""
An encoding that can be asked for.
"""
type EncodingProfile {
"""
The pinned name, `aac-256-v1`.
"""
encoding: String!
"""
The family, `aac-256`, which follows the newest version.
"""
family: String!
"""
The stored profile: the encoding, with `+clip30-v1` for a clip.
"""
profile: String!
clip: Boolean!
codec: String!
bitrateKbps: Int
lossless: Boolean!
"""
Within `media_preview`.
"""
preview: Boolean!
reach: EncodingReach!
description: String!
}
"""
Which masters a profile is made for without being asked.
"""
enum EncodingReach {
"""
Every audio master, moments after it lands.
"""
EVERY_MASTER
"""
Every audio master an organization with lossless media holds —
enterprise organizations, and any other staff have turned it on for.
"""
ENTERPRISE
"""
Only after `requestEncoding`.
"""
ON_REQUEST
}
"""
Whether an encoding can be fetched yet.
"""
enum EncodingStatus {
"""
Mint it with `url(encoding:, clip:)`.
"""
READY
"""
Being made, or about to be: an eager profile, or one requested.
"""
PENDING
"""
The worker gave up on it; //SCHEMA staff have the reason.
"""
FAILED
}
type HttpHeader {
name: String!
value: String!
}
type InvoiceSummary {
invoicePk: Int!
periodStart: String!
periodEnd: String!
status: String!
currency: String!
totalCents: Int!
issuedAt: String
dueAt: String
}
input IssueDeliveryAccountInput {
"""
The SFTP login. Unique on this organization's endpoint, not globally:
two DSPs may each have an `orchard`.
Must be POSIX-shaped (3–32 characters, starting with a letter, then
`a–z`, `0–9`, `_`, `-`) because it becomes a real daemon user. The rule
is enforced by a CHECK constraint rather than restated here, so there is
only one place for it to be wrong.
"""
username: String!
"""
Who the account is for, in human terms: 'The Orchard', 'Sony Music UK'.
"""
displayName: String!
"""
Which of this organization's catalogs these deliveries land in. Defaults
to the one the credential is already scoped to, which is the right
answer for every org that has exactly one.
A catalog belonging to another organization is refused by the database's
composite FK, not by a check in this resolver.
"""
catalogPk: Int
"""
Generate a password. Defaults to **false**: keys are the better shape
for machine-to-machine delivery, so the more secure option is the one
you get by saying nothing.
A key-only account cannot authenticate until
[`AdminMutation::add_delivery_key`] gives it one. That is intentional —
it is created dormant rather than created open.
"""
withPassword: Boolean
}
"""
A newly issued account, and the one and only appearance of its password.
Same contract as an API key: shown once, stored as a hash, never
recoverable. A lost password is reset, which is why
[`AdminMutation::reset_delivery_password`] exists.
"""
type IssuedAccount {
deliveryAccountPk: Int!
username: String!
"""
`null` for a key-only account. Copy it now — it cannot be read back.
"""
password: String
}
"""
The label a release came out on.
A **party**, not a string: ERN 4.3 references one from the message's
`PartyList`, so the label arrives with its DPID and its localized names, and
two spellings of one label do not become two labels. That is what makes
grouping a catalog by label work at all (`0010`).
Deliberately not a [`Credit`]: a label did not perform, write or produce
anything, and putting it among the contributors would place it in every
"who played on this" answer.
"""
type Label {
"""
Stable handle for "every release on this label".
"""
partyPk: Int!
"""
The label's default name. Null only when the delivery named a party with
no name at all.
"""
name: String
"""
DDEX Party Identifier, when the sender supplied one.
"""
dpid: String
"""
`LabelType` — `DisplayLabel`, `UserDefined`. Usually absent: most deliveries
name one label and leave its role implicit.
"""
labelType: String
"""
Null means the sender did not territory-qualify it, which is NOT the
same as Worldwide.
"""
territory: String
isDefault: Boolean!
sequence: Int
}
"""
A label a release came out on, for `labels`.
"""
input LabelInput {
"""
The label, from `createParty`.
"""
partyPk: Int!
"""
`LabelType` — `DisplayLabel` or `UserDefined`. Usually left out: most releases
name one label and leave its role implicit.
"""
labelType: String
"""
Leave unset unless the label really is territory-qualified (a reissue
can have a different label per territory). `null` is **not** the same
as `Worldwide`.
"""
territory: String
"""
Display order when there are several.
"""
sequence: Int
"""
The default among several. At most one; when none is marked, the
first is.
"""
isDefault: Boolean! = false
}
"""
One label, with how much of this catalog is on it.
"""
type LabelSummary {
"""
Pass to `releases(labelPartyPk:)` for everything on this label.
"""
partyPk: Int!
name: String
dpid: String
"""
DISTINCT releases: a release naming this label under two territories is
two `release_labels` rows and still one release.
"""
releases: Int!
}
"""
One text value with its full ERN qualification.
ERN carries titles and names many times over — once per language/script, and
once per territory — and the schema keeps every variant. The scalar `title`
and `party.name` fields resolve one of these for convenience; this is the
whole set.
"""
type LocalizedText {
value: String!
"""
`DisplayTitle`, `FormalTitle`, `FullName`, …
"""
type: String
"""
The **entire** `LanguageAndScriptCode` as the sender wrote it —
`ja-Jpan`, `ja-Latn`, `zh-Hans-CN` — not just the language subtag.
//SCHEMA stores it verbatim so the ERN can be regenerated exactly.
"""
language: String
"""
The ISO 15924 subtag pulled out of `language` as a query convenience, so
the two deliberately overlap: `ja-Jpan` yields script `Jpan`.
"""
script: String
"""
`ApplicableTerritoryCode`. `null` means unqualified, which is NOT the
same as an explicit `Worldwide`.
"""
territory: String
"""
The sender's `IsDefault`. Frequently false on *every* variant, which is
why picking one automatically is lossy.
"""
isDefault: Boolean!
}
"""
What a resource's file is.
"""
enum MediaKind {
AUDIO
IMAGE
VIDEO
OTHER
}
type Message {
messagePk: Int!
ddexMessageId: String!
fileName: String!
senderDpid: String!
senderName: String
status: String!
failureReason: String
dateReceived: DateTime!
"""
A short-lived signed URL for the **original ERN XML**, exactly as
delivered.
This is the provenance link the product note asks for: every catalog row
traces to a message, and the message can still produce the bytes that
produced it. It is also the DDEX inspector's input — "why is this
malformed" is unanswerable without the literal file, which is why
`blob-store` keeps it rather than only the parse.
# Why `MediaRead` and not `CatalogRead`
The raw delivery carries more than the display metadata a player needs:
commercial terms, sender DPIDs, the full deal structure. A key powering
an app's track listing has no business reading it. `MediaRead` is
//SCHEMA's blob-access grant, so the raw document sits behind the same
gate as the masters — narrow first, relaxable later.
"""
sourceUrl(
"""
Lifetime in seconds; capped by the store.
"""
ttlSeconds: Int
): String!
}
"""
A freshly minted API key, and the only appearance of its token.
Stored as a SHA-256 digest thereafter, so this is not "shown once" as a
policy — it is shown once because no other copy exists.
"""
type MintedApiKey {
apiKeyPk: Int!
"""
Copy it now. There is no way to retrieve it later.
"""
token: String!
"""
The non-secret leading segment, also persisted, so the console can
identify this key in a list afterwards.
"""
tokenPrefix: String!
}
"""
The mutation root, merged the same way the query root is.
Authoring (catalog rows, on `CatalogWrite`), administration (the
credentials that let somebody else write them, on `ConfigWrite`), and
self-service account changes (on no scope at all — see [`account`]). Kept
apart because the scope that guards each differs, and a single root would
make it easy to write a new administrative mutation behind an authoring
check.
"""
type Mutation {
"""
Start a release. It is created in `draft` and is not part of the catalog
until published — it does not appear in `releases` or `release(upc:)`.
"""
createRelease(input: CreateReleaseInput!): Release!
"""
Amend a release — fix a UPC, add the title that was missing, set the
type, the ℗ and © notices or the label.
Only fields you pass are touched; an omitted one keeps its current value
rather than being cleared, so a client that does not know about a field
cannot erase it. The lists (`plines`, `clines`, `labels`) are the one
way to clear: passing one replaces every entry, so `[]` removes them.
# Works on published and delivered releases
Including ones that arrived over DDEX. An operator who finds a typo in a
delivered release must be able to correct it without asking the label to
redeliver.
Editing a release a **delivery** last asserted marks it
`locallyModified` (`0007`). That is not a warning — a corrected release
is usually more accurate than what arrived, which is why somebody
corrected it. It records that the row no longer matches what the label
sent, which matters because a redelivery is last-writer-wins and will
overwrite it.
"""
updateRelease(releasePk: Int!, input: CreateReleaseInput!): Release!
"""
Rename a track, or correct its ISRC or duration.
Exists so the file can come first: drop the audio, then name it. Locking
the title to creation time would force one workflow on everybody, and
the one it forces is the wrong way round for anybody working from a
folder of files.
Absent fields are left alone rather than cleared.
"""
updateResource( releasePk: Int!, resourcePk: Int!, title: String, isrc: String, durationSeconds: Int,
"""
The track's ℗ notices. Absent keeps them; a list replaces them all; `[]` clears.
"""
plines: [CopyrightLineInput!],
"""
The track's © notices — usually for artwork, text or video. Same rule as `plines`.
"""
clines: [CopyrightLineInput!]
): Boolean!
"""
Put the tracks in a given order.
`resourcePks` is the new order, first to last. Tracks in the release but
absent from the list keep their relative position after the listed ones,
so a partial list reorders rather than truncates.
Track numbers are positions, recomputed as 1..n — they are not resource
ids and never were, though a release that had tracks removed used to
look like they were, because removal left permanent gaps.
Returns `false` if any id names a resource outside this release, and
applies nothing: half of a bad order is a tracklist nobody asked for.
"""
reorderTracks(releasePk: Int!, resourcePks: [Int!]!): Boolean!
"""
Take a track or image back off a draft, and delete it.
Idempotent: removing something already gone returns `false` rather than
erroring, so a double-click is not a failure.
Any uploaded bytes stay in storage: media is stored by content address
and may back other resources.
"""
removeResource(releasePk: Int!, resourcePk: Int!): Boolean!
"""
Withdraw a release-level credit.
The party itself survives — parties are reused across releases and
deduped by DPID, so deleting one because a credit was removed would take
down every other release naming them.
"""
removeReleaseCredit(releasePk: Int!, partyPk: Int!, role: String!, roleKind: String! = "DisplayArtistRole"): Boolean!
"""
Withdraw a track-level credit. Same party-survives rule as
`removeReleaseCredit`.
"""
removeResourceCredit(resourcePk: Int!, partyPk: Int!, role: String!, roleKind: String! = "ResourceContributorRole"): Boolean!
"""
Add a track or an image to a draft release.
"""
addResource(input: AddResourceInput!): DraftResource!
"""
Create a party — an artist, label, producer or writer.
Parties belong to the catalog that created them, so this is the
customer's own namespace. Reuse the returned `partyPk` to credit the same
party again: creating one twice by name makes two parties, deliberately,
because matching on a bare name would merge two artists who happen to
share one.
"""
createParty(input: CreatePartyInput!): DraftParty!
"""
Credit a party on a draft release — a display artist, or anyone
credited at album level rather than on one track. The label is not a
credit: set it with `labels` on `createRelease` / `updateRelease`.
"""
creditRelease(releasePk: Int!, input: CreditInput!): Release!
"""
Credit a party on one track.
Distinct from [`Self::credit_release`] because DDEX distinguishes them: a
licensed-in track on a compilation keeps its own performers while the
release carries the compiler's label.
"""
creditResource(resourcePk: Int!, input: CreditInput!): DraftResource!
"""
Publish a draft: assert it, and put it in the catalog.
This is the act with consequences — everything before it is private to
the catalog that authored it. Publication is what mints the `sources` row
recording *who* asserted the release, which is why `created_by` is the
**publisher** rather than whoever started the draft: drafting is
collaborative, and the person who clicked Publish is the one making the
claim.
Idempotent by construction rather than by checking: the source's
idempotency key is `{releasePk}:{revision}`, so a second click carries
the same key and is absorbed by a unique index. Editing the draft bumps
the revision, so a publish after edits is a genuine second source.
"""
publishRelease(releasePk: Int!): PublishResult!
"""
Ask for permission to upload bytes for a resource.
The client hashes locally and declares the digest; //SCHEMA authorizes
**exactly** that content address and the store refuses anything else. The
server cannot compute the address itself — keys are content addresses, so
it would need the bytes it is trying to avoid receiving.
"""
requestUpload( resourcePk: Int!,
"""
Lowercase hex SHA-256 of the file.
"""
sha256: String!,
"""
Size in bytes. Rejected above 5 GiB — use SFTP for larger files.
"""
byteSize: Int!
): UploadTicket!
"""
A presigned PUT for a file being **delivered**, not authored.
The difference from [`request_upload`] is what it is scoped to. That one
names a `resourcePk` and backs a draft the operator is building; this one
names nothing, because a delivered file arrives *before* //SCHEMA knows
what it belongs to — which is the entire out-of-order premise. The
binding happens in [`deliver_file`], after the bytes exist.
Scoping it to nothing is safe because the key is a content address: the
caller can only write to `sha256/<their own digest>`, and a caller who
already has the bytes learns nothing by storing them. Tenancy attaches
at the row, not at the object — the same reason `ContentKey` carries no
catalog (`blob-store/src/key.rs`).
"""
requestDeliveryUpload(
"""
Lowercase hex SHA-256 of the file.
"""
sha256: String!,
"""
Size in bytes. Rejected above 5 GiB — use SFTP for larger files.
"""
byteSize: Int!
): UploadTicket!
"""
Discard a blocked delivery that will never be completed.
Restricted to `pending` releases by the DAL, which is what makes it a
safe thing to put a button on: a pending release was held out of the
catalog and no DSP ever saw it, so discarding takes nothing back. The
same operation against a live release would be a retraction, and that is
a takedown rather than a cleanup.
Returns `false` when nothing matched — an unknown release, another
catalog's, or one that has since gone live. Deliberately not an error:
the honest answer to "remove this" when it is already gone is that it is
gone.
"""
discardBlockedRelease(releasePk: Int!): Boolean!
"""
Withdraw a release from this catalog — a takedown.
The manual counterpart of a DDEX `PurgeReleaseMessage`, for a takedown
that arrived any other way: an email from the label, a rights dispute, a
mistake. Both end in the same transition and both announce
`release.state_changed` with `to: "withdrawn"`.
Withdrawal is a state, not a delete — rows, resources and bytes stay —
so it can be undone with `reinstateRelease`. It is recorded as a source
naming whoever withdrew it (no one, for an API key), the way publishing
is. A release already withdrawn is returned unchanged, so a double-click
reads as success; a draft is refused, because nothing has been
published to take back.
"""
withdrawRelease(releasePk: Int!): Release!
"""
Put a withdrawn release back in the catalog.
It returns to the state it was withdrawn from. A release that was live
goes back to `active`; one withdrawn while its media was still landing
goes back to `pending` and goes live when its files arrive, as it would
have — or at once, if they arrived while it was withdrawn. A redelivery
of the release does **not** reinstate it: a takedown stays until
someone chooses to undo it, here.
A release that is not withdrawn is returned unchanged.
"""
reinstateRelease(releasePk: Int!): Release!
"""
Ingest a DDEX message uploaded through the console.
Same pipeline as the SFTP worker — `parse_ddex` → `map` →
`ingest_message` — deliberately, so a delivery completed by hand and one
that arrived over SFTP produce identical rows. Availability comes from
the catalog's own config for the same reason: a console delivery must not
quietly bypass an `auto_activate` policy the operator set.
# The namespace is derived from the bytes
SFTP takes it from the batch directory. There is no directory here, so it
is `console-<first 12 of the SHA-256>` — which makes redelivering the
same file a **no-op by construction** rather than by a check: it lands in
the same namespace, hits the dedup gate on `ern_messages`, and any media
already matched against that namespace stays matched. Randomising it
would have made every re-drop a fresh half-empty delivery.
"""
deliverMessage(
"""
Lowercase hex SHA-256 of the already-uploaded XML.
"""
sha256: String!,
"""
The original filename, for the delivery log.
"""
fileName: String!
): DeliveredMessage!
"""
Land a delivered file against a namespace, exactly as SFTP would.
This is the console's entry to the same path
`DataStore::record_landed_file` serves for the ingest worker — same
staging, same inline match against `expected_files`, same automatic
promotion when the last file arrives. A file dropped in a browser and a
file dropped over SFTP become indistinguishable rows, which is the
property worth protecting: the operator completing a delivery by hand
must not produce a second-class delivery.
`deliveryNamespace` is the caller's to supply because only the caller
knows whether this is a new batch or bytes for an existing one. The
console resolves it from `outstandingFiles`; nothing here validates that
it matches anything, because a file legitimately arrives *before* the
message that expects it, and rejecting the early half would break
out-of-order delivery in the name of tidiness.
**Omit it when nothing expects the file yet** — media dropped before its
message. The file is stored and waits under its bare filename
(`relativeUri`, no directories), and the console message that names it
claims it within 24 hours (`deliverMessage`). That is what makes arrival
order not matter in the console as well as over SFTP.
"""
deliverFile(
"""
The batch key, matching the message's exactly. Omit it when no message expects the file yet: it then waits, by filename, to be claimed.
"""
deliveryNamespace: String,
"""
The <File> URI this satisfies, verbatim from the message.
"""
relativeUri: String!,
"""
Lowercase hex SHA-256 of the uploaded bytes.
"""
sha256: String!
): DeliveredFile!
"""
Record that the bytes are in place, and back the resource with them.
//SCHEMA confirms with the store rather than believing the client: a
`Stored` — and therefore the row this writes — is obtainable only for
bytes that are actually present, so `completeUpload` cannot lie about a
file. That is store-then-record surviving a flow where the API never
touched the bytes.
"""
completeUpload(resourcePk: Int!, sha256: String!): DraftResource!
"""
Ask for an on-demand encoding of a resource — an MP3 for a download
store, a WAV, ALAC, a clip of an encoding other than AAC 128.
Writes intent and returns: the encoding is `PENDING` until the worker
has made it, and `resource.encoded` says when. Asking twice is one
request; asking for one that exists returns it `READY`. The same scopes
as minting it apply — there is no asking for what you could not fetch.
"""
requestEncoding( resourcePk: Int!,
"""
A family ("mp3-320") or a pinned encoding ("mp3-320-v1").
"""
encoding: String!,
"""
The clip of that encoding rather than full length.
"""
clip: Boolean! = false
): Encoding!
"""
Mint an API key for this organization.
# A credential can never mint one more powerful than itself
Requested scopes are checked against the caller's own. Without that, a
key holding `ConfigWrite` could mint a key holding everything, and
revoking the first would not revoke what it had created — privilege
escalation that survives the revocation meant to stop it.
The same rule makes the console's checkbox list honest: it asks
`grantableScopes` and offers exactly what will be accepted.
# Scoped to one catalog
Defaulting to the caller's. A catalog belonging to another organization
is refused by the composite foreign key on `(catalog_pk, org_pk)`, in
the database rather than in a check here that could be forgotten.
"""
mintApiKey( name: String!,
"""
Wire spellings: catalog_read, catalog_write, config_read, config_write, media_read, media_preview, media_lossless, media_write.
"""
scopes: [String!]!, catalogPk: Int,
"""
Optional expiry. A key with none never expires on its own.
"""
expiresAt: DateTime
): MintedApiKey!
"""
Revoke a key, immediately and permanently.
The row survives with its `revoked_at` set: a boolean would answer the
access question and lose the audit one. Idempotent — a second call does
not move the timestamp, so "when did this stop working" keeps its first
answer.
"""
revokeApiKey(apiKeyPk: Int!): Boolean!
"""
Add somebody to this organization.
# Same organization, always
The organization is `p.org_pk` — taken from the credential, never from
an argument. There is no way to spell "add this person to a different
org" because the mutation does not accept one, which is the same
property that keeps authoring mutations inside one catalog.
# A new account, or an existing person joining
If the address is unknown, an account is created and a password comes
back once. If it is a //SCHEMA user already — the consultant working for
two labels — they gain a membership and their existing credential is not
touched, so `password` is null. [`auth::membership`] explains why that
distinction is a tenancy boundary rather than a convenience.
# Roles
`owner`, `admin` or `member`. Only an owner may confer ownership; see
[`may_confer_ownership`]. Ask `grantableRoles` rather than guessing.
"""
addOrgMember(input: AddOrgMemberInput!): AddedMember!
"""
Change a member's role.
Refuses on the caller's own row. Somebody stepping down or locking
themselves out should be a deliberate act by *another* administrator,
not a mis-click on the row that happens to be yours — and the version of
this that goes wrong ends with an organization nobody can administer.
Also refuses to demote the last owner. That invariant lives in
[`auth::membership::set_role`], inside the transaction, so it cannot be
lost to a race between two administrators.
"""
setOrgMemberRole( userPk: Int!,
"""
Wire spellings: owner, admin, member.
"""
role: String!
): Boolean!
"""
Remove somebody from this organization.
Their //SCHEMA account survives — membership is the org-scoped fact, and
the person may belong to another organization whose access this one has
no business ending. Access here stops immediately whatever sessions they
hold, because the principal is rebuilt from `org_members` on every
request.
Returns false when they were not a member, so a `userPk` belonging to
somebody else's organization is a no-op rather than an error that
confirms the id exists.
"""
removeOrgMember(userPk: Int!): Boolean!
"""
Give a member a freshly generated password, ending their sessions.
The only recovery path: //SCHEMA stores an argon2id hash, so a forgotten
password is replaced rather than looked up. Shown once, like every other
secret here.
Refused for somebody who also belongs to another organization — that
would be a cross-tenant account takeover — and refused on the caller's
own row, which would log them out and hand them a password they would
then have to type back in.
"""
resetOrgMemberPassword(userPk: Int!): String!
"""
Issue an SFTP login on this organization's endpoint.
"""
issueDeliveryAccount(input: IssueDeliveryAccountInput!): IssuedAccount!
"""
Replace an account's password with a freshly generated one, invalidating
the old one immediately. The only remedy for a lost password.
"""
resetDeliveryPassword(deliveryAccountPk: Int!): String!
"""
Withdraw an account's access.
The row survives, because `sources` points at it and provenance must
outlive the credential that produced it. Idempotent: a second call does
not move the timestamp, so "when did access end" keeps its first answer.
"""
revokeDeliveryAccount(deliveryAccountPk: Int!): Boolean!
"""
Authorise a public key on an account.
Takes the `authorized_keys` line the sender produced
(`ssh-ed25519 AAAA… alice@laptop`) and stores it verbatim. Several keys
may be live at once, which is what makes rotation gapless: add the new
one, confirm it works, then revoke the old one.
"""
addDeliveryKey( deliveryAccountPk: Int!,
"""
An authorized_keys line, e.g. 'ssh-ed25519 AAAA… alice@laptop'.
"""
publicKey: String!
): DeliveryKey!
"""
Withdraw one key, leaving any others live.
"""
revokeDeliveryKey(deliveryAccountKeyPk: Int!): Boolean!
"""
Set this organization's SFTP hostname — `bandcamp.ftp.schema.fm`.
Provisioning DNS and a listener for that name is an operations act
outside //SCHEMA's control, so this records the intent rather than
performing it. It is here so a DSP's endpoint is data an operator can
set, not a redeploy.
Returns the accounts unchanged — the hostname moves, the logins do not.
"""
setSftpHost(
"""
A bare hostname, no scheme and no port.
"""
host: String
): [DeliveryAccount!]!
"""
Change the signed-in user's own password.
Requires the current password — proving you still hold it is what
makes this different from
[`super::admin::AdminMutation::reset_org_member_password`], which an
administrator can do without one because they are not the account
holder. Revokes every other session this user holds, so a stolen
cookie stops working the moment its owner changes the password
specifically to invalidate it; the session making this call is left
alone.
Refused for an API key — there is no password behind one — and while a
//SCHEMA employee is impersonating, so staff can never change a
customer's password while wearing their identity.
"""
changePassword(oldPassword: String!, newPassword: String!): Boolean!
"""
Change where invoices are sent.
**The whole of `BillingWrite`.** Currency, payment terms and the legal
entity are contract terms and are not parameters here — no customer
credential can change what they are charged, and that asymmetry is the
design rather than an unfinished mutation.
"""
updateBillingContact(billingEmail: String!, postalAddress: String): BillingContact!
"""
Mint a Stripe Billing Portal session and return the URL to send the
customer to.
This is the whole of //SCHEMA's payment-method handling. The card is
entered on Stripe's page, never on ours, so no card number reaches this
codebase and PCI scope stays at SAQ-A.
Behind `BillingWrite` rather than `BillingRead`: the session lets the
holder change the payment instrument, which is not a read even though
this call returns a string.
"""
createBillingPortalSession: String!
"""
Cancel your trial. Nothing is charged; your SFTP endpoint is turned off
now; the console and API keep working until the trial's end, when the
organization and its data are deleted. Resumable until then.
Idempotent: canceling a canceled trial returns it unchanged and sends
no second email.
"""
cancelTrial(reason: String): Trial!
"""
End the trial now and start paying: your card is charged today instead
of when the trial ends, and the subscription starts with it.
Records the request; the charge follows within seconds, made by the
billing service rather than this API. Refused for a canceled trial —
resume it first, so that "canceled" never quietly turns into a charge.
Idempotent: asking twice is one request.
"""
startSubscription: Trial!
"""
Undo a cancellation, any time before the trial ends. The trial keeps
its original end date, and the SFTP endpoint is set up again — on a new
address, since the old one went back to the pool.
"""
resumeTrial: Trial!
"""
Create an endpoint. Returns its signing secret, once.
Every owner of the organization is emailed — who created it, where it
sends and what it receives — in the same transaction. If none can be,
the endpoint is not created.
"""
createWebhookEndpoint(input: CreateWebhookEndpointInput!): CreatedWebhookEndpoint!
"""
Change an endpoint. Any change that lets it receive more than before —
a new URL, more event types or catalogs, turning it back on — emails
every owner, in the same transaction.
"""
updateWebhookEndpoint(input: UpdateWebhookEndpointInput!): WebhookEndpoint!
"""
Replace an endpoint's signing secret. Returns the new one, once. The old
one keeps signing beside it for 24 hours, so a receiver can switch over
without missing a delivery.
"""
rotateWebhookSecret(endpointPk: Int!): String!
"""
Delete an endpoint, with its delivery log. Returns false if this
organization has no endpoint by that id.
"""
deleteWebhookEndpoint(endpointPk: Int!): Boolean!
"""
Send a `webhook.test` event to this endpoint, through the same path as
every other event — signed, retried and logged — so a test that arrives
and verifies means the integration works. Returns the event's id.
"""
sendTestWebhook(endpointPk: Int!): String!
"""
Send one delivery again, with the same `webhook-id`, as a fresh run of
attempts. Safe on a delivery that succeeded: receivers dedupe on the id.
"""
redeliverWebhook(deliveryPk: Int!): Boolean!
"""
Add an artwork size. Renditions of every cover your catalogs hold are
made from the moment it is saved; until each is ready,
`artwork(size:)` answers `PENDING`.
"""
createArtworkSize(input: CreateArtworkSizeInput!): ArtworkSize!
"""
Change a size. Only what is given changes. A new label re-renders
nothing; new dimensions, crop, format or quality are a new rendition,
`PENDING` until made, and the old one is retired once nobody uses it.
That second kind of change returns the size under a new
`artworkSizePk`: the old definition is kept, retired, because billing
prices past days from it.
"""
updateArtworkSize(input: UpdateArtworkSizeInput!): ArtworkSize!
"""
Remove a size. `artwork(size:)` with its label is an error from now on.
Its renditions are retired once no organization holding the cover uses
the same spec. `false` when there was no such size.
"""
deleteArtworkSize(artworkSizePk: Int!): Boolean!
"""
Close an open alert without fixing it, saying why. Only where the
alert's `dismissable` is true; anything else is closed by its fix.
"""
dismissAlert( alertPk: Int!,
"""
Why it needs no fix. Required, and kept with the alert.
"""
note: String!
): Alert!
"""
Mark alerts read for the signed-in person. Returns how many were newly
marked; ids that are not this catalog's alerts are ignored. Sessions
only.
"""
markAlertsRead(alertPks: [Int!]!): Int!
}
"""
ISO 8601 calendar date without timezone.
Format: %Y-%m-%d
# Examples
* `1994-11-13`
* `2000-02-24`
"""
scalar NaiveDate
"""
A person with access to this organization.
Carries no credential material and cannot: passwords are argon2id hashes in
`user_identities`, and `hasPassword` is the only thing about them the
console needs.
"""
type OrgMember {
userPk: Int!
email: String!
displayName: String
"""
`owner`, `admin` or `member`.
"""
role: String!
"""
`invited`, `active`, `suspended` or `deactivated`. Only `active` may
sign in, so a row's existence is not permission to log in.
"""
status: String!
"""
Whether they hold a password at all. False for somebody who signs in
only through an external provider.
"""
hasPassword: Boolean!
"""
When they joined **this** organization, which may be long after their
//SCHEMA account was created.
"""
joinedAt: DateTime!
lastLoginAt: DateTime
"""
True when this person also belongs to another organization. Surfaced
because it is why `resetOrgMemberPassword` refuses for them — their
credential is not this organization's to replace.
"""
sharedWithOtherOrgs: Boolean!
"""
True for the caller's own row, so the console can refuse to let somebody
remove or demote themselves without comparing ids it would have to be
told.
"""
isYou: Boolean!
}
"""
A file a delivered message is still waiting for.
Exposed so the console can resolve a dropped file's `deliveryNamespace`
without the operator ever seeing one. A file arriving over SFTP carries its
namespace in its path; a file dropped in a browser carries nothing, and this
is what recovers it.
"""
type OutstandingFile {
"""
Echo this back on `deliverFile`. Opaque — do not parse it.
"""
deliveryNamespace: String!
"""
The `<File>` URI verbatim, as the message wrote it. May contain
directories, so a dropped filename matches it by *suffix*, not equality.
"""
relativeUri: String!
releasePk: Int
title: String
}
"""
A party: an artist, label, publisher or person.
Parties belong to the catalog and are reused within it, so the same
artist reached through two different releases is the same `partyPk`.
"""
type Party {
partyPk: Int!
"""
DDEX party identifier, when the sender supplied one. Many parties —
especially individual performers — legitimately have none.
"""
dpid: String
"""
One name, chosen by the sender's `IsDefault` flag.
**Lossy, and often arbitrary.** A party routinely carries the same name
romanized and in its own script (`Saeko Shu` / `しゅうさえこ`) with
neither marked default, in which case this picks whichever the database
returns first. Use [`Party::names`] whenever the script matters.
"""
name: String
"""
Every name this party carries, in all languages, scripts and
territories.
"""
names(
"""
Filter by text type, e.g. "DisplayTitle".
"""
type: String,
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan" or "ja-Latn". Not a prefix — use `script` to select by script alone.
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan" or "Latn".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [LocalizedText!]!
}
"""
A phonogram copyright notice — the ℗ line.
Qualified by language, script and territory exactly as a title is, because
ERN gives `PLine` the same attributes.
"""
type Pline {
"""
`PLineText`, e.g. `(P) 1994 EMI Music Japan Inc.`
"""
value: String!
"""
`Year`, verbatim as delivered. A **string**, not an integer: //SCHEMA
never lets validation gate delivery, so a sender writing `1994-01`
reaches the catalog rather than failing the message. Cast at the edge.
"""
year: String
"""
`PLineCompany`. Usually absent — most senders put the company only
inside `value`.
"""
company: String
language: String
script: String
territory: String
isDefault: Boolean!
}
"""
The terms in force, and the next ones if a change is scheduled.
Read-only. Nothing a customer can do here changes what they are charged —
see `Scope::BillingWrite`.
"""
type PriceTerms {
effectiveFrom: String!
"""
The platform fee per `fee_interval` — a month's, or a year's.
"""
platformFeeMicros: Int!
"""
`month` or `year`: how often the platform fee recurs.
"""
feeInterval: String!
"""
`advance` when the platform fee is charged before the period it covers
(self-serve plans), `arrears` when it is invoiced after. Usage — storage
overage and delivery — is always invoiced after.
"""
feeTiming: String!
includedGb: Int!
overageMicrosGbMonth: Int!
ingestMicrosGb: Int!
includedRequests: Int
"""
When the rate changes next, if a deal has been scheduled.
Surfaced so a customer learns about a scheduled change from the console
rather than from an invoice — which is the whole point of letting a deal
be entered months ahead.
"""
nextChangeOn: String
}
"""
The result of attempting to publish.
Validation problems come back as **data, not errors**. "No UPC yet" is an
expected answer the console renders as a checklist, and a GraphQL `errors`
entry is the wrong shape for it: errors null out the field, carry no
structure, and can only report one thing at a time. `problems` reports
everything wrong in one pass so a user fixes it in one pass.
"""
type PublishResult {
"""
True when the release is now in the catalog — including when a previous
call already put it there, so a double-click reads as success.
"""
published: Boolean!
"""
Everything blocking publication. Empty when `published`.
"""
problems: [String!]!
"""
The release as it now stands.
"""
release: Release
}
"""
What a purge did to one release it named.
"""
enum PurgeOutcome {
"""
It was live (or held, awaiting files) and is now withdrawn.
"""
WITHDRAWN
"""
It had already been withdrawn. The purge's intent already held.
"""
ALREADY_WITHDRAWN
"""
The identifier belongs to an unpublished draft — nothing to take back.
"""
DRAFT
"""
This catalog holds no release with that identifier. Nothing was created.
"""
UNKNOWN
"""
Another delivery account delivered it, so this purge may not withdraw
it. Not reachable from the console, which has no delivery account.
"""
OTHER_ACCOUNT
}
"""
One release a purge named.
"""
type PurgedRelease {
upc: String
grid: String
"""
The release it resolved to; `null` when `outcome` is `UNKNOWN`.
"""
releasePk: Int
outcome: PurgeOutcome!
}
"""
Both query roots.
"""
type Query {
"""
Who this credential is. Requires authentication but no scope: a caller
must always be able to discover what it holds.
"""
viewer: Viewer!
"""
The catalog this credential acts on.
Takes no id argument on purpose. The catalog comes from the credential,
never from the query — a resolver that accepted a `catalogPk` would need
its own authorization check, and that is the check that eventually gets
forgotten.
"""
catalog: Catalog
"""
One release by primary key, **including drafts**.
The data-plane surface deliberately hides drafts: `releases` and
`release(upc:)` both exclude them, because an unpublished release is not
part of the catalog yet. That leaves authoring with no way to read back
its own work, which is what this is for — and why it lives on the control
plane beside the other console-facing fields.
Gated on `CatalogWrite`, not `CatalogRead`: seeing half-finished work is
an authoring privilege. A read-only integration powering someone's app
has no business seeing a release nobody has published.
"""
draftRelease(releasePk: Int!): Release
"""
Work in progress — releases still being authored.
Gated on `CatalogWrite` for the same reason `draftRelease` is: seeing
unpublished work is an authoring privilege, not a reading one.
"""
draftReleases(limit: Int! = 25): [Release!]!
"""
Recent deliveries into this catalog, newest first.
"""
messages(limit: Int! = 25): [Message!]!
"""
Deliveries that arrived but cannot go live, and what is blocking each.
`pending` releases are excluded from `releases` — they are not part of
the catalog, because nothing should go out missing its audio or its
sleeve. That makes them invisible, which is worse than showing them
wrongly: a blocked delivery is something an operator has to chase.
Promotion is automatic. When the missing files land — by redelivery or
by the reconcile sweep — the release goes `active` without anybody
pressing anything. This is a worklist, not an approval queue.
"""
blockedReleases(limit: Int! = 25): [BlockedRelease!]!
"""
Every label in this catalog, busiest first.
The catalog's label axis: grouping releases by who put them out is one
of the two or three things anybody browsing a catalog actually wants,
and `0010` indexes `(party_pk, release_pk)` so this is a scan of the
index rather than of the releases.
Drafts and pending deliveries are excluded, matching `releases` — a
half-arrived delivery should not inflate a label's count.
"""
labels(limit: Int! = 200): [LabelSummary!]!
"""
Files delivered messages referenced that have not arrived yet.
The console uses this to route a dropped file: match its name against
`relativeUri` and reuse the `deliveryNamespace` of whatever wanted it.
That is the whole reason an operator never has to know what a namespace
is — SFTP recovers it from the directory, and this recovers it from the
expectation the message already recorded.
`CatalogRead`, not `MediaWrite`: it reveals filenames a message
referenced, which is metadata, and refusing it to a read-only credential
would make the delivery UI unable to explain itself.
"""
outstandingFiles(limit: Int! = 200): [OutstandingFile!]!
"""
Every API key this organization holds, newest first.
Never returns a token. The plaintext exists once, in the response to the
mutation that created it, and is stored only as a SHA-256 digest
thereafter — so this could not return one even if a caller asked.
`ConfigRead`, not `CatalogRead`: enumerating an organization's
credentials is an administrative act, and a key that lists a catalog's
tracks has no business doing it.
"""
apiKeys: [ApiKey!]!
"""
Everyone with access to this organization, longest-standing first.
# Org-scoped, and the org comes from the credential
`p.org_pk`, never an argument — the same rule every other resolver
follows for `catalog_pk`. There is no way to ask about another
organization's membership because no resolver accepts an organization,
so none can forget to check one.
`ConfigRead` rather than `CatalogRead`: a list of who can reach a
catalog is administrative, and a key that reads a track listing has no
business enumerating the people behind it.
"""
orgMembers: [OrgMember!]!
"""
The roles this caller may confer on a member.
Asked rather than hardcoded, for the same reason `grantableScopes` is:
offering a choice the server will refuse is worse than not offering it.
Only an owner may confer ownership — see
[`super::admin::AdminMutation::add_org_member`].
"""
grantableRoles: [String!]!
"""
The scopes this caller could grant to a new key.
A credential may never mint one more powerful than itself, so the
console asks rather than guessing — offering a checkbox that the server
will refuse is worse than not offering it.
"""
grantableScopes: [String!]!
"""
The SFTP logins this organization has issued, newest first.
# Scoped by org, not by catalog
Every other field here is catalog-scoped, because the catalog is the
isolation boundary. Delivery accounts are the exception by construction:
the endpoint hostname belongs to the *organization* (`0006`), so an org
with a production and a sandbox catalog issues accounts on one endpoint
and needs to see them together. Reading the org's own credentials is not
a cross-tenant read — `p.org_pk` comes from the credential, exactly as
`p.catalog_pk` does.
`ConfigRead` rather than `CatalogRead`: these are credentials, and an
integration that lists a catalog's tracks has no business enumerating
who may deliver into it.
"""
deliveryAccounts: [DeliveryAccount!]!
"""
Public keys authorized on one delivery account.
Returns empty rather than erroring for an account belonging to another
organization — the org predicate is in the query, so a probe for
somebody else's account is indistinguishable from one with no keys.
"""
deliveryAccountKeys(deliveryAccountPk: Int!): [DeliveryKey!]!
"""
The values of one DDEX vocabulary, for a picker.
# This cannot mint a row, and that is the point
It selects. `common::vocab`'s rule is that resolve-or-create belongs to
ingest alone, because a value arriving from a query parameter must never
be able to write into the `observed` drift report — the report means "a
DSP sent us something DDEX does not define", and a typo'd search box
would make it mean nothing.
The same reasoning is why this query exists at all: with the allowed
values readable, the console can offer them instead of a free-text box,
and an authoring mutation can refuse what is not among them.
`search` is a case-insensitive substring; omit it for the whole kind.
The cap is 1000, which is above the largest set that exists
(`InstrumentType`, 709) rather than an arbitrary round number — clients
are expected to fetch a whole vocabulary once and filter locally, since
these change when DDEX ships an AVS revision and not otherwise. A 500
cap silently truncated `CurrentTerritoryCode` at 503.
"""
vocabValues(
"""
e.g. "DisplayArtistRole", "ResourceContributorRole".
"""
kind: String!, search: String, limit: Int! = 50
): [VocabValue!]!
"""
Whether DDEX enumerates a kind — i.e. whether authoring will refuse a
value outside the set.
The console asks so it knows whether to present a closed picker or a
picker-with-free-text. `GenreText` and `LanguageCode` are genuinely open
sets and a closed list there would be wrong.
"""
vocabKindIsClosed(kind: String!): Boolean!
"""
Message counts by status over a trailing window — the dashboard's
ingestion-health numbers.
"""
ingestionStats(days: Int! = 30): [StatusCount!]!
"""
Releases in this catalog.
"In this catalog" is `releases.catalog_pk`, not a filter that could be
omitted: releases are per-catalog (`0001` decision 2), so a release this
catalog was never delivered is a different row entirely. Without
`states`, the published catalog: `active` and `withdrawn`.
"""
releases( limit: Int! = 50, offset: Int! = 0,
"""
Partial, case- and accent-insensitive match against the release's title, a release-level display artist credit, or a label — matching ANY of the three, not all. Accents fold both ways, so `sinead` finds `Sinéad`.
"""
q: String,
"""
Only releases in these states. Omitted, `ACTIVE` and `WITHDRAWN` — the published catalog. Naming `DRAFT` needs `catalog_write`, as `draftReleases` does.
"""
states: [ReleaseState!]
): [Release!]!
"""
Total releases [`Self::releases`] can page through for the same
search — the count a paging client needs to know when to stop asking,
since a page that happens to come back full is not on its own proof
there is another.
"""
releasesCount( q: String,
"""
Only releases in these states. Omitted, `ACTIVE` and `WITHDRAWN` — the published catalog. Naming `DRAFT` needs `catalog_write`, as `draftReleases` does.
"""
states: [ReleaseState!]
): Int!
"""
One release by UPC.
`null` covers both "no such UPC" and "exists in the corpus but not in
your catalog", deliberately indistinguishably — otherwise this endpoint
would answer "has //SCHEMA been delivered this release by anyone?" to
any caller with a key.
"""
release(upc: String!): Release
"""
The encodings this organization can ask for — listed here so clients
never hardcode one. `url(encoding:)` takes `encoding` (pinned) or
`family` (newest version), with `clip: true` for the rows where `clip`
is true.
Per organization, so it needs a credential: organizations with lossless
media (enterprise by default) see the lossless profiles, everyone else
the lossy ones. Read on every request, like the gate in [`mint`], so
the list changes the moment a plan does.
"""
encodingProfiles: [EncodingProfile!]!
"""
Files refused at intake in this catalog, newest first: today, lossy
audio masters. A release waiting for one stays `pending` until an
accepted file arrives at the same path.
"""
deliveryRejections(limit: Int! = 50): [DeliveryRejection!]!
"""
Quantities for the current period. `UsageRead` only.
"""
usage: Usage!
"""
Spend so far and the forecast. `BillingRead` — this carries rates.
"""
billingPeriod: BillingPeriod!
"""
The terms in force, and the next scheduled change. `BillingRead`.
"""
priceTerms: PriceTerms
"""
Where invoices are sent, and whether a card can be managed yet.
`BillingRead`.
"""
billingContact: BillingContact
"""
Issued and draft invoices, newest period first. `BillingRead`.
//SCHEMA renders no PDF — an invoice document is Stripe's to host, and
rebuilding that would be rebuilding a solved thing with worse
deliverability.
"""
invoices(
"""
How many to return; capped at 36.
"""
limit: Int
): [InvoiceSummary!]!
"""
Your organization's trial, or `null` when it is not in one. `UsageRead`
— when the trial ends is not commercial detail, and a member planning
deliveries has every reason to know it.
"""
trial: Trial
"""
Every event type an endpoint can subscribe to. `webhook.test` is not
among them: it is sent to one endpoint on request (`sendTestWebhook`).
"""
webhookEventTypes: [WebhookEventType!]!
"""
This organization's webhook endpoints, oldest first. No secret is ever
returned: it was shown once, when created or rotated.
"""
webhookEndpoints: [WebhookEndpoint!]!
"""
One endpoint, or `null` if this organization has none by that id.
"""
webhookEndpoint(endpointPk: Int!): WebhookEndpoint
"""
Your organization's artwork sizes, by label. They apply to every catalog
it owns; ask for a cover by label with `Resource.artwork(size:)`.
"""
artworkSizes: [ArtworkSize!]!
"""
The bounds a size must fall within.
"""
artworkSizeLimits: ArtworkSizeLimits!
"""
This catalog's alerts: faults only the customer can fix, one per
condition. Open alerts list blocking first, then most recently seen.
"""
alerts( status: AlertStatus! = OPEN,
"""
Only alerts of this kind.
"""
kind: AlertKind,
"""
Only alerts about this release.
"""
releasePk: Int,
"""
At most this many, up to 200. Defaults to 50.
"""
first: Int,
"""
`endCursor` from the previous page.
"""
after: String
): AlertConnection!
"""
Counts of this catalog's open alerts.
"""
alertCounts: AlertCounts!
}
type Release {
releasePk: Int!
upc: String
grid: String
"""
The default title. Territory-qualified variants are not resolved here —
choosing one needs a territory, and `0001` decision 6 is explicit that
an absent territory is not the same as `Worldwide`.
"""
title: String
"""
`pending`, `active` or `withdrawn` **for this catalog**.
"""
state: String!
availableFrom: DateTime
updatedAt: DateTime!
"""
When this release was last corrected locally after a **delivery**
asserted it (`0007`). `null` means it still matches what the label sent,
or that no label ever sent it.
Not a warning. A locally-corrected release is usually more accurate than
what arrived — that is why somebody corrected it. What it is not, is what
the label sent, and the difference matters because a redelivery is
last-writer-wins and will overwrite the correction.
"""
locallyModifiedAt: DateTime
"""
When this release was withdrawn from this catalog — by a DDEX purge or
by `withdrawRelease`. `null` unless `state` is `withdrawn`;
reinstatement clears it.
"""
withdrawnAt: DateTime
"""
Every resource in this release, flattened into track order.
The common case — a player wants a track list and does not care which
disc a track sits on. Use [`Release::groups`] when the disc/side
structure matters.
This is a depth-first flatten of exactly the tree `groups` returns, not
an independently-ordered query. Two orderings derived separately would
eventually disagree, and a track list that disagrees with the disc
listing is worse than either alone.
"""
resources(
"""
Only resources of this kind — the tracks, or the artwork.
"""
kind: MediaKind
): [Resource!]!
"""
Parties credited on the release itself — display artists, label, and
anyone else credited at album level rather than on one track.
`role` filters case-insensitively, so `contributors(role: "MainArtist")`
is the artist-name query. Track-level credits live on
[`Resource::contributors`]; DDEX distinguishes the two and so does this.
"""
contributors(
"""
Filter by DDEX role, e.g. "MainArtist". Case-insensitive.
"""
role: String,
"""
Credits applicable in this territory, e.g. "US": rows scoped to it, plus unqualified rows where nothing more specific exists. An explicit Worldwide credit matches only territory: "Worldwide".
"""
territory: String
): [Credit!]!
"""
Every title of this release, in all languages, scripts and territories.
"""
titles(
"""
Filter by text type, e.g. "DisplayTitle".
"""
type: String,
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan" or "ja-Latn". Not a prefix — use `script` to select by script alone.
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan" or "Latn".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [LocalizedText!]!
"""
Phonogram copyright notices for the release as a whole.
"""
plines(
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan".
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [Pline!]!
"""
The © notices — the composition/publishing right.
Separate from [`Self::plines`] because they are separate rights. For an
audio release this is the only level a © appears at: ERN attaches one to
`Image`, `Text` and `VideoEdition`, but not to `SoundRecordingEdition`.
Filtered on the same qualification axes, and reusing the ℗ filter — the
two carry identical language/script/territory/isDefault attributes, so a
second copy of that resolution rule would be a place for them to drift.
"""
clines(
"""
BCP-47 tag, matched case-insensitively against the whole tag.
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Latn".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [Cline!]!
"""
The label(s) this release came out on, default first.
Several is normal rather than exceptional: ERN names an original label
and a licensor separately, and a reissue can carry a different label per
territory. Ordering is the sender's — `isDefault`, then the order the
message listed them in.
"""
labels: [Label!]!
"""
The disc/side tree, parents first.
"""
groups: [ResourceGroup!]!
"""
The release's front cover at one of your organization's artwork sizes —
`resources` filtered to the `FrontCoverImage`, then
`Resource.artwork(size:)` on it, in one field. `null` when the release
has no front cover or its file has not arrived. The first in track order
if a delivery names two.
"""
coverArt(
"""
A label from artworkSizes, e.g. "tile-600".
"""
size: String!
): Artwork
"""
Release-level dates the delivery asserted — `ReleaseDate`,
`OriginalReleaseDate`, and any sender extension of `EventDateType`.
"""
dates: [ReleaseDate!]!
"""
Availability windows from the deals this release was delivered with —
DDEX's own concept of when a commercial model may sell it, as opposed
to [`Self::available_from`], which is this catalog's own publish gate.
"""
deals: [Deal!]!
}
"""
A release-level date the message asserted.
`dateType` is `ReleaseDate`, `OriginalReleaseDate`, or another value a
sender's own `EventDateType` extension supplies — DDEX publishes no
allowed-value set for this element, so the mapper uses the element name
itself.
"""
type ReleaseDate {
dateType: String!
territory: String
value: NaiveDate!
}
"""
A release's availability state in this catalog, as a filter.
"""
enum ReleaseState {
"""
Being authored; published by nobody yet.
"""
DRAFT
"""
Delivered, waiting for its files.
"""
PENDING
"""
Live.
"""
ACTIVE
"""
Taken down — by a DDEX purge or `withdrawRelease`.
"""
WITHDRAWN
}
"""
One track (or image, or video) as it appears inside a release.
"""
type Resource {
resourcePk: Int!
isrc: String
"""
The DDEX Type value — `MusicalWorkSoundRecording`, `FrontCoverImage`, …
"""
resourceType: String
title: String
durationSeconds: Int
"""
`incoming` until its file lands, then `analyzing` → `stored`. A resource
can legitimately exist before its bytes do — that is the out-of-order
choreography, not an error.
"""
state: String!
byteSize: Int
"""
An opaque value that changes whenever this resource's file is replaced,
for clients to use as a cache version. It is not a content address and
cannot be used to fetch anything. `null` until bytes are stored.
"""
contentVersion: String
"""
Position within its group.
"""
sequence: Int!
technical: Technical
"""
Audio, image, video or other: measured once analyzed, otherwise read
from `resourceType`.
"""
mediaKind: MediaKind!
"""
Parties credited on this recording — performers, producers, writers.
`role` filters case-insensitively; omit it for everyone.
"""
contributors(
"""
Filter by DDEX role, e.g. "Producer". Case-insensitive.
"""
role: String,
"""
Credits applicable in this territory, e.g. "US": rows scoped to it, plus unqualified rows where nothing more specific exists. An explicit Worldwide credit matches only territory: "Worldwide".
"""
territory: String
): [Credit!]!
"""
Every title of this recording, in all languages, scripts and
territories.
The scalar `title` picks one; this is the set it picked from. A Japanese
release routinely carries both `夢のララバイ` and `Yume no Lullaby`, and
which one a client wants is a client decision.
"""
titles(
"""
Filter by text type, e.g. "DisplayTitle".
"""
type: String,
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan" or "ja-Latn". Not a prefix — use `script` to select by script alone.
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan" or "Latn".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [LocalizedText!]!
"""
Phonogram copyright notices for **this recording**.
Distinct from the release's: on a compilation a licensed-in track keeps
the ℗ of whoever owns that recording, so the two genuinely differ.
"""
plines(
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan".
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [Pline!]!
"""
Copyright (©) notices for **this resource** — in ERN, those of artwork,
text and video; a sound recording's © is the release's.
Loaded alongside the ℗ notices since `resource_clines` existed, but not
exposed until the API could write them: a field you can set and not read
back is worse than neither.
"""
clines(
"""
Whole LanguageAndScriptCode, e.g. "ja-Jpan".
"""
language: String,
"""
ISO 15924 script subtag, e.g. "Jpan".
"""
script: String,
"""
Applicable in this territory: rows scoped to it, plus unqualified rows where nothing more specific exists.
"""
territory: String,
"""
Only rows whose IsDefault matches. Note many senders flag none, so `true` can legitimately return nothing.
"""
isDefault: Boolean
): [Cline!]!
"""
Every file of this resource: the delivered original, and each encoding
//SCHEMA has made of it. Lists, never mints — pick one and ask `url`.
"""
encodings(
"""
Only lossless (true) or only lossy (false) files.
"""
lossless: Boolean,
"""
Also list encodings being made (PENDING) or that failed (FAILED).
"""
includePending: Boolean! = false
): [Encoding!]!
"""
A short-lived signed URL for one file of this resource — the one place
a media URL is minted.
The caller always names the file: an encoding family (`aac-256`, which
follows the newest version), a pinned encoding (`aac-256-v1`), or
`original` — the bytes the sender delivered. `clip: true` asks for the
preview clip of a lossy encoding. Lossless files (FLAC, WAV, ALAC, a
lossless original) need `media_lossless` and an organization with
lossless media (enterprise by default); clips and AAC 128 need only
`media_preview`.
`null` when the resource has no file yet — a resource legitimately
exists before its bytes do. Any other `null` comes with an error whose
`code` says why: `ENCODING_PENDING`, `ENCODING_NOT_REQUESTED`,
`ENCODING_NOT_AVAILABLE`, `ENCODING_FAILED`,
`LOSSLESS_NOT_ENABLED`, `UNKNOWN_ENCODING`.
# Why this is safe to sign
`blob_store::signed_get_url` signs whatever it is handed and says so:
content keys carry no catalog, so possession implies no entitlement, and
establishing reachability is the caller's job. That is satisfied
structurally here — a `Resource` can only be built by `build_tree` from
a `ReleaseContents`, which the loader fetches with the principal's own
`catalog_pk`, through a DAL query that re-asserts `releases.catalog_pk`.
There is no path that produces a `Resource` for a release this catalog
was never delivered.
That guarantee got materially stronger on 2026-08-13. While releases
were one global corpus, reachability rested on an availability row that
an *ingest* could create by resolving a UPC — so a mistyped identifier
could grant a catalog access to another's masters. Releases are now
per-catalog (`0001` decision 2), and a row belonging to another catalog
is not merely filtered out, it is unreachable.
The DAL re-asserts it once more: `media::mint` reads the resource back
by `(catalog_pk, resource_pk)` before signing, so a `resource_pk` from
anywhere else finds nothing.
The release's state does not gate a URL (Kevin, 2026-10-23): if the
bytes are here they can be minted, and every mint is logged.
"""
url(
"""
"original", a family such as "aac-256", or a pinned encoding such as "aac-256-v1".
"""
encoding: String!,
"""
The preview clip of that encoding.
"""
clip: Boolean! = false,
"""
Lifetime in seconds; capped by the store.
"""
ttlSeconds: Int,
"""
Have the browser save a named file (Artist - Title.ext) rather than play it.
"""
download: Boolean! = false
): String
"""
This cover at one of your organization's artwork sizes, by its label
(`artworkSizes`). `media_read`, as for the original. `null` while the
resource has no file; an unknown label is an `UNKNOWN_ARTWORK_SIZE`
error listing the organization's labels, and any image but a
`FrontCoverImage` is `ARTWORK_NOT_FRONT_COVER`. See `graphql::artwork`.
"""
artwork(
"""
A label from artworkSizes, e.g. "tile-600".
"""
size: String!
): Artwork
}
"""
One node of the disc/side tree.
Recursive by design: ERN nests `ResourceGroup`s, and flattening the tree
here would lose the distinction between a two-disc album and a one-disc
album with twice the tracks.
"""
type ResourceGroup {
groupPk: Int!
sequence: Int!
title: String
"""
Resources directly in this group, in track order.
"""
resources: [Resource!]!
"""
Nested subgroups.
"""
children: [ResourceGroup!]!
}
type StatusCount {
status: String!
count: Int!
}
"""
What //SCHEMA measured from the file itself — never what the sender
asserted. `null` until the file has been analyzed, usually seconds after it
lands (`resource.analyzed` announces it).
"""
type Technical {
mediaKind: String
container: String
codec: String
bitrateKbps: Int
sampleRateHz: Int
bitDepth: Int
channels: Int
durationMs: Int
lossless: Boolean
integratedLufs: Float
truePeakDbtp: Float
widthPx: Int
heightPx: Int
}
"""
The trial, for an organization that is in one.
"""
type Trial {
startedAt: DateTime
"""
When the first charge falls due — or, once canceled, when access ends
and the organization's data is deleted.
"""
endsAt: DateTime!
"""
Set once canceled. Nothing will be charged; resumable until `endsAt`.
"""
cancelledAt: DateTime
"""
Set once an owner or admin asked to start the subscription now. The
charge follows within seconds; until it lands the organization is still
in its trial.
"""
conversionRequestedAt: DateTime
"""
Whether the caller may cancel or resume: a signed-in owner or admin,
not impersonated. Lets the console show the button only where it works.
"""
mayChange: Boolean!
}
input UpdateArtworkSizeInput {
artworkSizePk: Int!
"""
Renaming never re-renders: renditions are keyed by what is rendered.
"""
label: String
width: Int
height: Int
crop: ArtworkCrop
format: ArtworkFormat
"""
`null` for the default (JPEG) or none (PNG). Left out, it is kept —
unless the format changes, when it resets to that default.
"""
quality: Int
upscale: Boolean
}
input UpdateWebhookEndpointInput {
endpointPk: Int!
"""
A new destination. The signing secret does not change.
"""
url: String
"""
`null` clears it.
"""
description: String
eventTypes: [String!]
"""
`null` for every catalog; omit to leave as it is.
"""
catalogPks: [Int!]
"""
Turn it off or back on. Turning it off fails what was still pending;
turning it on receives events from now, never what it missed.
"""
enabled: Boolean
}
"""
The answer to "I would like to upload these bytes".
"""
type UploadTicket {
"""
True when //SCHEMA already holds these exact bytes — from any catalog,
since the store is content-addressed and global. Nothing needs to be
uploaded; call `completeUpload` straight away.
This is the README's "ingestion appears instant for the second DSP",
surfaced where a client can act on it. Labels reuse one image across a
single, an EP and an album, so it is a common answer for artwork.
"""
alreadyPresent: Boolean!
"""
`null` when `alreadyPresent`.
"""
uploadUrl: String
"""
Headers that **must** be sent with the PUT, exactly as given. They are
inside the signature: altering or omitting one invalidates the URL, which
is what stops a client uploading bytes other than the ones it declared.
"""
requiredHeaders: [HttpHeader!]!
expiresInSeconds: Int!
}
"""
Quantities for one period. No money — see the module doc.
"""
type Usage {
"""
Half-open. `periodEnd` is the first day *not* included.
"""
periodStart: String!
periodEnd: String!
"""
Average bytes delivered and held across the days counted so far, today
included. Storage overage is priced on this plus `bytesEncodedHeld`.
"""
bytesHeld: Int!
"""
What the catalogs hold right now. The figure a person means by "how big
is my catalog"; `bytes_held` is the one they are billed on, and the two
differ whenever the catalog grew or shrank this month.
"""
bytesHeldNow: Int!
"""
Average bytes of the encodings //SCHEMA made of those masters — the AAC
streams and previews, FLAC, requested MP3s — over the same days. Stored
on your behalf, and priced as storage with `bytesHeld`.
"""
bytesEncodedHeld: Int!
"""
The encodings held right now.
"""
bytesEncodedHeldNow: Int!
"""
Bytes delivered during the period, up to now. Asserted, not
deduplicated — the number a customer can verify against their own
records.
"""
bytesIngested: Int!
"""
Complete days with a sample, and days in the period. A figure averaged
over four days is a different claim from one averaged over thirty-one,
and the UI should be able to say so.
"""
observedDays: Int!
periodDays: Int!
"""
When the live figures were measured.
"""
asOf: DateTime!
}
"""
The authenticated caller's own view of itself. The first thing any
integration calls, and the cheapest way to check a key works.
"""
type Viewer {
"""
`api_key` or `user`.
"""
actorType: String!
"""
Present only for human callers.
"""
userPk: Int
orgPk: Int!
catalogPk: Int!
"""
Exactly what this credential may do, so a client can hide what it cannot
use rather than discovering it through errors.
"""
scopes: [String!]!
}
"""
One allowed value of a DDEX vocabulary.
"""
type VocabValue {
"""
Verbatim, exactly as the AVS spells it. Never normalized — //SCHEMA has
to be able to regenerate the literal it stored.
"""
value: String!
"""
True when DDEX defines this value in an allowed-value set. False means
it was `observed` — minted at ingest because a delivery used it and
refusing the delivery was not an option.
"""
ddexDefined: Boolean!
}
"""
One event's delivery to one endpoint: the delivery log.
"""
type WebhookDelivery {
deliveryPk: Int!
"""
The `webhook-id` it was sent with, and the envelope's `id`.
"""
eventId: String!
eventType: String!
occurredAt: DateTime!
"""
`pending` (still being tried), `succeeded` or `failed`.
"""
state: String!
attempts: Int!
lastAttemptAt: DateTime
"""
The HTTP status the receiver answered with, if it answered.
"""
lastStatus: Int
"""
Why the last attempt failed: `http 503`, `timeout after 10s`,
`redirect to …`, `refused: …`.
"""
lastError: String
"""
Up to 4 KiB of what the receiver answered.
"""
lastResponseExcerpt: String
lastLatencyMs: Int
"""
When the next attempt is due, while `pending`.
"""
nextAttemptAt: DateTime
succeededAt: DateTime
}
"""
A destination for this organization's events.
"""
type WebhookEndpoint {
endpointPk: Int!
url: String!
description: String
eventTypes: [String!]!
"""
`null` for every catalog the organization has, including later ones.
"""
catalogPks: [Int!]
enabled: Boolean!
enabledAt: DateTime!
disabledAt: DateTime
disabledReason: String
"""
Since when every delivery has failed, while that is so.
"""
failingSince: DateTime
createdAt: DateTime!
updatedAt: DateTime!
"""
This endpoint's deliveries, newest first. Kept for 30 days.
"""
deliveries(
"""
At most this many, up to 100. Defaults to 20.
"""
first: Int,
"""
Only deliveries older than this `deliveryPk`, for the next page.
"""
before: Int,
"""
Only deliveries in this state: `pending`, `succeeded` or `failed`.
"""
state: String
): [WebhookDelivery!]!
}
"""
One event type an endpoint can subscribe to.
"""
type WebhookEventType {
"""
The wire name, e.g. `release.state_changed`.
"""
type: String!
description: String!
}
"""
Directs the executor to include this field or fragment only when the `if` argument is true.
"""
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to skip this field or fragment when the `if` argument is true.
"""
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Provides a scalar specification URL for specifying the behavior of custom scalar types.
"""
directive @specifiedBy(url: String!) on SCALAR
schema {
query: Query
mutation: Mutation
}