> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloudcruise.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Provider

> Bind vault credentials to items in your own password manager by implementing the Custom Secret Provider API

## **What You Build**

If your credentials live in a password manager that CloudCruise has no built-in integration for, you can put a small HTTPS service in front of it. CloudCruise calls your service at run time to fetch the username, password, and one-time code of the item a [vault](https://docs.cloudcruise.com/concepts/vault.md) credential is bound to. It works the same way as the [1Password integration](https://docs.cloudcruise.com/integrations/1password.md): you rotate or update a login in your password manager, and your workflows use the new value on the next run.

You host the API; CloudCruise calls it.

1. You implement the endpoints below on an HTTPS base URL you control.
2. In CloudCruise you add a **Custom provider** connection and point it at your base URL.
3. You bind vault credentials to items in your password manager by their `ref`, your own item identifier.
4. When a workflow needs the credential, CloudCruise calls `POST /v1/items/resolve` and uses the returned username, password, and TOTP seed.

| Endpoint | Required | Used for |
| - | - | - |
| `POST /v1/capabilities` | yes | Connection test; reports whether you implement item listing |
| `POST /v1/items/resolve` | yes | Fetching the secret values of one item |
| `POST /v1/items/describe` | yes | Showing the bound item's name in the dashboard |
| `POST /v1/items/list` | if `item_listing` | Letting users pick an item instead of typing its `ref` |

All endpoints are `POST` with a JSON body, including read operations. Item references travel in the body, never in the URL, so they are never altered by URL encoding and do not appear in access logs.

The full contract is an OpenAPI 3.1 file you can download to generate server stubs: [custom-secret-provider-api.yaml](https://docs.cloudcruise.com/custom-secret-provider-api/custom-secret-provider-api.yaml). Each operation is also documented under **API Reference → Custom Secret Provider API**.

***

## **Creating a Connection**

1. Navigate to [Workspace Settings → Integrations](https://app.cloudcruise.com/settings/integrations).
2. In the **Secret Providers** section, add a **Custom provider** connection.
3. The dialog generates a random **signing secret** (64 hexadecimal characters, as for [webhooks](https://docs.cloudcruise.com/concepts/webhooks.md)) and shows it **before the connection exists**. You can replace it with your own secret in the same format, generated from a cryptographically secure random source (for example `openssl rand -hex 32`). Prefer one secret per connection.
4. **Deploy the signing secret on your side first.**
5. Enter a connection name, your base URL, and a **Cache duration** (see [Caching and call volume](#caching-and-call-volume)), then click **Connect**.

Clicking **Connect** immediately sends a signed `POST /v1/capabilities` request to your base URL. Your endpoint can only accept it once it knows the signing secret, which is why the secret must be deployed before you connect. If the call fails, the connection is not created.

CloudCruise stores the capabilities your endpoint reports. After you enable item listing, click **Test connection** again so CloudCruise picks it up.

A workspace can have up to 10 custom provider connections. Testing a connection, changing its base URL and browsing its items are limited to a few calls per minute; past that, CloudCruise answers `429` and you can retry shortly.

Then [bind a credential](#binding-credentials) to an item.

***

## **Request Authentication**

Every request is signed with HMAC-SHA256 using the connection's signing secret. Each request carries:

| Header | Value |
| - | - |
| `X-CloudCruise-Timestamp` | Unix time in seconds when the request was signed |
| `X-CloudCruise-Request-Id` | Unique id of this request (UUID). Retries get a new id and a new signature |
| `X-CloudCruise-Connection-Id` | The CloudCruise connection id (UUID, not secret). Use it to pick the signing secret if several connections point at the same endpoint. It identifies the connection, which belongs to one CloudCruise workspace, not the workflow, run or end customer a request is made for |
| `X-CloudCruise-Signature` | `v1=<hex>`, or several comma-separated `v1=<hex>` values during a secret rotation |

The signed string is six lines joined by `\n` (no trailing newline):

```
<X-CloudCruise-Timestamp>
<X-CloudCruise-Request-Id>
<X-CloudCruise-Connection-Id>
POST
<endpoint path, e.g. /v1/items/resolve>
<lowercase hex SHA-256 of the raw request body bytes>
```

The request id and connection id are signed, so a captured request cannot be replayed under a new id or against a different connection.

The endpoint path is the path **relative to your configured base URL**. If your base URL is `https://pm.example.com/cloudcruise`, the request goes to `https://pm.example.com/cloudcruise/v1/items/resolve` and the signed path is `/v1/items/resolve`. Use the literal path of the endpoint your handler implements, not the path as received, so no URL normalization is involved and a gateway that rewrites or strips a path prefix cannot break verification.

`signature = hex(HMAC-SHA256(key = signing secret as UTF-8 bytes, message = signed string))`

**You must:**

* Compute the body hash over the **raw bytes you received**, before any JSON parsing. Re-serialized JSON will not match.
* Accept the request if **any** `v1=` value in the signature header matches, compared in constant time.
* Reject requests whose timestamp is more than **300 seconds** away from your clock, in either direction. Keep your server time synchronized (NTP).
* Serve over HTTPS with a publicly trusted certificate. Plain HTTP and self-signed certificates are rejected by CloudCruise.

Reject **every** failed check with the same `401` response and error code `invalid_signature`: an unknown connection id, a missing, duplicated or malformed header, a timestamp outside the window and a wrong signature alike. Different answers would tell a caller which check failed and which connection ids exist. Verify the signature before you parse the body or act on it.

**We strongly recommend** rejecting a repeated `X-CloudCruise-Request-Id`: remember each id until its timestamp has left the 300-second window, that is, for 300 seconds past its signed timestamp rather than past its arrival, because a timestamp may be up to 300 seconds in the future. This matters most on the read endpoints: a replayed `resolve` returns the secret values to whoever replays it. It is a recommendation rather than a requirement only because it needs a small store of recent ids on a hot path; without it you rely on TLS and the 300-second window. CloudCruise never reuses an id, so this cannot reject a genuine request.

Record an id only **after** its signature and timestamp have verified, and check-and-store it in one atomic step (a set-if-absent with an expiry in your cache or database). An id recorded before verification lets an unauthenticated caller fill your store with random ids. Keep the store bounded.

**You may additionally** restrict access to CloudCruise's egress IPs: `35.164.71.220`, `54.191.200.70`, `18.236.26.110` (the same addresses as [webhook deliveries](https://docs.cloudcruise.com/concepts/webhooks.md#ip-allowlisting)). Do not rely on this alone: the list can change, and CloudCruise announces such changes in advance.

### **Hardening checklist**

* Keep the signing secret in a secret manager or in environment configuration, never in source control. Do not reuse it for anything else, including your webhook secret.
* Never log request or response bodies. `resolve` responses carry passwords and one-time-code seeds. Log the connection id, request id, endpoint and status only.
* Limit how many bytes of the request body you read before hashing it. CloudCruise's requests are small.
* Give each CloudCruise connection its own signing secret, and answer `403 forbidden` for any item that connection should not read, so a connection can only resolve what you intend it to.
* Rotate the signing secret as soon as you suspect it was exposed (see [Rotating the Signing Secret](#rotating-the-signing-secret)).

### **Example verification (Node.js)**

```js theme={null}
import crypto from "node:crypto"

// rawBody is the exact bytes received (a Buffer read with a size limit), not JSON.stringify(req.body).
function verify(req, rawBody, secret, endpointPath) {
  const tsHeader = String(req.headers["x-cloudcruise-timestamp"] ?? "")
  const ts = Number(tsHeader)
  if (!/^\d+$/.test(tsHeader) || Math.abs(Date.now() / 1000 - ts) > 300) return false
  const requestId = String(req.headers["x-cloudcruise-request-id"] ?? "")
  const connectionId = String(req.headers["x-cloudcruise-connection-id"] ?? "")
  if (!requestId || !connectionId) return false
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex")
  const signed = [tsHeader, requestId, connectionId, "POST", endpointPath, bodyHash].join("\n")
  const expected = crypto.createHmac("sha256", secret).update(signed).digest()
  return String(req.headers["x-cloudcruise-signature"] ?? "")
    .split(",")
    .map((part) => part.trim())
    .filter((part) => part.startsWith("v1="))
    .slice(0, 2)
    .some((part) => {
      const given = Buffer.from(part.slice(3), "hex")
      return given.length === expected.length && crypto.timingSafeEqual(given, expected)
    })
}
```

### **Test vector**

Check your implementation against these exact values (ignore the timestamp window for this test):

| Input | Value |
| - | - |
| Signing secret | `4f2b9c1e7a6d4e0f8b3a5c2d1e9f7a6b0c8d2e4f6a1b3c5d7e9f0a2b4c6d8e1f` |
| `X-CloudCruise-Timestamp` | `1767225600` |
| `X-CloudCruise-Request-Id` | `0b7f3c1e-5d2a-4e8b-9f61-2c4d8a7e3b10` |
| `X-CloudCruise-Connection-Id` | `25290e80-bbd5-41b3-861e-dea30cc26e27` |
| Endpoint path | `/v1/items/resolve` |
| Raw body (UTF-8, no trailing newline) | `{"ref":"prod/acme-portal/login"}` |
| Body SHA-256 | `214759033f4faf415a8e46c59a9bcac16492c5ab5a143ba38d1f6bd48fd86c1f` |
| Expected signature | `v1=b0464320ca365095c4f3075e0f78a24bcc20723217346dbcd5bc6002bd6cf2da` |

The same request verified as `/v1/items/describe` must fail; its signature would be `a09211a0cccd4bf4b66f625b70f8f498b33dedb7fb0bed06bf1a6c17cb708369`. Any change to the body bytes, including whitespace, must also fail.

***

## **Item References**

A `ref` is your own identifier for one item: 1 to 512 printable ASCII characters (`0x20` to `0x7E`). CloudCruise treats it as opaque, stores it on the vault credential, and sends it back unchanged. Pick something stable: if an item's `ref` changes, credentials bound to it stop resolving.

### **Binding credentials**

Bind a [vault](https://docs.cloudcruise.com/concepts/vault.md) credential to an item in the dashboard (**Credentials source**, then your connection and the item), or through the [Vault API](https://docs.cloudcruise.com/vault-api/create-vault-entry.md) by passing the connection's `secret_provider_id` and the item's `ref` as `secret_ref`. You can [list your connections](https://docs.cloudcruise.com/vault-api/list-secret-providers.md) to find the connection id. A connection's `provider_type` is `custom_http`.

If you implement `POST /v1/items/list`, users pick an item from a list in the dashboard and [the items are also available through the API](https://docs.cloudcruise.com/vault-api/list-secret-provider-items.md). Otherwise users type the `ref`.

***

## **Two-Factor Authentication (TOTP)**

`totp_seed` is the item's TOTP **secret**, not a one-time code. Return a base32 seed or an `otpauth://totp/...` URI; from a URI, CloudCruise reads only the `secret` parameter. CloudCruise generates codes itself with the standard parameters: SHA-1, 6 digits, 30-second period. Items using other parameters are not supported in this version.

CloudCruise trims the value and removes whitespace. A URI without a `secret` parameter, or a seed that is not valid base32, cannot be used.

Omit `totp_seed` for items without two-factor authentication. Only a workflow step that needs an authenticator code fails, with a reason such as `custom provider returned no totp_seed for <ref>` or `custom provider returned an unusable totp_seed for <ref>: <why>`. The other values of the item are used normally.

See [Two-Factor Authentication](https://docs.cloudcruise.com/concepts/twofactor.md) for the other methods.

***

## **Caching and Call Volume**

CloudCruise caches the resolved values of each credential for the cache duration (default 5 minutes, configurable from 0 to 24 hours on the connection, overridable per credential). The cache is encrypted with the workspace's key. The TOTP seed is cached with the other values; one-time codes are generated fresh every time and never cached.

The cache is cleared:

* for one credential, when its binding or settings are edited,
* for the whole workspace, when connection settings change or a user clicks **Clear cached secrets** in Workspace Settings → Integrations.

`resolve` is called, unless a cached value exists:

* when a run is submitted for a specific credential, to check that it resolves,
* every time a run is handed to a browser, including after a requeue,
* when a user opens the credential in the dashboard, or an API client reads it through the Vault API,
* occasionally while CloudCruise analyses a failed run.

The cache does not depend on whether a run ends up needing to log in: a run that is already signed in through saved cookies still resolves the credential. Typically expect about one `resolve` call per bound credential per cache window; with caching off, expect at least one per run. More calls happen when:

* several runs start at once while the cache is empty; each may call `resolve`,
* a call failed, or returned none of the three values; such results are not cached, so the next run calls again.

`describe` is called each time a user opens a bound credential in the dashboard and when a user binds one. It is not cached; keep it cheap.

### **What CloudCruise holds, and for how long**

* **The cache** is shared by all runs of the workspace, kept per credential, and encrypted with the workspace's key. It never outlives the cache duration.
* **A run** gets the credential's values each time it is handed to a browser and holds that copy while it runs and for up to 30 minutes after it ends. A requeued run is handed to a browser again and picks up the current values. Changing or revoking the item affects runs handed to a browser afterwards (once the cache expires or is cleared), not runs already in progress.
* **When your service is unavailable**, runs keep using a cached value until it expires. After that, a run submitted for a specific credential is rejected, and a run that is already queued fails when it is handed to a browser (temporary failures requeue it). There is no stale fallback beyond the cache duration.
* **A cache duration of 0** means CloudCruise resolves the item for every run. The run still holds its copy as described above, and the browser running the workflow receives the values it has to type.
* **Two separate authorizations.** Your endpoint decides whether CloudCruise may obtain a secret from you. CloudCruise's own access control decides who may use a secret CloudCruise has obtained: runs of the workspace, its members, and its API keys.
* **Revoking access.** Revoking the signing secret or removing the item's access on your side stops CloudCruise from obtaining the secret again; clicking **Clear cached secrets** in CloudCruise removes the cached copies. Neither recalls copies already held by runs in progress, held for up to 30 minutes after a run ends, or read by API clients. Immediate revocation of every copy is not possible; change the password itself if it must stop working.

API keys of a workspace are not restricted to individual operations; any workspace API key can read credentials bound to your provider through the Vault API.

***

## **Timeouts, Retries, and Size Limits**

* Answer promptly. CloudCruise gives each request a few seconds, so a slow answer is treated as a failure.
* On timeouts, connection errors, `429` and `5xx`, CloudCruise retries a limited number of times within a short overall time limit. A `Retry-After` of up to 10 seconds is honoured. It never retries other `4xx` responses.
* Every retry is a new request with a new id and signature, so make your handlers safe to call more than once.
* Responses larger than **64 KiB** (`list`: **5 MiB**) are rejected.

***

## **Network Rules**

* The base URL must be HTTPS and its hostname must resolve only to public addresses. Private, loopback, link-local (including cloud metadata addresses) and other reserved addresses are rejected.
* CloudCruise checks the address it actually connects to on every request, not only when the connection is saved.
* **Redirects are never followed.** Any `3xx` response is treated as an error. Configure the final URL as the base URL.

***

## **Errors**

Return errors as `{"error": {"code": "...", "message": "..."}}`. The message is shown to CloudCruise users as plain text, truncated to 1000 characters and only when the body is a valid error object. It must not contain secret values or details about your infrastructure.

A `2xx` response that is not valid JSON, misses a required property or has a property of the wrong type is treated as a provider error (`invalid_response`) and not retried. Unknown properties are ignored.

A failed `resolve` either rejects the run submission (when the run is submitted for a specific credential) or fails the run when it is handed to a browser. Both report the provider's error, distinct from "credential not found" and from website errors. In the table, "fails" means whichever of the two applies.

| Status | `code` | Meaning | What CloudCruise does |
| - | - | - | - |
| 400 | `invalid_request` | The request is malformed | Fails as a provider configuration error; no retry |
| 401 | `invalid_signature` | Signature, timestamp or secret check failed | Fails as a provider authentication error; no retry |
| 403 | `forbidden` | Signature valid, but this connection may not access the item | Fails as a provider access error; no retry |
| 404 | `item_not_found` | No item with this `ref` | Fails the run with "item not found"; no retry |
| 429 | `rate_limited` | Too many requests | Retried as described above |
| 5xx | any | Your service failed | Retried as described above, then fails the run as "secret provider unavailable" |

A `400` from `capabilities` is also a failure: the connection is not created. Provider errors are reported separately from errors on the website being automated, so an outage on your side never looks like a site failure.

***

## **Rotating the Signing Secret**

1. Add a second signing secret to the connection in CloudCruise (generated, or your own in the same format).
2. While both secrets are active, CloudCruise sends two signatures, comma-separated in `X-CloudCruise-Signature`. A connection has at most two active secrets, so the header never holds more than two `v1=` values; you may reject one that does.
3. Deploy the new secret on your side. Your verification accepts the request if any signature matches, so the old secret keeps working while you deploy.
4. Remove the old secret in CloudCruise.

***

## **Forward Compatibility and Versioning**

The four endpoints described here will not change incompatibly within `v1`. Within `v1`, CloudCruise may add optional request fields, optional response fields it understands, headers, and endpoints, all in backwards-compatible ways. **Ignore request properties and headers you do not recognise.** CloudCruise ignores response properties it does not recognise. Breaking changes are published under `/v2`, and `v1` stays supported alongside it.

The `capabilities` response must always include `item_listing` and `password_write`; `item_search` is optional. `password_write` and `item_search` are **reserved**: return `false` for both in this version.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.