Skip to main content
This is the detailed reference for configuring and governing integrations. For the short version and help choosing an execution path, start with Tools. Checkfu splits external capabilities into four resources: A Tool is something an agent calls and Checkfu runs. A Custom tool is called the same way but runs in your application. A Skill is something an agent reads. They are not variations on one idea.

Connections

A Connection is a Workspace-scoped handle to one provider account. Runtime-backed accounts keep credentials entirely inside the selected IntegrationRuntime adapter; native accounts use Checkfu’s sealed first-party custody. Neither mode exposes credential bytes through Checkfu’s API, events, logs, sandbox, or harness input, and there is no read-back endpoint.

Object model and custody

These are the nouns Connect Center and CapabilityGateway share. A vault, package, snapshot, or placement is never permission. A live supplier offering list is candidate metadata. Checkfu qualification of an exact package and runtime adapter is what makes a row connectable.

Connect Center

Connect Center is the setup surface for Checkfu’s dashboard and embedded customer applications: Your backend sends only credential-free intent. The handoff posts directly to the selected adapter’s trusted origin, so the embedding browser and backend never handle the provider credential. Completion is asynchronous and not pushed to you: there is no connection-lifecycle webhook (webhook fan-out is Session-scoped, and a Connection is a control-plane resource with no session event). When the user finishes the provider’s OAuth or hosted credential entry, Checkfu observes the short-lived ConnectionSession and adopts its immutable Connection revision only after exact runtime authority confirms completion. The browser carries no authority of its own, and a replayed handoff cannot mint a second account. Use the Principal-scoped Integration Gateway session operations—or the SDK Connect Center controller—to begin, observe, replay, reconnect, and revoke without your application handling provider secrets. integrationGateway.listOfferings is Principal-scoped and returns only Checkfu-qualified packages, joined with placement availability and that Principal’s existing accounts. beginConnectionSession accepts the offering, label, return URL, optional placement, and optional narrowing. The package remains the ceiling: a caller may narrow scopes or destinations but cannot widen them.
Reconnect and revoke sessions bind the exact Connection version. Access apply, Bridge revoke, native health checks, and other mutations likewise use optimistic concurrency or idempotency keys; retries never create ambient authority.
Connection reads accept root, admin, developer, and viewer; setup, reconnect, health checks, access changes, and revocation require root, admin, or developer. Runner keys are data-plane credentials and cannot browse Connections. Connections and their owning Principals are private to the selected Workspace: a foreign ID is indistinguishable from a missing ID, and a denied request cannot create, rotate, health-check, or revoke anything.

Native custody diagnostics (advanced)

GET /v1/connection-providers is a lower-level reflection of native custody declarations. It is not the Connect Center catalog and must not be used to build a second provider wall. Runtime-backed apps, qualification, placements, and account joins live only in integrationGateway.listOfferings. The native reflection carries:
  • provider: the native custody provider name used by the lower-level Connection operations.
  • custody_kind: the deployment-declared credential mode of the integration — oauth (interactive connect flow, served by the MCP specification’s own discovery, registration, token, and refresh endpoints), api_key (write-only credential entry through a ConnectionSession), bearer, or mtls (a deployment-held Worker certificate binding activated through hosted setup without exposing certificate bytes).
  • scope_mode and scopes: fixed means the deployment pinned an exact reviewed scope set (shown in scopes) and an authorize request must match it exactly; caller_specified means the authorize request supplies the scopes and scopes is empty.
  • configured: false while custody cannot start setup. An incomplete declaration — an oauth integration missing the MCP server_url it authorizes against, or an mtls integration missing its exact host binding, say — fails the whole custody configuration closed rather than serving half of it, and every declared provider is then listed with configured: false so an embedded wall stays honest.
  • review: the honesty label for how much human review this provider’s catalog entry has had — curated, structural, or unreviewed (D218). See below.
  • tool_count: how many tools the entry’s own ingested source describes, or null where the source publishes no tool list. Every MCP row is null — a registry entry names no tools, so the count is only knowable from a real tools/list on a connected session. Render null as “discovered on connect”, not as zero.
  • connect_defaults: the published package’s reviewed connect declaration — its own description, its reviewed minimum scopes, and its reviewed egress rules — or null when nothing published one for this deployment’s connect flow (D222).
