> For the complete documentation index, see [llms.txt](https://docs.concurrence.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.concurrence.com/developer-guide/platform-api/integrations.md).

# Integrations

Connect workspaces to REST APIs with managed credentials, endpoint definitions, and isolated endpoint tests.

Integrations connect a workspace to external REST APIs such as CRMs, scheduling systems, and custom backends. The Platform API integration surface manages the connection, credential configuration, endpoint definitions, and isolated endpoint tests.

The operations below expose the exact request and response contracts. The surrounding guidance focuses on modeling and design choices.

## Key Concepts

* **Connection**: a REST base URL plus optional authentication configuration. The `base_url`, and any `token_url` or `exchange_url`, must be an HTTPS URL on a public host; create and update return `422` otherwise. When a call runs, a host that doesn't resolve to a public address is refused before anything is sent. Calls and token requests do not follow redirects, so a `3xx` response is an error; point each URL at its final address.
* **Endpoint definitions**: child resources that describe callable HTTP operations, input schemas, path/query/body mapping, and response handling.
* **Endpoint IDs**: management operations address endpoint child resources by ID. Human-readable endpoint names remain useful for skill configuration and review.
* **Endpoint testing**: test one endpoint with sample parameters before exposing it to a skill or conversation flow.
* **Approval policies**: integrations can require human approval before write operations, or before every endpoint call, depending on workflow risk.
* **Managed connectors**: MCP-style capabilities are system-provisioned rather than part of the public REST integrations CRUD surface.

## Integration Operations

### Create an Integration

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations" method="post" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### List Integrations

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations" method="get" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Get an Integration

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}" method="get" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Update an Integration

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}" method="patch" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Delete an Integration

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}" method="delete" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

## Authentication

The `auth` object's `type` field selects one of the supported credential schemes. Inline secret values are provisioned to secure storage on creation and never returned in API responses.

| `auth.type`                 | Use case                                                                                                                                                  |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `static_header`             | API key or static bearer token sent in a fixed header. For a bearer token, set `header_name` to `Authorization` and store `Bearer <token>` as the secret. |
| `oauth2_client_credentials` | OAuth2 client-credentials grant.                                                                                                                          |
| `oauth2_jwt_bearer`         | OAuth2 JWT-bearer grant.                                                                                                                                  |
| `custom_token_exchange`     | Per-request bearer token minted from a workspace secret plus dynamic identifiers.                                                                         |

The inline secret is a single sibling `secret_value` field on the create/update body; there are no per-type `*_value` fields. SMART on FHIR Backend Services authentication is not an integration auth type.

### Custom Token Exchange

`custom_token_exchange` covers third-party APIs that require a per-call bearer token derived from a workspace-level secret plus one or more request-scoped identifiers. For example, an integration can combine a workspace credential with a per-end-user identifier to mint a user-scoped bearer token.

The integration-level `secret_value` is referenced in exchange templates with the `{secret}` placeholder. Treat token reuse and caching as implementation details rather than part of the integration contract.

When the token exchange needs the authenticated external-user identity, bind that parameter with `auth.identity_bindings` so the value is resolved from verified session context instead of model-supplied tool input. See [External User Subject-Key Binding](/developer-guide/platform-api/integrations/external-user-subject-key-binding.md).

## Endpoint Definitions

Define endpoints as child resources on the integration. Each endpoint describes one callable operation:

* HTTP method and path relative to the integration base URL.
* Input schema for the arguments callers may provide.
* Mapping from those arguments into path parameters, query parameters, headers, and request body.
* Optional response shaping so the agent sees only the useful result.

Create or update endpoints before wiring them into a skill. Use endpoint tests to validate auth, parameter mapping, response shape, and upstream error behavior without starting a live conversation.

### Endpoint Input Schemas <a href="#endpoint-input-schemas" id="endpoint-input-schemas"></a>

