Authentication
Every call carries a workspace API key. A key belongs to one workspace and acts with the permissions of one role in it, and no more.
Create a key
In the web app, open Settings, then the API keys tab. Choose a label, a role and, if you want one, an expiry date. The key is shown once; copy it then.
You can also create one with the SDK. The calling key needs the api_keys.create permission, and listing roles needs roles.read.
const { roles } = await pd.roles.list({ workspaceId });
const writer = roles.find((role) => role.name === "Writer");
if (writer === undefined) throw new Error("no Writer role in this workspace");
const { apiKey, key } = await pd.apiKeys.create({
workspaceId,
body: { label: "Site sync", roleId: writer.id, expiresAt: "2027-01-01T00:00:00.000Z" },
});
console.log(apiKey.prefix, key); // `key` is returned only by this call
A key looks like pd_live_ followed by 43 characters. PrismDraft stores a hash of it, so it cannot be shown again. pd.apiKeys.list returns the first 16 characters (prefix), the role (roleId, null once the role is deleted), lastUsedAt, expiresAt and revokedAt, never the key.
Use a key
import { PrismDraft } from "@prism-draft/sdk";
const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });
The SDK sends the key as Authorization: Bearer <key>. It does not read environment variables itself, so pass the key in. An empty or whitespace-only apiKey throws a TypeError when you construct the client, which catches an unset environment variable before it turns into a 401.
Without the SDK, send the same header:
GET https://api.prism-draft.com/api/articles?workspaceId=<workspace id>
Authorization: Bearer pd_live_…
A missing, unknown, revoked or expired key answers 401 with the code UNAUTHORIZED. See Errors.
What a key can do
A key has the permissions of its role, named resource.action (articles.publish, categories.update). A workspace starts with these roles; it can edit them or add custom ones with pd.roles.create.
| Role | Permissions |
|---|---|
| Owner | Every permission. |
| Admin | Every permission except workspace.delete and credits.adjust. |
| Editor | Creates, generates, edits, approves, rejects and publishes articles; manages categories and images. |
| Writer | Creates, edits and generates articles. Cannot approve, reject or publish. |
| SEO Manager | Edits articles and their SEO metadata, approves and rejects, manages categories. |
| Viewer | Reads content, and comments on articles. Cannot read billing, credits or integrations. |
Read the exact list a key holds from the workspace:
const { permissions } = await pd.workspaces.get({ workspaceId });
if (!permissions.includes("articles.publish")) {
console.log("this key cannot publish");
}
A call the key's role does not allow answers 403 with the code FORBIDDEN, and error.details.permission names the permission it lacked. Give each integration its own key with the smallest role that works; a read-only integration needs only articles.read, which pd.roles.create can grant in a custom role.
A key is never a member of a project. A project whose access has been limited to named members answers a key with 403 (You are not a member of this project), and article lists leave its articles out. Use a user's session in the web app for those projects.
The reference pages say which permission each method needs.
One workspace
A key works in the workspace it was created in. Any other workspace answers 404 with RESOURCE_NOT_FOUND, the same answer as for a workspace that does not exist, so a key cannot be used to probe other workspaces.
The workspace id is the id after /w/ in the web app's address. A key created by a person can also list the workspaces that person belongs to, and pd.workspaces.get succeeds only for the key's own:
const { workspaces } = await pd.workspaces.list();
for (const workspace of workspaces) {
console.log(workspace.id, workspace.name);
}
pd.workspaces.list and pd.workspaces.create act for a person, so they need a key created by a signed-in user. A key created with another key has no creator: they answer 401, createdBy: "me" filters answer 400, and the articles it writes have createdBy: null.
Expiry, revocation and rotation
expiresAtis optional. After it passes, the key answers401.pd.apiKeys.revoke(needsapi_keys.revoke) disables a key at once. Revoking a key that is already revoked answers404.pd.apiKeys.rotate(needsapi_keys.createandapi_keys.revoke) issues a new key with the same label, role and expiry, and revokes the old one in the same transaction. The old key stops working immediately, and the new key is returned once. An expired key cannot be rotated: create a new one. The same goes for a key whose role was deleted (roleIdisnull; it answers401and rotating it answers409).
To change a key without downtime, create a second key, deploy it, then revoke the first:
const { apiKeys } = await pd.apiKeys.list({ workspaceId });
const old = apiKeys.find((candidate) => candidate.label === "Site sync");
if (old === undefined) throw new Error("no key labelled Site sync");
// A key whose role was deleted has `roleId: null`; it cannot be used and has nothing to copy.
if (old.roleId === null) throw new Error("the Site sync key has lost its role");
const created = await pd.apiKeys.create({
workspaceId,
body: { label: "Site sync (2027)", roleId: old.roleId },
});
// Deploy `created.key`, then take the old key out of service:
await pd.apiKeys.revoke({ workspaceId, keyId: old.id });
Rate limits
The API allows 300 requests per minute per key by default, and a workspace can have a monthly cap on API requests. Both answer 429. Retries and timeouts explains how the SDK treats each.
The public blog
pd.blog.listPosts, pd.blog.listCategories and pd.blog.getPost read PrismDraft's own public blog and need no valid key, though the constructor still wants a non-empty apiKey.