Authenticate requests
How to authenticate from your code, and how to choose scopes so a leaked key does as little as possible.
Send the key#
Put the key in the Authorization header of every request:
query {
viewer { actorType catalogPk scopes }
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n viewer { actorType catalogPk scopes }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
viewer { actorType catalogPk scopes }
}
`,
}),
});
const { data, errors } = await res.json();Load the key from the environment or a secret manager — never from source code:
const SCHEMA_API_KEY = process.env.SCHEMA_API_KEY;
if (!SCHEMA_API_KEY) throw new Error("SCHEMA_API_KEY is not set");Keep keys on the server#
A key grants everything its scopes allow on its catalog, to anyone who holds it. Call //SCHEMA from your backend and expose only what your app needs. Never ship a key in a web page, a mobile app or a desktop binary.
Choose the narrowest scopes#
Give each integration its own key with only the scopes it uses:
| Integration | Scopes |
|---|---|
| A catalog browser or search page | catalog_read |
| A player | catalog_read, media_read |
| A metadata sync that edits releases | catalog_read, catalog_write |
| An uploader for masters and artwork | catalog_read, catalog_write, media_write |
| An automated DDEX deliverer | catalog_read, catalog_write, media_write |
| Provisioning tooling (keys, members, SFTP accounts) | config_read, config_write |
See the full table of which operation needs which scope.
Discover what a key can do#
Ask viewer at startup and fail fast if a scope you rely on
is missing. viewer.scopes spells scopes CatalogRead, so normalize before
comparing with catalog_read (schema() is the helper defined
below):
const wire = (s) => s.replace(/[A-Z]/g, (c, i) => (i ? "_" : "") + c.toLowerCase());
const { data } = await schema(`{ viewer { scopes } }`);
const held = new Set(data.viewer.scopes.map(wire));
for (const need of ["catalog_read", "media_read"]) {
if (!held.has(need)) throw new Error(`API key is missing ${need}`);
}Handle rejection#
An invalid, revoked or expired key does not produce an HTTP error; the
response carries an error with extensions.code of UNAUTHENTICATED. Check
for it and stop retrying — the key will not start working on its own.
async function schema(query, variables) {
const res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (body.errors?.some((e) => e.extensions?.code === "UNAUTHENTICATED")) {
throw new Error("//SCHEMA rejected the API key");
}
return body;
}Rotate a key#
Keys do not need to expire, but you can give one an expiresAt when you mint
it. To rotate without downtime, see
Manage API keys.