The agent calls an endpoint as a tool, so its `input_schema` must stay in the same tool-schema subset as a [skill input schema](/developer-guide/platform-api/workspaces/skills.md#input-schema-compatibility):

* The root schema must describe a single JSON object.
* Do not use a top-level `$ref`, `anyOf`, `oneOf`, or `allOf`.
* Array-typed properties must declare `items`.

Create and update return `422` with a message naming the unsupported construct. An update that leaves out `input_schema` is not checked. An endpoint stored with an unsupported schema stays stored, but the agent cannot call it until its `input_schema` is reshaped. To express "one of these parameters is required", drop the root composition, keep each parameter optional, and state the rule in the endpoint `description`. The skill-only size and depth limits do not apply to endpoints.

### Endpoint Write Declarations <a href="#endpoint-write-declarations" id="endpoint-write-declarations"></a>

An endpoint's `declarations` object states what its calls do, so the platform does not guess from endpoint or skill names. Its `writes` entry has two parts:

* `kind`: `booking`, `cancellation`, `reschedule`, `message` when a successful call records a message for your team, such as a ticket, a handoff, or a callback request, `other`, `none` when the endpoint never writes, or `varies` when the companion skill that calls it decides. A skill declares that kind in its own `write_kind`.
* `success`: how a response reports that the write happened, required unless `kind` is `none`. Use `{"reported_by": "http_status"}` when any 2xx response is the write, `{"reported_by": "graphql"}` for a GraphQL mutation, which reports the write in its `data` and `errors`, or `{"reported_by": "field", "pointer": "/code", "equals": "0"}` when a response field holds a known value. `equals` is compared as JSON, so `"0"` is not `0`. Under a `field` or `graphql` rule, a 2xx body that answers the rule another way is a successful call that wrote nothing, and a body that doesn't hold what the rule reads leaves the write's outcome unknown; see [Writes Answered With a 2xx](#writes-answered-with-a-2xx). Use `http_status` for a vendor whose success body doesn't report the write.

```json
{
  "declarations": {
    "writes": {
      "kind": "booking",
      "success": { "reported_by": "field", "pointer": "/status", "equals": "booked" }
    }
  }
}
```

<figure><img src="https://1651797362-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2YwM0LHtEGLt9S21OyC3%2Fuploads%2Fgit-blob-7849800f5766fc99fbb26492ee7c01030e0c8bef%2Fbacked-confirmations.svg?alt=media" alt="A tool call backs a done claim only when it succeeded, declares a write, and its response reported the write. The declared kind sets which claims it backs, for the rest of a voice call or within the same turn on text."><figcaption><p>Built-in writes declare their own kind, and <code>world_ticket_create</code> is a message write. When a check catches a reply claiming a write that nothing backs, a correction replaces the rest of the reply on voice and the whole reply on text.</p></figcaption></figure>

The reply checks let the agent say an appointment was booked, cancelled, or moved, or that a message was taken or passed on, only after a write that backs the claim succeeded and its response reported it: anywhere earlier in the call on voice, or in the same turn on [text](/developer-guide/platform-api/conversations.md#reply-check). On voice, a reply that calls a matching write can also promise to do it now. A `booking` write backs a booked or moved claim, a `cancellation` write a cancelled one, a `reschedule` write any of the three, and a `message` write a message claim. An endpoint declared `varies` backs a claim only through the `write_kind` of the companion skill that calls it. An endpoint without `writes`, or one declared `none` or `other`, never backs such a claim, and neither does a call that failed, including one answered 4xx or 5xx, or a write whose outcome is unknown (see [Endpoint Retries](#endpoint-retries)). Built-in writes declare their own kind; `world_ticket_create`, for example, is a `message` write. Create stores only the declarations it sends, and an update merges them by key. The endpoint test response reports `wrote`: whether the declared write happened according to the response. It is null when the endpoint declares no write or declares `none`, and when the call didn't succeed; after an unknown outcome, null doesn't mean nothing was written. Declare `writes` on every scheduling, ticketing, or handoff endpoint.

On text and platform-managed voice, an endpoint declared `message` also lets the agent take a message when someone asks for a person and the conversation can't transfer. When the current action state offers that endpoint, or a companion skill that calls it, or that calls a `varies` endpoint with `write_kind: message`, the agent is told to offer to take a message, record it with that tool, and confirm it only after the write succeeds. If a message already sent through that tool has an unknown outcome, the agent doesn't take another; it is told to say it couldn't confirm the message went through and how to reach the team. See [Runtime Safety](https://docs.concurrence.com/operations-and-safety/runtime-safety#control-map).

### Endpoint Retries <a href="#endpoint-retries" id="endpoint-retries"></a>

Every call to an endpoint follows the same retry rules, whether it comes from a conversation, directly or through a companion skill, from the endpoint test, from the skill tester, or from a trigger action.

* **Reads** are sent again after a status listed in `retry_on_status`, a timeout, a connection that fails before a response arrives, or a 2xx answer cut off before its body ends, up to `max_retries` times.
* **Writes** are sent again after a `429` or `503` that the endpoint lists in `retry_on_status`, or after a connection that never opened, up to `max_retries` times. Those failures mean the vendor didn't act on the request. A write is not retried after a timeout or another error status.

For retries, a call is a write when its endpoint's `writes.kind` is anything but `none`, or, with no `writes` declared, when its method is `POST`, `PUT`, `PATCH`, or `DELETE`. Declare `none` as the `writes.kind` of a `POST` endpoint that only reads to keep the read retries. That declaration doesn't change approval or grants: under `approval_policy: writes` and for role grants, every `POST`, `PUT`, `PATCH`, or `DELETE` endpoint counts as a write, and so does every endpoint that declares a write.

On text, an integration call the reply waits on gets about 20 seconds in all, retries and the waits between them included, so it can end before the endpoint's `timeout_seconds`. The call then fails as a timeout, and a write cut off while its request is out has an unknown outcome and isn't sent again. Background calls, calls a companion skill makes, and voice calls keep the endpoint's `timeout_seconds`, so [run a slow call in the background](/developer-guide/platform-api/conversations/voice-agent.md#execution-model) or through a companion skill. The [endpoint test](#test-an-endpoint) reports `over_text_budget` when a call runs that long.

<figure><img src="https://1651797362-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2YwM0LHtEGLt9S21OyC3%2Fuploads%2Fgit-blob-a73da7e23c5158b35e7178456603484a0ee3b32e%2Fintegration-retries.svg?alt=media" alt="A read is sent again after a listed status, a timeout, a failed connection, or a 2xx answer cut off mid-body, and a write only after a listed 429 or 503 or a connection that never opened. A write that gets no answer settling it, such as a redirect, has an unknown outcome and isn&#x27;t sent again."><figcaption><p>The same rules apply in conversations, endpoint tests, skill tests, and trigger actions. Reconcile an unknown outcome in the destination system.</p></figcaption></figure>

A write whose request went out without an answer that settles it has an unknown outcome: the vendor may have made the write. That happens after a timeout, a dropped or cut-off connection, or a `502`, `504`, `520`, or `524`, the statuses a gateway in front of the vendor returns when the vendor failed or was slow. It also happens after a redirect, which is never followed. The tool result carries `outcome` and a note:

```json
{
  "error": "Request timed out",
  "outcome": "unknown",
  "note": "No answer came back that says whether this request went through, so it may have, and sending it again could do it twice."
}
```

In a conversation, the platform refuses the same write with the same arguments for the rest of the conversation without sending it, and for an endpoint declared `message` it refuses any second message through that tool, however it is worded. The refusal carries `"outcome": "unknown"` too. On text and both voice runtimes, the agent is also told not to try the write again and to tell the person it couldn't confirm the request went through, and how to reach the team. The reply checks never treat the write as done, and a reply the checks catch claiming it was done is replaced with a line saying the agent can't confirm it went through. The checks can miss a phrasing. The skill tester's `sub_tool_logs` and a trigger action's tool results carry the same `outcome` and note, and the [endpoint test](#test-an-endpoint) reports `"outcome": "unknown"`, with an `error` that says the vendor may have made the write.

After any other error status, the write fails, reports no write, and backs no claim. A call refused before anything is sent, for example after an authentication failure or a host that isn't public, fails without a retry.

#### Writes Answered With a 2xx <a href="#writes-answered-with-a-2xx" id="writes-answered-with-a-2xx"></a>

A write the vendor answered with a 2xx is never a plain failure. It is a successful call, with `wrote` set by the endpoint's `success` rule, or its outcome is unknown:

* **`http_status`**: any 2xx is the write, even when the body is empty, isn't valid JSON, or fails the endpoint's `response_template`.
* **`field`**: the rule reads the member the pointer names, in a JSON object or array or a form-encoded body. The `equals` value there is the write, and any other value is a write that didn't happen. A body that doesn't hold the member, such as an HTML page, plain text, an empty `204`, JSON that doesn't parse, or JSON without the field, leaves the outcome unknown.
* **`graphql`**: with no `errors`, the write happened when the mutation's payload holds a value other than null in a field besides its user errors (`userErrors`, `user_errors`, `errors`, or `messages`), `__typename`, and `clientMutationId`. With `errors`, an answer without a `data` member is a request that never ran, and an error at the mutation's own field, with that field null, is the mutation refusing; neither wrote anything. Errors only below the mutation's field, with its payload still holding a result, came after the write, so the write happened. Any other answer with errors, such as a payload nulled by an error below it, or an error with no `path` beside a `data` member, even `"data": null`, leaves the outcome unknown, and so does a body that isn't a GraphQL response, such as a gateway's own JSON.

Under any rule, a 2xx body that can't be decoded as text leaves the outcome unknown. A read whose 2xx body doesn't parse or fails its `response_template` fails, and the agent can call it again.

Make write endpoints idempotent where you can, and check the destination before you retry a failed or unknown write yourself; see [Verify an Integration Action](/developer-guide/guides/verify-an-integration-action.md).

### Create an Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints" method="post" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### List Endpoints

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints" method="get" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Get an Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints/{endpoint\_id}" method="get" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Update an Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints/{endpoint\_id}" method="patch" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Delete an Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints/{endpoint\_id}" method="delete" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

### Test an Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/integrations/{integration\_id}/endpoints/{endpoint\_id}/test" method="post" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

The test calls the real endpoint with the `params` you send and follows the [retry rules](#endpoint-retries) a conversation does, so a write that times out goes out once. The response reports `retries`, `wrote`, and `outcome`, which is `succeeded`, `failed`, or `unknown`. `succeeded` means the vendor answered with a 2xx, and `wrote` then says whether a declared write happened under its `success` rule. `failed` means the call failed and nothing says the vendor acted on it. `unknown` means a write went out and no answer says whether it went through, so check the vendor before you send it again. When a write's 2xx body didn't hold what its `field` or `graphql` rule reads, `status_code` and `raw_response` show the answer that came back. `error` explains a failed or unknown call, and it is also set on a write that succeeded when its `response_template` failed, or when its body didn't parse and no `field` or `graphql` rule reads it, so read `outcome` rather than treating any `error` as a failure. For an HTTP failure, `error` reads `HTTP <status> from <url>` with the full request URL, which only the admin who sent the arguments sees.

The test waits as long as the endpoint's `timeout_seconds` and retries allow, while a text turn gives up on a call after `text_budget_ms`, 20 seconds with retries and the waits between them included. `over_text_budget` is `true` when the call's `duration_ms` reached `text_budget_ms`: the call can pass here and still time out in a text conversation. Voice calls, background calls, and a companion skill's calls keep the endpoint's `timeout_seconds`.

Everywhere else, a failed call's error reads `HTTP <status> from <origin>`, `Request timed out`, or the name of the connection error, where the origin is the scheme, host, and any port. It leaves out the request path, the query, and the vendor's response text. A configuration problem, such as a redirect or a missing authentication parameter, keeps its own message. This is the error the agent reads and a conversation's tool call records. The start of the vendor's response body reaches the agent separately, as `detail`. Use the endpoint test when you need the full request. A failed call, including one answered 4xx or 5xx, reports `succeeded: false` in the conversation's tool calls, and on text and platform-managed voice it doesn't trigger `navigate_on_completion`.

## Approval Policies

Use approval policies when an integration call can create, update, delete, or otherwise affect external records. A write-gated policy routes write operations through human approval before execution, including a `GET` endpoint that declares a write and a `POST` endpoint declared `none`; an all-gated policy requires approval for every endpoint call. Treat a missing approval policy value as no approval required. Approval happens only in text conversations.

See [Approval-Gated Integration Writes](/developer-guide/platform-api/integrations/approval-gated-integration-writes.md) for the approval lifecycle, channel coverage, and guidance on deciding what to gate.

## Usage with Skills

Skills reference integration endpoints through `integration_tools`, giving the skill's LLM access to selected external API operations during execution:

```json
{
  "slug": "check-appointments",
  "integration_tools": [
    { "integration": "scheduling", "endpoint": "list_slots" }
  ]
}
```

Keep endpoint input schemas narrow and explicit. The agent uses the schema to decide when to call a tool and how to populate arguments; overly broad schemas increase the chance of malformed upstream requests.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.concurrence.com/developer-guide/platform-api/integrations.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