A ProviderPackage is a runtime ceiling, not only catalog metadata. Before custody starts, Checkfu resolves the exact package for the provider and credential flow, verifies its custody identity, requires its minimum scopes, rejects scopes above its allowed ceiling, and accepts only narrower egress rules. The Connection freezes the package name, version, digest, and review tier in provider_package. New authority without an unambiguous package is refused. Rows created before package binding remain visibly unbound with a null provider_package; no public migration operation silently promotes them.
Which providers can be oauth is narrower than it looks. The OAuth path is the MCP specification’s — discovery, dynamic client registration, token, and refresh all derive from an integration’s declared server_url — which is why adding an MCP provider is a catalog row rather than code. A plain, non-MCP provider API has no such standard to ride, and Checkfu ships no per-provider OAuth engine: those integrations connect with a pasted credential (api_key or bearer) instead. Per-provider OAuth for non-MCP APIs is deferred until a customer names one (D215), so a catalog capture whose source publishes only OAuth2 is refused rather than half-supported.
The native reflection is not a second authority: it reports the exact native custody mapping, and lower-level operations still enforce the ProviderPackage ceiling. The legacy interactive authorize operation excludes mTLS because that flow has no authorization URL. The reflection never returns custody identifiers, mTLS binding names, integration keys, connect tokens, or secret material. Use Connect Center to render “connect your apps.” It shows every qualified offering with review, availability, setup flow, placement, destinations, and tool count while keeping supplier coordinates private. The server owns every refusal; clients present its typed unavailable state rather than reimplementing qualification or package ceilings.

What review promises, and what it does not

Breadth and review capacity do not grow at the same rate, so the catalog states which one a row has had rather than implying both (D218): Show the label; do not hide the rows. A weaker label is information your user should have, not a reason to drop a provider from the wall — the same availability-honesty rule the rest of these docs follow, applied to assurance instead of capability. The label also cannot flatter itself: a curated review may only fill in what the structural capture left out, and one that moves a host, a base URL, a tool count, a scope, or a probe outside what the source published is refused. Promoting a provider buys review, never a different provider. The corollary is worth stating because it is what makes the catalog trustworthy: where a provider’s source publishes no auth mode, no scope vocabulary, no absolute server URL, or no parameter-free health probe, the pipeline refuses the entry by name instead of guessing a default. A provider missing from the wall has a published reason rather than a silence.

Health and drift

A Connection reports three independent fields:
  • lifecycle: pending, active, disabled, or revoked. The administrative state.
  • health: pending, healthy, reauthorization_required, or unavailable. Whether it currently works.
  • drift: none, custody_configuration, or capability_refresh. The latter means an IntegrationRuntime discovered a new immutable capability snapshot; it remains quarantined until explicitly adopted, so the admitted predecessor authority does not change.
POST /v1/connections/{id}/health-check refreshes these fields for native custody-backed Connections. Integration Gateway Connections report capability_refresh through their connection workflow and cannot enter that legacy custody mutation path. In both cases, a Connection with any drift reports health: "unavailable" in its control-plane projection. A custody_configuration drift is broken and fails closed. During an Integration Gateway capability_refresh, however, the authority retains the admitted predecessor revision while the new snapshot is quarantined for explicit review. Released-agent selection resolves the exact admitted immutable ConnectionRevision through CapabilityGateway, so the already-pinned predecessor remains callable without silently adopting the quarantined candidate. Health is where a lapsed grant surfaces, because nothing pushes one at you. Custody refreshes an expiring token inline, at the moment a ToolInvocation needs it; a refresh the authorization server answers with invalid_grant marks the grant for reconnect, so that ToolInvocation fails closed immediately, every later ToolInvocation fails closed without presenting the dead token again, and the next health check reports reauthorization_required. Only POST /v1/connections/{id}/reauthorize clears it. Connect Center offers Reconnect for that grant failure and for a genuinely unavailable account; a quarantined capability refresh stays a review of the successor snapshot, not a credential reconnect. Poll the health check on the cadence your product can tolerate rather than waiting for a notification that never comes.

