Your team, keys and customers, step by step
For whoever runs your plycdn account: inviting colleagues, creating and replacing API keys without downtime, serving your own customers as sub-organizations, keeping an eye on usage and limits, and reading the audit log. Every route and field is in the reference: Getting and rotating API keys, Managing sub-organizations from code and Your quotas.
You need: a plycdn account you can sign in to. For the parts done from code, a server running one of plycdn's server packages (ASP.NET Core, Node.js or Python) and an admin API key.
1How it works
- People sign in to the dashboard. Everyone you invite has the same access.
- Keys are for your servers. A key is created in the dashboard, shown once, and reaches either one organization or your whole account with all its sub-organizations. Standard keys serve your users' requests; an admin key is for the automation that sets things up.
- Sub-organizations are separate libraries under your account, one per customer, campus or department. You choose which one a call is for with its organization id.
- Usage and limits are counted for the whole account, with limits you can set for each sub-organization. The audit log records who changed what.
2Your team
Account → Team lists everyone who can sign in, with status, last sign-in and join date.
Every member can do the same things: manage the library, settings, connectors and API keys. plycdn has no roles to assign, so invite only people you trust with all of it. Members belong to your account and can work in every sub-organization.
Invite someone. Account → Team → Invite. Enter their work email (name is optional) and choose Send invitation. They get an email with a link to choose a password, valid for 7 days. Until they accept, they show as Invited.
Resend. For a person still Invited, open the actions menu on their row and choose Resend invitation.
Remove. Open the actions menu on a member and choose Remove from team; for a pending invitation it reads Withdraw invitation and the link in the email stops working. A removed member loses access at once, including any session they have open. Their account isn't deleted and nothing they did in the library is undone. You cannot remove yourself, which also guarantees your account always has someone who can sign in.
Everyone signs in with a password, and can also add a passkey (fingerprint, face or a security key): Sign in with a passkey. A sign-in lasts up to 12 hours; Account → Sign-in & security → Sign out everywhere ends it on every device at once, and so does a password reset.
Inviting is limited to 30 a hour. People and invitations are managed in the dashboard only: every API key is refused there with dashboard_session_required.
3Keys: standard or admin
Developers → API keys. Up to two keys can be active at once for each organization, and two for the whole account.
| Standard | Admin | |
|---|---|---|
| Does | Registers and searches content, settings, usage, and storage files, uploads, links, purges, videos | All of that, plus managing sub-organizations, the account audit, creating, changing or removing buckets, video libraries, domains, webhooks and the storage account, live inputs, and observability |
| Goes in | The service that handles your users' requests | Only the automation that sets accounts and buckets up |
| Created | In the dashboard | In the dashboard only, after you confirm I understand what this key can do |
A standard key used for an admin call is refused with admin_key_required (403). Never put an admin key in a browser, an app, or the service your users reach.
Create one. Create key → give it a Name (where it's used, so you recognise it later), choose Access (Standard or Admin), Reach (below) and Expires (Never, 90 days, 180 days or a year) → Create key. Copy it now: it is shown once and cannot be read again, by you or by us. Put it in your server's secret store.
Use it. A request carries the key, and the id of the organization the call is for.
Authorization: ApiKey YOUR_KEY
X-Organization-Id: 00000000-0000-0000-0000-000000000000
The server packages set both for you. Their first argument is the organization ID, shown with Copy at the top of
every dashboard page (Find your IDs). A key made for one organization needs no X-Organization-Id header (next section). A key in the address or in a request body is refused with 401 api_key_required.
Replace a key with no downtime.
- Open Developers → API keys. On the key in use, choose Rotate → Create the new key. A new key of the same access is shown once; the old one keeps working.
- Copy the new key into your secret store and deploy it.
- Watch Last used on the old key. When it stops moving, choose Deactivate on it (the page offers a Finish the rotation shortcut). Requests using it are refused from then on.
A Deactivated key can be brought back with Reactivate. An expired key stops working at its expiry, so replace an expiring key before the date. A third active key for the same reach is api_key_limit_reached: deactivate one first. More than 10 keys created or rotated in an hour is rate_limited. Keys can't be created, rotated or deactivated from code, by any key.
4Find your IDs
Every call in the server packages starts with the organization ID, then names what it acts on: a bucket, a video library, a video, a live input. The dashboard shows each ID in full with a Copy button wherever you see the thing itself, so you never have to read one out of the page address.
| ID | Used as | In the dashboard | From your code or your AI assistant |
|---|---|---|---|
| Organization ID | organizationId (first argument of every call), the X-Organization-Id header |
At the top of every page, next to the organization's name; each sub-organization's in Account → Sub-organizations and in the organization menu | GetAccountAsync, getAccount, get_account; the assistant's whoami |
| Bucket ID | bucketId: sign links, upload, list and purge files |
Storage → Storage buckets, under each bucket's name, and at the top of the bucket's page | ListBucketsAsync, listBuckets, list_buckets; the assistant's list_buckets |
| Video library ID | libraryId: upload videos, sign playback, live inputs |
Video streaming → Video libraries, under each name, and at the top of the library's page | ListLibrariesAsync, listLibraries, list_libraries; the assistant's list_video_libraries |
| Video ID | videoId: playback links, status |
At the top of the video's page | The answer of the upload or video list call |
| Live input ID | liveInputId: publish tokens, go-live checks |
Live streaming → Live streams, and at the top of the live input's page | The answer of the live input calls |
| Storage credential ID | The request log's credential filter |
Developers → Storage credentials, beside each credential | The credential list call |
| Webhook ID | Changing or deleting a webhook | Developers → Webhooks, beside each webhook | The webhook list call |
| Custom domain ID | Changing or deleting a domain | Delivery → Custom domains, and at the top of the domain's page | The domain list call |
On a bucket's or library's Signing tab the organization ID, the bucket or library ID and the key ID are together, and the code sample there already has your IDs in it, so you can copy it as it is.
5Give a key to one organization only
By default a new key reaches one organization, so a service that belongs to one customer, campus or department can never touch another. Choose the whole account only for automation that really serves all of them.
In the dashboard. In the organization switcher, select the organization the key is for (your main organization or one sub-organization). Developers → API keys → Create key, then choose Reach:
- Only {organization name} (the default): the key works for that organization and no other. It needs no
X-Organization-Idheader. - The whole account: {main organization} and every sub-organization: offered only when your main organization is selected. The key works as before, and
X-Organization-Idchooses the organization.
Choose Only an organization for the service that handles one customer's requests, and the whole account for the automation that sets up and administers all of them. Every key you made before Reach existed is a whole-account key and keeps working unchanged.
A key made for one organization:
- needs no
X-Organization-Id. If you send the header, it must name that organization. - never manages sub-organizations or reads the account audit, even an admin key. Those need a whole-account key.
- when it is made for your main organization, sees only the main organization's own data, usage and analytics, never its sub-organizations'.
- when it is made for your main organization, never changes what sub-organizations inherit: the main organization's indexing settings (Content search → Indexing settings), video settings and watch page brand. A key for the main organization only can read these settings but not change them, because sub-organizations inherit them. Change them in the dashboard or with a key that reaches the whole account. A key made for a sub-organization changes that sub-organization's own settings as usual.
Use it without the header.
curl https://api.plycdn.com/api/content/v1/search \
-H "Authorization: ApiKey $PLYCDN_CAMPUS_NORTH_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "enrolment policy"}'
The server packages take the organization id as their first argument and send it for you. Pass the id of the organization the key was made for.
ASP.NET Core
// appsettings: "Plycdn": { "ApiKey": "<the key made for Campus North>" }
var results = await content.SearchAsync(campusNorthId, new ContentSearchQuery { Query = "enrolment policy" });
Node.js
import { PlycdnContentApi } from '@plycdn/content-search-node';
const content = new PlycdnContentApi({ apiKey: process.env.PLYCDN_CAMPUS_NORTH_KEY! });
const found = await content.search(campusNorthId, { query: 'enrolment policy' });
Python
from plycdn_content_search import PlycdnContentApi
content = PlycdnContentApi(os.environ["PLYCDN_CAMPUS_NORTH_KEY"])
found = content.search(campus_north_id, {"query": "enrolment policy"})
Check it works.
- Developers → API keys: the new key's Reach column reads the organization's name (a whole-account key reads Whole account). Use the Reach filter to list one or the other.
- Search with the key and the organization's own id (or, with curl, no header): it answers.
- Search with the same key and the id of another organization of your account:
403 organization_out_of_key_reach.
If something goes wrong.
organization_out_of_key_reach(403): the call names an organization this key was not made for, asks for sub-organizations or the audit, or changes the main organization's settings, video settings or watch page brand with a key made for the main organization only. Use the key made for that organization, or a key that reaches the whole account (or change the settings in the dashboard).organization_forbidden(403): the id is not one of your organizations, or the sub-organization is deactivated.organization_required(400): a whole-account key sent noX-Organization-Id. Send it, or use a key made for one organization.api_key_limit_reached(409): two keys are already active for that organization (or for the whole account). Deactivate one first.
6Check what a key reaches
Before you build on a key, ask plycdn what it is: the organization it acts as, every organization it can name, its prefix and scope (never the key itself), the regions you can create buckets in with their S3 and Azure endpoints, and the products your plan includes. Any key, standard or admin. Reference: What your key reaches.
In the dashboard. Developers → API keys lists each key with its prefix (the first characters, which prefix in the answer repeats), Access and Reach. Account → Profile & organization shows the organization's name and id, and Account → Sub-organizations each sub-organization's.
From code.
curl https://api.plycdn.com/api/content/v1/account \
-H "Authorization: ApiKey $PLYCDN_API_KEY" \
-H "X-Organization-Id: $PLYCDN_ORGANIZATION_ID"
A key made for one organization needs no X-Organization-Id.
ASP.NET Core
AccountSummary account = await storage.GetAccountAsync(organizationId); // storage: PlycdnStorageApi
Console.WriteLine(quot;{account.Organization.Name}: {account.Reach}, key {account.Key?.Prefix} ({account.Key?.Scope})");
Node.js
import { PlycdnStorageApi } from '@plycdn/node/storage';
const storage = new PlycdnStorageApi({ apiKey: process.env.PLYCDN_API_KEY! });
const account = await storage.getAccount(organizationId);
console.log(account.organization.name, account.reach, account.key?.prefix, account.organizations.length);
Python
from plycdn.storage import PlycdnStorageApi
storage = PlycdnStorageApi(os.environ["PLYCDN_API_KEY"])
account = storage.get_account(organization_id)
print(account["organization"]["name"], account["reach"], account["key"]["prefix"], len(account["organizations"]))
Check it works.
key.prefixis how the key you hold begins, andkey.scopematches its Access under Developers → API keys (standardoradmin).- A whole-account key answers
reach: "account"and lists your main organization and every active sub-organization inorganizations. A key made for one organization answersreach: "organization"and lists only that organization. productsnames what your plan includes, such ascontent_search,storageandvideo.
If something goes wrong.
401 invalid_api_key: the key is unknown, deactivated or expired. Create a new one under Developers → API keys.organization_required(400): a whole-account key sent noX-Organization-Id. Send your account's id or a sub-organization's.organization_out_of_key_reach(403): the key was made for one organization and the call names another. Name that organization, or send no header.organization_forbidden(403): the id is not one of your organizations, or the sub-organization is deactivated.
7Serve your own customers as sub-organizations
Each sub-organization has its own files, collections, permissions and search results, which are never shared. A new one inherits your settings until it has its own.
In the dashboard. Account → Sub-organizations → New sub-organization: Name, optional Contact email and Description, and Add label for your own labels (such as region: north). Under Monthly limits set optional caps. Open switches the dashboard to it; the copy button gives its id. Deactivate closes its library without deleting anything, and editing it reactivates it.
From code. You need an admin key that reaches the whole account, and your account's own organization id (a sub-organization can't create others).
ASP.NET Core
var sub = await organization.CreateSubOrganizationAsync(accountId, new PlycdnSubOrganizationRequest
{ Name = "Campus North", ContactEmail = "[email protected]" });
await organization.SetOrganizationQuotasAsync(accountId, sub.Id,
new PlycdnOrganizationQuotasRequest { Searches = 10000, IndexGigabytes = 5 });
Node.js
import { PlycdnOrganizationApi } from '@plycdn/node/account';
const organization = new PlycdnOrganizationApi({ apiKey: process.env.PLYCDN_ADMIN_KEY! });
const sub = await organization.createSubOrganization(accountId, { name: 'Campus North', contactEmail: '[email protected]' });
await organization.setOrganizationQuotas(accountId, sub.id, { searches: 10000, indexGigabytes: 5 });
Python
from plycdn.account import PlycdnOrganizationApi
organization = PlycdnOrganizationApi(os.environ["PLYCDN_ADMIN_KEY"])
sub = organization.create_sub_organization(account_id, {"name": "Campus North", "contactEmail": "[email protected]"})
organization.set_organization_quotas(account_id, sub["id"], {"searches": 10000, "indexGigabytes": 5})
The Content Search packages carry the same calls (PlycdnAccountApi and PlycdnContentApi). The full set is ListSubOrganizations, GetSubOrganization, CreateSubOrganization, UpdateSubOrganization (only fields you send change; IsActive = true reactivates), DeactivateSubOrganization, ListAccountAudit and the quota and statement calls, named per language in SDK-REFERENCE. Per language: ASP.NET Core, Node.js, Python.
Act on behalf of a sub-organization. Pass its id as the organization on any call. With a whole-account key the id chooses (a key made for one organization works only for that organization). A call with a sub-organization's id only ever sees that sub-organization's content:
await content.search(sub.id, { query: 'enrolment policy' });
await content.updateSettings(sub.id, { sourceHosts: ['north.blob.core.windows.net'] });
A deactivated sub-organization is refused with 403 organization_forbidden until reactivated. Account-wide calls (creating sub-organizations, the audit, limits) need your account's own id: with a sub-organization's, organization_forbidden.
Limits for each sub-organization. Searches and written answers a month, and content kept searchable in GB. PUT changes only what you send: a number sets a limit, null removes it, and 0 allows none. Changes apply within 30 seconds. A sub-organization can never change its own limits.
Statements. Your account has one statement, allowance and monthly minimum for the whole account. A key made for a sub-organization sees only its own lines at your list price, and ExportStatementCsv gives its own lines only.
8Usage, quotas and what happens at a limit
Account → Usage has these tabs:
- This month (or the month you pick): what was used, day by day, what it comes to, and a By organization panel when you have sub-organizations. A month still open is a preview; closed months are final.
- Quotas: each monthly ceiling with used, remaining and percent, plus Dashboard searches and, for a sub-organization, Limits for this sub-organization.
- Statements: each closed month's statement, to view or download.
- Processed files: everything processed in the month, file by file, whether or not it was charged.
- Plan: the Your plan panel lists every feature by area (search, storage and video, live streaming, observability): those your plan includes are ticked, and the others are marked on request, with a way to ask for them. Below it, Limits shows the ceilings your plan sets (files, buckets, custom domains, the largest file, the longest video), and, when your plan includes live streaming, Live streaming limits shows its safety caps (streams at once, the longest stream, the tallest picture, the highest bitrate, real-time viewers, inputs, the rewind window and restream destinations). The viewer watermark and player logo are included in every plan.
At a limit. Quotas are soft: work already under way finishes and is charged. At 100% of a quota, new work of that kind (media hours, document volume, searches, answers) is refused with 429 quota_exceeded until the quota is raised or the month resets, unless your account is on pay-as-you-go, where work above quota continues at your list price. Reading, results already returned and playback are never affected. Alerts fire once per threshold per month, typically at 80% and 100%, in the dashboard and by email to your organization's contact address. A limit you set for one sub-organization is a separate quota_exceeded whose detail says "by this sub-organization"; pay-as-you-go never lifts it, and your account and its other sub-organizations carry on. A feature your plan lacks is not_included_in_plan, and a plan ceiling is plan_limit_reached.
From code.
const quotas = await organization.getUsageQuotas(accountId); // plan quotas, used, remaining
const mine = await organization.getOrganizationQuotas(accountId, sub.id); // limits you set, with use
const csv = await organization.exportStatementCsv(accountId, 2026, 9);
GET /api/content/v1/plan answers the same plan: features and limits, with the live safety caps under the names
live_cap_reached gives (live_concurrent_streams and the others) while live streaming is in it. Read one limit by name with
plan.Limit(PlanLimits.LiveConcurrentStreams) (ASP.NET Core), planLimit(plan, PLAN_LIMITS.LiveConcurrentStreams) (Node.js)
or plan_limit(plan, "live_concurrent_streams") (Python); a limit that does not apply is null.
These helpers are in the Content Search packages: PlycdnContentApi.GetPlanAsync with PlanLimits in
Plycdn.ContentSearch.AspNetCore, getPlan with planLimit and PLAN_LIMITS from @plycdn/content-search-node, and
get_plan with plan_limit from plycdn_content_search. With the Storage & video packages only, read GET /plan and its
limits by name.
GetUsageQuotasAsync and the statement calls work with any key. To tell which limit stopped work, read the quotas as the organization concerned. List prices are on Plans and prices; how each quantity is counted is in How usage is counted.
9The audit log and deletion receipts
Two places record what was done:
- Account → Audit log: who changed what in storage and video, newest first: buckets and libraries and their settings, signing keys, uploads, custom domains, videos, webhooks, deletions and takedowns. Filter by From, To, Action and the id of a bucket, library, domain, file, video or webhook. Times are in your account's time zone. Viewing files is counted in usage, not logged here.
- Developers → API keys → Change history: who created, rotated, deactivated or reactivated a key, who changed a sub-organization or its limits (here or from code), who searched as someone else (
dashboard.search_as), who added, renamed or removed a passkey (shown as Added passkey "…", Renamed passkey "…", Removed passkey "…"), and who signed out everywhere (Signed out everywhere); sign-ins with a passkey (Signed in with a passkey) are listed under Show sign-ins. See Sign in with a passkey.
From code (admin key, your account's id), the second is the account audit:
const page = await organization.listAccountAudit(accountId, { limit: 50 }); // page.items, page.next
Pass next as before for the next page. Entries name the person (by email) or the key (by name and prefix) and never hold a key or a value that was changed.
Deletion receipts. When a file is deleted from search, plycdn signs a receipt saying what was removed, when, and when its last copies leave our backups. It appears at the foot of Account → Audit log a few seconds later, with a copy button for the signed receipt and Download. From code: listDeletionReceipts (ListDeletionReceiptsAsync, list_deletion_receipts), with assetId for one file; while a deletion is running the list is empty and pending is true. Keep the receipt with your own records.
Verify a receipt. Anyone can, without your account:
- Read the
kidin the receipt'sjwsheader and take that key fromhttps://plycdn.com/.well-known/plycdn-receipt-keys.json. - Check the EdDSA signature with any library that supports it. Any change to the receipt fails it.
- If the key has a
validUntil, the receipt is valid only when itscompletedAtis not after it.
A short Node.js check is in Proof of deletion. A receipt never holds the file's name or your externalId, only the SHA-256 of the id, so you can show which of your files it is by hashing the id you hold.
10Check it works
- Invite a colleague's email. They show as Invited; when they accept, Active. Withdraw an invitation you don't need.
- Create a standard key, call your server with it, then Rotate it: both work. Deploy the new one, watch Last used on the old key stop, and Deactivate it. Calls with the new key still work.
- Create a sub-organization from code and see it under Account → Sub-organizations. Search with its id and confirm it finds nothing from your own library.
- Set
searches: 1for it, search twice with its id: the second is429 quota_exceeded. Set the limit back withnull. - Developers → API keys → Change history lists each step.
11Questions and problems
admin_key_required (403). Your server holds a standard key for a call that needs an admin key. Create an admin key in the dashboard for the automation that does it.
organization_forbidden (403). The organization id isn't yours or is deactivated, or you called an account-wide route (sub-organizations, audit, limits) with a sub-organization's id. Use your account's own id.
organization_out_of_key_reach (403). The key was made for one organization and the call is for another, or for sub-organizations or the audit, or it was made for the main organization only and the call changes settings that sub-organizations inherit. See Give a key to one organization only.
401 invalid_api_key. The key is unknown, deactivated or expired. 401 api_key_required means no key in the Authorization header; a key in the address or body is never read.
dashboard_session_required. Keys, people and invitations have no API. Do it in the dashboard.
api_key_limit_reached. Two keys are already active for that organization (or two for the whole account). Deactivate one first.
The invite says already_a_member, already_invited or member_of_another_organization. The person is already on your team, or has a pending invitation (resend it), or already has a plycdn account with another organization and must leave it first.
A sub-organization call answers 404 sub_organization_not_found. The id is unknown or belongs to another account.
rate_limited creating sub-organizations. More than 60 an hour; wait and retry.
422 validation_error creating one. fields names the first field at fault: name is 1 to 255 characters, description up to 2,000, and metadata at most 50 keys. A PUT that sends none of the editable fields is also refused.
quota_exceeded (429). See At a limit above. Not retryable until the limit is raised or the month resets.
Nothing in the audit log for something I viewed. Viewing files is counted in usage, not logged.