Get started

Authentication

API keys, profile access, optional restrictions, and how to keep keys safe.

Every request to https://api.cutedyno.com carries an API key as a bearer token:

curl https://api.cutedyno.com/v1/profiles \
  -H "Authorization: Bearer $CUTEDYNO_API_KEY"

There is no OAuth dance for your own access and no session cookie. A key is the whole story. Keys start with cdyn_live_, and CuteDyno stores only a hash — the full value is shown exactly once, when the key is created.

Every new key has full API access. You limit what it can reach with profile access and optional restrictions, not with permission presets.

Create a key

const { key, secret } = await cutedyno.apiKeys.create({
  name: 'Publishing worker',
});

// `secret` is the only time you will see the full key.
console.log(secret);

Mint keys in the dashboard under Developers → API, or with POST /v1/api-keys.

The API & MCP tab, where you create a key and copy the hosted MCP config for Cursor or Claude.
The API & MCP tab, where you create a key and copy the hosted MCP config for Cursor or Claude.

Profile access

Every key has a home profile — the profile it was created in. What varies is how far beyond that it can reach.

  • Omit profileIds and the key can act on every profile in the account. This is the key a platform integration wants.
  • Set profileIds and the key is limited to that list. This is the key you hand to a customer, a contractor, or an agent.
const { secret } = await cutedyno.apiKeys.create({
  name: 'Acme only',
  profileIds: [acme.id],
});

Requests choose their profile with a profileId query parameter or body field. Leave it out and the request lands on the key's home profile, which is why single-tenant integrations never have to think about profiles at all.

// Account-wide key acting on one specific customer.
await cutedyno.posts.list({ profileId: acme.id });

// Same call from a key whose home profile is Acme.
await cutedyno.posts.list();

Asking for a profile the key cannot reach fails with 403 and code profile_not_accessible. That is the same answer whether the profile belongs to somebody else or does not exist, so the API cannot be used to probe for valid IDs.

Note one deliberate restriction: a key with profileIds set cannot mint further keys. Only account-wide keys can call POST /v1/api-keys, so a leaked customer key cannot widen its own access.

Optional restrictions

Three optional limits narrow a key further:

await cutedyno.apiKeys.create({
  name: 'Instagram-only worker',
  profileIds: [acme.id],
  allowedAccountIds: [instagramAccount.id],
  maxPostsPerDay: 10,
  expiresIn: 30,
});
FieldEffect when set
allowedAccountIdsPosts may only target these connected accounts. Anything else returns account_not_allowed.
maxPostsPerDayCaps posts created by this key, resetting at midnight UTC. Over the cap returns daily_post_limit.
expiresInDays until the key stops working. After that, requests return api_key_expired.

Approval is not a key setting. To require a human before posts publish, set requireApproval on the profile policy instead.

Rotation

To rotate without downtime: create the new key, deploy it, confirm traffic has moved by watching lastUsedAt on the old key in GET /v1/api-keys, then revoke the old one with DELETE /v1/api-keys/:id. Both keys work during the overlap. Revocation takes effect on the next request.

Keeping keys safe

  • Never ship a key to a browser, mobile app, or desktop app. All three are readable by users. Proxy through your own backend.
  • Use a separate key per environment and per service, so revoking one thing does not take down everything.
  • Prefer profileIds over handing out account-wide keys when a caller only needs one customer.
  • Audit usage in GET /v1/logs: every authenticated request is recorded with the key that made it, its route, status, and duration.

Authentication errors

CodeStatusMeaning
authentication_required401No Authorization header, or it was not a bearer token.
invalid_api_key401The key does not exist, was revoked, or is malformed.
api_key_expired401The key passed its expiresAt.
insufficient_permission403Valid key, but this action is not allowed — for example a profile-scoped key minting another key.
profile_not_accessible403The key is not scoped to the requested profile.
invalid_profile_id400profileId was sent but was not a string.

None of these are worth retrying — nothing about the outcome will change on a second attempt. Full list in the error reference.