Egress rules

Every Connection carries an explicit, default-deny network boundary. A rule names a host, port, path prefix, and allowed methods. A request that does not match a rule is not sent. The scheme is not a choice: only https is representable. Hosts must be public DNS names. Loopback, .internal suffixes, and IP literals are all rejected at the schema.
allowed_headers is itself default-deny, not an optional extra filter. Every header on a request must appear in the rule’s allowlist, so omitting allowed_headers means no request headers may be sent at all. A rule that forgets to allow Content-Type will not match a request that sends one.
An explicit denylist blocks routing- and authorization-bearing headers (Authorization, Host, custom override headers, and the forwarding-proxy header prefixes), so a tool call cannot smuggle its own credential or redirect the call to a different host. The prefixes belonging to the credential aggregator D215 retired stay on that denylist: relaxing a fence is not a cleanup, and a caller can still try them against a provider that happens to honor them.

How a harness calls one

A Connection is exposed to the harness as its own MCP server at /v1/runtime/mcp/{connection_id}, authenticated by the Run’s short-lived admission token. It is harness-facing rather than customer-facing, so it does not appear in the generated API reference; it is published in the complete OpenAPI document at /api/openapi.complete.json, and you never call it yourself. The harness asks for a tool call by name; Checkfu compiles the outbound request server-side, applies governance and approvals, and returns only the response. For credentialed HTTP and OpenAPI tools, the selected custody boundary attaches the credential at its outbound edge. A public upstream MCP tool bypasses custody and carries no credential to the provider. The endpoint is narrower than “every Connection in the Workspace”. It serves a Connection only when:
  • the Connection is owned by the Run’s acting Principal;
  • its lifecycle is active and its health is healthy.
tools/list is narrower still: it returns only the tools attached to the admitted Agent version that belong to this Connection’s provider, not the Connection’s full discovered surface. This is what “credential-free” means concretely: the agent’s process never holds a secret it could exfiltrate. Credentialed HTTP calls receive their secret only after the agent’s involvement ends; public upstream MCP calls remain credential-free end to end. Authenticated upstream MCP is not part of the v1 public-MCP path.

Who can reach what

Access to a Connection is a use_connection PermissionAssignment and nothing else. Two read projections answer the question from each end, and both derive from the live PermissionAssignment rows on every read — so a PermissionAssignment deleted through any surface disappears from them immediately:
  • GET /v1/agents/{agent_id}/assigned-connections — what this agent can reach, each edge joined with the Connection’s provider, label, lifecycle, health, and owner Principal, plus the PermissionAssignment id, optional time window, and rationale.
  • GET /v1/connections/{connection_id}/assigned-agents — which agents hold this Connection. Deliberately the agent direction only: principal- and group-subject PermissionAssignments on the same Connection (vault member access, below) are not listed here.
POST /v1/agents/{agent_id}/assigned-connections assigns one Connection — or every Connection a vault names at this moment, never a standing subscription — by minting ordinary PermissionAssignments. DELETE .../assigned-connections/{connection_id} revokes every matching PermissionAssignment and is idempotent by construction: unassigning a pair that was never assigned succeeds with an empty revoked_permission_assignment_ids. Revocation takes effect at the next live authorization check, including for a Session already running.
Time bounds are part of PermissionAssignment identity, which produces one surprising case worth knowing before you hit it. Assigning a window beside an existing unbounded PermissionAssignment adds a second row rather than narrowing the first — the unbounded one still authorizes. To time-box access, unassign the unbounded PermissionAssignment.

Vaults: a named grouping, not a second permission system

