Skip to content

Provider Keys (BYOK) & Skills

Copy page

Two setup flows that other pages point at:

  • BYOK — your own credentials for a third-party provider (OpenAI, Deepgram, ElevenLabs, Anthropic…), stored per workspace. Needed before a Release can use a provider Divinci does not front with its own key.
  • Skills — instances of Divinci’s built-in integrations (calendar, email, SMS, web search). A Release’s tool loop attaches them by instance id, so the instance has to exist first.

They are independent — most skills need no BYOK — but they are both “do this before the thing you actually wanted to configure”.

A BYOK record is one credential for one provider, scoped to one workspace. 31 of the 33 providers accept one (the exceptions are divinci and redis, which are platform infrastructure).

// The credential's shape is per provider — openai wants apiKey (+ optional
// organization), others want different fields entirely.
const key = await divinci.byok.create(workspaceId, {
name: "Production OpenAI",
providerId: "openai",
auth: { apiKey: "sk-...", organization: "org-..." },
});
const { page, total } = await divinci.byok.list(workspaceId, { providerId: "openai" });
await divinci.byok.update(workspaceId, key._id, { available: false }); // metadata only
await divinci.byok.delete(workspaceId, key._id);

Rotation replaces the credential in place, keeping the record’s id:

await divinci.byok.rotate(workspaceId, key._id, {
auth: { apiKey: "sk-new-value..." },
});

Nothing that references the key needs re-pointing — RAG vectors, releases, and tools all hold the BYOK id, not the secret. Use rotate() rather than delete-and-recreate, which would orphan every consumer.

The stored credential is encrypted at rest and never comes back from the API. A record returns metadata only: name, providerId, available, timestamps, and a keyPreview (a masked fragment, enough to tell two keys apart). There is no endpoint that reads a workspace provider key back out.

A tool declares which provider it needs, and whether it can use Divinci’s key:

useBYOKMeaning
unsetEither Divinci’s platform key or yours
"divinci-only"Platform key only — BYOK is ignored
"byok-only"Your key is mandatory; the tool cannot run without one

Around seventeen tools are byok-only today — chiefly the Vertex-hosted third-party models (Grok, Qwen, Kimi, GLM, Jamba) and the Vertex Gemini fine-tuners. Selecting one of these without a matching BYOK record leaves it non-functional. For voice specifically, the Cloudflare-hosted TTS/STT options run on the platform key while OpenAI Whisper, Deepgram, ElevenLabs and Cartesia bill to yours — see Channels & Access → Voice.

A catalog integration is the Divinci-authored thing (google-calendar, web-search). A skill instance is your configured copy of it. Releases attach instances, never catalog entries.

// 1. See what exists
const catalog = await divinci.skills.listCatalog();
// → [{ id: "google-calendar", label, authType: "oauth2", actions: [...], available }]
// 2. Create your instance
const skill = await divinci.skills.create({
integrationId: "google-calendar",
title: "Clinic bookings",
instanceDescription: "Books 30-minute consults on the clinic calendar",
});
// 3. skill.id is what a Release attaches

Each catalog entry advertises its authType (oauth2 / api-key / basic), its actions with their JSON Schemas, whether each action mutates, and an available flag — an entry with available: false is present in the catalog but not runnable.

Three calls, with your redirect in the middle:

const { authorizeUrl, state } = await divinci.skills.startConnect(
skill.id,
"https://yourapp.example.com/oauth/callback",
);
// send the user to authorizeUrl; they come back with ?code=...
await divinci.skills.finishConnect(skill.id, {
code,
state, // the state from startConnect
redirectUri: "https://yourapp.example.com/oauth/callback", // must MATCH exactly
});
await divinci.skills.disconnect(skill.id); // revoke later

The redirectUri passed to finishConnect must be identical to the one given to startConnect — providers bind the authorization code to it, and a mismatch fails the exchange.

Read connection state off the instance:

connectionMeaning
connectedReady
expiredWas connected; token needs refreshing — reconnect
disconnectedNever connected, or revoked
not-requiredThis integration needs no auth

toolOverrides adjusts individual actions without forking the integration:

await divinci.skills.update(skill.id, {
toolOverrides: {
bookAppointment: {
confirmationMessage: "Book {dateTime} for {durationMinutes} minutes?",
requireConfirmation: true,
},
},
});

requireConfirmation: true adds a confirmation stop to an action that would not otherwise have one — useful for the self-directed actions (email-release, sms-release) that skip the automatic floor. It cannot remove confirmation from a mutating action; that floor is enforced server-side.

Attaching is a Release update, not a skills call:

await divinci.releases.updateInWorkspace(workspaceId, releaseId, {
skillConfig: {
enabled: true,
catalogSkills: { [skill.id]: ["bookAppointment", "getAvailabilities"] },
},
});
// read back what a release has attached
const attached = await divinci.skills.listForRelease(releaseId);

Full loop configuration — the tool-calling model, iteration caps, MCP skills, and the confirmation model — is on Assistant Tools.

  • BYOK is per workspace; skills instances are per workspace. Cloning a Release into another workspace carries the ids but not the records they point at. Re-create both on the destination side.
  • available: false on a BYOK record does not delete it. It withdraws the key from selection while keeping it (and its id) intact — the reversible way to take a credential out of service.
  • A skills catalog entry is not a promise. available: false means present but not runnable; check it before building a flow around an integration.
  • email-user cannot be a workspace instance. It resolves recipients from the acting user’s identity, so it is refused on the release side and exists only as a user-owned skill. Release-owned emailing is email-release.