A ConnectionVault is the “these Connections, these people, these agents” surface a connections builder renders. It is a named, Workspace-scoped grouping whose member access compiles into ordinary use_connection PermissionAssignments, one per member × Connection, each compiled edge naming the exact PermissionAssignment id it produced. The vault row itself grants nothing. No runtime check consults it, and deleting a compiled PermissionAssignment revokes access whatever the vault still lists. Read the vault to render or edit; read the PermissionAssignments for the truth about live access. POST /v1/connection-vaults/apply takes the whole document inline and reconciles: it mints PermissionAssignments for new edges, re-mints any compiled PermissionAssignment deleted elsewhere, and revokes only PermissionAssignments the vault itself compiled for dropped edges. Up to 1024 edges compile in one apply, dry_run=true returns the plan without materializing, and an Idempotency-Key is required. Three behaviors to design around:
  • The compile refuses to adopt. A pre-existing equivalent PermissionAssignment minted outside the vault conflicts rather than being absorbed. Because the apply also revokes, adopting a row it did not create would let a later vault edit destroy an operator’s independent authority — so it declines and you reconcile.
  • Members are PermissionAssignment subjects. A member is a principal or group ref, and a Group member costs one row per Connection while staying live through ordinary group expansion. Membership churn is a Group PATCH, not a PermissionAssignment rewrite.
  • Archiving is not revoking. POST /v1/connection-vaults/{vault_id}/archive soft-fences the row and is reversible by re-applying the same name. It never revokes the compiled PermissionAssignments — withdrawing access is a re-apply with a smaller document, so revocation stays a deliberate, auditable PermissionAssignment change.
The document carries no time window on purpose. A window belongs to one agent’s access to one Connection; a vault’s member access is standing membership, and one clock over the whole document would expire every member at once. Time bounds ride the assignment verb instead.

Tool sources

A ToolSource is a descriptor Checkfu discovers tools from. Three kinds: POST /v1/tool-sources/{id}/sync re-runs discovery and refreshes the Tool rows, updating tool_count, last_sync_at, and status (pending, healthy, or sync_failed). Each discovered Tool carries an input_schema, a schema_hash, and safety_hints. The hints are a struct of three independent booleans, not an enum:
They feed the approval decision, but they do not decide it alone. Governance is consulted first and can deny or require approval regardless of the hints. For an HTTP-bound tool, a ToolInvocation skips review only when the transport is read-only, read_only is true, destructive is false, and requires_approval is false. MCP transport always uses POST, and MCP hints are authored by the upstream server and replaced on every source sync, so they are catalog metadata rather than authorization evidence: an MCP-bound tool never skips review on hints, and only an explicit allow ActionPolicy auto-allows one. A tool marked read_only and requires_approval still goes to a human. See ActionApprovals. GET /v1/tool-sources/{id}/tools returns bounded Tool summaries with each schema hash, safety hints, transport binding, and enablement state. Retrieve one Tool with GET /v1/tool-sources/{id}/tools/{name} when you need its complete input and output schemas. The targeted PATCH returns that same full Tool shape after changing enablement.
PATCH /v1/tool-sources/{id}/tools/{name} takes expected_version and enabled: enable/disable is the only property it controls. Transport bindings are immutable on a Tool by design: the upstream URL comes from the ToolSource descriptor at discovery time, so it cannot be repointed on an individual tool. Permission is a separate concern handled by PermissionAssignments and ActionPolicies.

The two defaults a source carries

Enablement is not only a per-tool PATCH. A ToolSource row carries the postures its tools inherit, so the answer is readable per source rather than inferred from a constant (D216):
  • default_permission is always_ask: a tool from a governed source needs a human on each use until an ActionPolicy says otherwise. Governed means it crosses the CapabilityGateway, which every ToolSource kind does — MCP and HTTP alike — so the default is uniform rather than per-kind. The counterpart is always_allow, the posture an AgentDefinition’s built-in toolset resolves under. That is not a weaker choice about the same boundary: built-ins never reach the CapabilityGateway, so there is no governed decision to soften, and scoping them is the launch surface’s job.
  • default_enabled decides what a newly discovered tool’s enabled starts at. It defaults to true, which is what every existing source keeps: the source admits what it discovers. Pass default_enabled: false at create for default-off subsetting — every tool this source discovers, now and on every later sync, arrives disabled until you enable it by name, so an upstream server that grows a new tool overnight cannot hand an agent a capability nobody reviewed. The posture is fixed at creation, like the source’s name and provider.
A sync never re-decides a tool that already exists. An explicit enable or disable is a review, and re-running discovery does not overwrite one — only a tool seen for the first time takes the source’s default_enabled.
An AgentDefinition’s built-in toolset (agent_toolset) has a field with the same name, and it is a different one. That default_enabled scopes the platform’s built-in tools on one agent; this one decides what a ToolSource’s discovered tools start at. They meet no boundary in common — built-ins never cross the CapabilityGateway.

Searching the catalog

POST /v1/catalog/search ranks the Workspace’s discovered Tools and its Skills against a query string. It takes a query of 1 to 1,024 characters, an optional kinds filter of tool, skill, or both, and an optional limit between 1 and 100. Each result carries its kind, its identity, its description, and a score. A tool result may also carry a stats object: the Workspace’s own reliability history for that exact tool, projected from settled ToolInvocations. It is present only when there is history to report. These are exact counts you divide yourself, and they are observability and ranking input only. Nothing on an enforcement path reads them: a tool with a terrible completion ratio is still called if a PermissionAssignment and ActionPolicy allow it. A result may additionally carry matched_concepts, up to eight names, when the query reached that entry through a mapped Concept rather than by matching its own text. Concepts are the Workspace’s vocabulary over this catalog; they only raise scores, and with none configured this search ranks exactly as text-only search does.

Custom tools

Not every operation belongs to a third-party account. A Custom tool is declared inline on the AgentDefinition with kind: "custom", carrying its own input_schema rather than a discovered one, and your application executes it. Checkfu never does. When the agent calls one, the Run parks in waiting until you post the matching user.custom_tool_result. That is why a Custom tool has no Connection and no ToolSource: there is no platform-held credential, no egress rule, and no outbound request at Checkfu’s edge. The call comes back to your own code, on your own network path, under your own auth.

ActionPolicy-only, and allowed by default

A Custom tool needs no PermissionAssignment. The Session was already admitted to run the definition that declares it, so calling it adds no authority the Session did not already have, and the park itself touches no credential or egress. Checkfu therefore evaluates ActionPolicy alone, and with no ActionPolicy in the way the call proceeds. An active ActionPolicy on that definition’s use_tool permission is what restricts it:
  • deny: the required action fails closed, as a terminal Run failure.
  • require_approval: the call becomes a human decision before your app is allowed to act.

The governed action flow

The escalated path is worth walking through, because the ActionApproval here authorizes you to act, not Checkfu to act on your behalf:
  1. The harness calls the tool, and agent.tool_use records the exact arguments, before any decision.
  2. run.requires_action parks the Run, naming the pending ActionApproval alongside the tool_use_id.
  3. A reviewer approves. Checkfu appends action_approval.resolved and run.action_authorized atomically, moving that same compute-closed Run from waiting on a human to waiting on your result. No Run, dispatch, lease, or Sandbox is created.
  4. Your app executes the call it read from agent.tool_use and posts the exact user.custom_tool_result. That is what creates the one ordinary continuation Run. For a child thread, post through the primary Session endpoint and echo the cross-posted session_thread_id alongside tool_use_id.
Wait for run.action_authorized before executing. A denial does not fail the Run: it ends the wait and continues with policy.denied guidance, so the agent is told the call was refused and carries on. Because approval releases no secret, a Custom-tool ActionApproval carries no proof token to redeem. run.action_authorized in the event log is the durable fact that the decision took effect. For CMA-shaped clients, the reviewer may send user.tool_confirmation to the ordinary Session event endpoint. A child confirmation carries the session_thread_id from the primary agent.tool_use cross-post. allow moves the same wait into its Custom-result phase; it does not authorize the platform to execute customer code or replace the later user.custom_tool_result. ActionApprovals covers what the reviewer sees and how the decision is frozen; Handle an approval walks the responding code.

Skills

A Skill is Workspace-owned content: instructions plus optional files, delivered into the sandbox filesystem. Skills are instructional or executable. They have no input schema, no transport binding, and no approval path, because nothing is being called. Skills follow the Agent Skills directory shape, so they move between systems as ordinary archives:
GET /v1/skills/{id}/export produces a deterministic tar: SKILL.md first, then files sorted by path. POST /v1/skills/import accepts one and stores the SKILL.md text verbatim. Every committed edit creates an immutable version. List them with GET /v1/skills/{id}/versions, read one with GET /v1/skills/{id}/versions/{version}, or export that exact directory from GET /v1/skills/{id}/versions/{version}/export. Session admission pins one of these versions and its content digest, so editing the current Skill cannot change or strand a Session that has already been admitted. Frontmatter written for another tool survives the round trip byte for byte when its name matches the skill. If it does not match, import rejects the archive; if a stored skill carries no matching frontmatter, export synthesizes a fresh block rather than emitting the original. The parser reads flat single-line key: value scalars only, so a description written as a YAML block scalar is not indexed. A SkillSource binds a Git Project as the authoring home so skills can be maintained in a repository and synced. Sync takes expected_version and an acted_as Principal, which must own and hold use_connection on the Project’s Connection. It reports a per-skill outcome of created, updated, unchanged, invalid, or missing. A skill that disappeared upstream is reported as missing, never deleted for you.

Skills that improve themselves

An agent that solves something hard the slow way has learned a skill worth keeping. A SkillProposal turns that trajectory into a reusable skill: authored, verified, and staged for a human to merge. It never delivers an unreviewed skill to a Run: the merge gate is the product. The lifecycle is a job that produces an immutable artifact and adopts it only through the existing SkillSource loop:
  1. Author. POST /v1/skill-proposals names 1–8 source Sessions (the first fixes the task class and its Agent) and a model_routing_profile_key; a platform job reads the trajectories and writes one immutable single-file instructional skill. Its entry is capped at 2560 characters: a verifiability bound, not a skill-size limit (see the next step). Status moves authoring → drafted, or authoring_failed.
  2. Verify. POST /v1/skill-proposals/{id}/verify takes expected_version plus the requesting Principal and replays the task in one isolated child Session that carries the complete proposed skill in its context, graded by an Outcome rubric derived from the original failure. The proposal reserves that exact Session before admission, so an exact retry resumes it instead of launching another. The verdict lands on the proposal; drafted → verifying → verified or verification_failed. Only a verified proposal may submit. (The whole skill rides the graded context rather than a staged skills mount, which is why the entry is bounded.)
  3. Submit. POST /v1/skill-proposals/{id}/submit takes the reviewed expected_version, SkillSource, and acting Principal. It moves verified → submitting, prepares one exact commit, records that commit, and then publishes the bound repo’s checkfu/<id> branch through the governed Connection. Exact retries resume the recorded attempt; a stale or different request conflicts. The branch is the change proposal (no pull request in v1). From here nothing is automatic: a human merges, sync runs, and the ordinary SkillSource → Skill path versions and delivers it. The proposal records the resulting skill version, closing the chain: session → proposal → verification → merge → version → future releases.
POST /v1/skill-proposals/{id}/discard takes expected_version and closes a proposal you do not want. It is available from drafted, authoring_failed, verifying, verified, and verification_failed; authoring, submitted, and already-terminal discarded do not start a new discard. A submitting proposal is discardable only while its reservation has no prepared commit; once a commit is recorded, provider publication may already be in flight and the exact submit must resume. A verification_failed proposal can instead be sent back to verifying and retried. GET /v1/skill-proposals/suggestions/{session} is a read-only structural signal: it reports whether a Session looks skill-worthy (many_tool_calls, or failure_then_success) so a console can offer the “make a skill” button. It never authors anything on its own, and it reads only structural counts, so it is safe under zero-data-retention.
The gate is deliberate. Every shipped self-authoring system we studied stages agent-written skills behind human approval or replay verification; the ungated skill marketplace that did neither grew a measurable malicious-skill supply chain. A verified proposal is a strong suggestion; a merged skill is a decision a person made.

Surfaces that exist only in part

Several shapes commonly assumed to exist do not:
  • A ToolSource has no item-level GET or DELETE. The per-source operations are sync and listing its tools; individual Tools support targeted GET and PATCH.
  • Neither PermissionAssignments nor ActionPolicies have a PATCH. Both are replaced rather than edited.
  • PermissionAssignments and ActionPolicies both have item-level GET and DELETE operations.
  • A SkillSource has a GET but no PATCH or DELETE.

Next steps

ActionApprovals

How a tool call becomes a human decision, and how that decision is frozen.

Agents

How a definition references the tools and skills discovered here.