> For a complete page index, fetch https://docs.conduit.xyz/llms.txt

# Build a Native Integration

> Build a native integration for the Conduit Marketplace. Covers the integration lifecycle, webhook events, status responses, OAuth account linking, and the TypeScript SDK.

Conduit operates a Marketplace where chain admins can add apps and tooling to
their chain directly from the Conduit platform. By listing your product as a
native integration, you get direct distribution to hundreds of Conduit chains:
admins install your integration with one click, and manage its settings through
your interface.

During installation, Conduit sends your service a webhook event. Use it to
start indexing, deploy smart contracts, or request additional configuration
from the customer.

> **Note**
>
> The API is subject to change as Conduit collects feedback from partners.

## Integration lifecycle

1. You give Conduit a webhook URL, and Conduit shares an integration ID and a
   secret with you.
2. When a customer installs your integration, Conduit sends an
   `IntegrationEvent` with `event=INSTALLED` to your webhook URL, with the
   secret as a header so you can verify the request came from Conduit.
3. If your integration links accounts, Conduit starts the OAuth 2.0 flow before
   sending the event, then sends the same event with an
   `Authorization: Bearer <TOKEN>` header.
4. You respond with the installation status: installed, installing, or
   configuration required.
5. When the customer uninstalls the integration, Conduit sends an
   `IntegrationEvent` with `event=UNINSTALLED`.

## List your integration

Conduit currently curates all integrations. To get your integration listed,
provide:

* Name of the integration
* Logo to display to customers
* Banner image to display before the description
* Short blurb that describes the integration in one sentence
* Long description in Markdown for the integration details page
* Author of the integration
* Category of the integration: DevTools, indexers, oracles, and so on
* Website URL
* Docs URL
* (Optional) OAuth 2.0 endpoint to link accounts

Conduit shares with you:

* An integration ID, used for the OAuth login flow
* An integration secret, included in API calls and the OAuth login flow

## Verify webhook requests

Conduit authenticates webhook requests in one of two ways, depending on how
your integration is configured:

* **Secret header** (default): each request includes the integration secret in
  the `X-Conduit-Integration-Secret` header. Compare it against your secret to
  confirm the request came from Conduit.
* **Signature**: if a signature secret is configured for your integration,
  each request instead includes the integration secret in the
  `X-Conduit-Integration-Api-Key` header and a signature in the
  `X-Conduit-Integration-Signature-256` header. The signature is `sha256=`
  followed by the hex-encoded hash-based message authentication code (HMAC),
  computed with SHA-256 over the raw request body and keyed with your signature
  secret. Compute the HMAC over the body exactly as received, before any JSON
  parsing.

If your integration links accounts, requests also include an
`Authorization: Bearer <TOKEN>` header with the OAuth access token.

## Event payload

The event payload is a JSON object with the following fields. Conduit omits
fields with empty or default values from the JSON, so treat missing fields as
unset.

| Field                     | Type             | Description                                                                                                                                                                       |
| ------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`                   | string, required | Event type: `INSTALLED`, `UNINSTALLED`, or `NETWORK_DELETED`. `NETWORK_UPDATED` is reserved in the schema but is not currently emitted by Conduit, so don't rely on receiving it. |
| `id`                      | string, required | A unique identifier for the network: the network slug, for example `example-chain-a1b2c3d4e5`. Use it when calling the public integration endpoints.                              |
| `installation_id`         | string           | Installation ID for the integration.                                                                                                                                              |
| `chain_id`                | string           | The chain ID of the network. Sent on `INSTALLED`, `UNINSTALLED`, and `NETWORK_DELETED`.                                                                                           |
| `parent_chain_id`         | string           | The chain ID of the parent (settlement) chain. `INSTALLED` only.                                                                                                                  |
| `type`                    | string           | Stack type: `OPTIMISM` or `ARBITRUM`. `INSTALLED` only.                                                                                                                           |
| `native_currency`         | object           | The network's native currency: `name`, `symbol`, `decimals`, and `contract` (the contract address on the settlement layer). `INSTALLED` only.                                     |
| `name`                    | string           | The name of the network. `INSTALLED` only.                                                                                                                                        |
| `rpc`                     | string           | The RPC endpoint URL for the network. `INSTALLED` only.                                                                                                                           |
| `ws`                      | string           | The WebSocket endpoint URL for the network. `INSTALLED` only.                                                                                                                     |
| `explorer`                | string           | The URL of the network's block explorer. `INSTALLED` only.                                                                                                                        |
| `logo_url`                | string           | The URL of the network's logo, when set. `INSTALLED` only.                                                                                                                        |
| `icon_url`                | string           | The URL of the network's icon, when set. `INSTALLED` only.                                                                                                                        |
| `brand_color`             | string           | The brand color of the network, when set. `INSTALLED` only.                                                                                                                       |
| `contracts`               | object           | A mapping of contract names to their addresses. `INSTALLED` only.                                                                                                                 |
| `private`                 | boolean          | Whether the network is private. Hide private networks in your app. Omitted when false. `INSTALLED` only.                                                                          |
| `network_type`            | string           | `MAINNET` or `TESTNET`. `INSTALLED` only.                                                                                                                                         |
| `arguments`               | object           | Configuration inputs the customer submitted during installation, as a map of string keys to string values. Omitted when the integration has no configuration. `INSTALLED` only.   |
| `file_optimism_contracts` | string           | The file URL for OP Stack contracts. `INSTALLED` only, OP Stack chains.                                                                                                           |
| `file_optimism_genesis`   | string           | The file URL for the OP Stack genesis file. `INSTALLED` only, OP Stack chains.                                                                                                    |
| `file_optimism_rollup`    | string           | The file URL for the OP Stack rollup configuration. `INSTALLED` only, OP Stack chains.                                                                                            |
| `file_arbitrum_core`      | string           | The file URL for Arbitrum core contracts. `INSTALLED` only, Arbitrum chains.                                                                                                      |
| `file_arbitrum_chaininfo` | string           | The file URL for Arbitrum chain information. `INSTALLED` only, Arbitrum chains.                                                                                                   |

Fields other than `event` and `id` are optional and may or may not be present.
Conduit might add new fields in the future, so make sure your JSON processing
handles unknown fields.

Here's an example installation event:

```json
{
  "event": "INSTALLED",
  "id": "example-chain-a1b2c3d4e5",
  "chain_id": "424242",
  "parent_chain_id": "11155111",
  "type": "OPTIMISM",
  "native_currency": {
    "name": "Ether",
    "symbol": "ETH",
    "decimals": 18,
    "contract": "0x0000000000000000000000000000000000000000"
  },
  "name": "Example Chain",
  "rpc": "https://rpc-example-chain-a1b2c3d4e5.t.conduit.xyz",
  "ws": "wss://rpc-example-chain-a1b2c3d4e5.t.conduit.xyz",
  "explorer": "https://explorer-example-chain-a1b2c3d4e5.t.conduit.xyz",
  "logo_url": "https://cdn.example.com/example-chain-logo.svg",
  "icon_url": "https://cdn.example.com/example-chain-icon.svg",
  "brand_color": "#6C4BF4",
  "file_optimism_contracts": "https://api.conduit.xyz/file/v1/optimism/contracts/example-chain-a1b2c3d4e5",
  "file_optimism_genesis": "https://api.conduit.xyz/file/v1/optimism/genesis/example-chain-a1b2c3d4e5",
  "file_optimism_rollup": "https://api.conduit.xyz/file/v1/optimism/rollup/example-chain-a1b2c3d4e5",
  "installation_id": "11111111-2222-3333-4444-555555555555",
  "network_type": "TESTNET",
  "arguments": {
    "example_configuration_key": "example value"
  },
  "contracts": {
    "AddressManager": "0x1111111111111111111111111111111111111111",
    "BondManager": "0x0000000000000000000000000000000000000000",
    "CanonicalTransactionChain": "0x0000000000000000000000000000000000000000",
    "L1CrossDomainMessenger": "0x2222222222222222222222222222222222222222",
    "L1StandardBridge": "0x3333333333333333333333333333333333333333",
    "L2OutputOracle": "0x4444444444444444444444444444444444444444",
    "OptimismPortal": "0x5555555555555555555555555555555555555555",
    "StateCommitmentChain": "0x0000000000000000000000000000000000000000"
  }
}
```

The uninstall event contains the network slug, the installation ID, and the
chain ID. Conduit sends `NETWORK_DELETED` instead of `UNINSTALLED` when the
customer deletes the network itself:

```json
{
  "event": "UNINSTALLED",
  "id": "example-chain-a1b2c3d4e5",
  "chain_id": "424242",
  "installation_id": "11111111-2222-3333-4444-555555555555"
}
```

## Respond to events

Your webhook responds to each event with a JSON object with the following
fields:

| Field                        | Type             | Description                                                                                                                                                     |
| ---------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                         | string, required | The identifier for the network.                                                                                                                                 |
| `status`                     | string, required | The status to update to: `NOT_INSTALLED`, `INSTALLING`, `INSTALLED`, `UNINSTALLING`, `FAILED_TO_INSTALL`, or `CONFIGURATION_REQUIRED`.                          |
| `configure_integration_link` | string           | The URL where the customer configures the integration. Required for `CONFIGURATION_REQUIRED`.                                                                   |
| `eta_seconds`                | number           | The estimated time until installation completes, in seconds. Applies to `INSTALLING`.                                                                           |
| `manage_integration_link`    | string           | The URL where the customer manages the integration. Applies to `INSTALLED`.                                                                                     |
| `failure_reason`             | string           | A failure reason. Only valid with `FAILED_TO_INSTALL`.                                                                                                          |
| `artifacts`                  | array            | A list of artifacts to render on the dashboard. Each artifact has a `name`, `key`, `type` (`string`, `address`, or `url`), and `value`. Applies to `INSTALLED`. |

Conduit validates your response:

* A `CONFIGURATION_REQUIRED` response must include
  `configure_integration_link`.
* A `failure_reason` is only accepted with status `FAILED_TO_INSTALL`.
* For `UNINSTALLED` and `NETWORK_DELETED` events, respond with status
  `UNINSTALLING` or `NOT_INSTALLED`.
* If your webhook returns HTTP 200 with a body Conduit can't parse, Conduit
  assumes a default status: `INSTALLING` for install events and `UNINSTALLING`
  for uninstall events.

If the integration installed successfully, reply with:

```json
{
  "status": "INSTALLED",
  "manage_integration_link": "<URL>",
  "artifacts": [
    { "name": "Dashboard", "key": "dashboard", "type": "url", "value": "https://mydashboardlink/" },
    { "name": "Entry Point", "key": "entry_point", "type": "address", "value": "0x0000000071727De22E5E9d8BAf0edAc6f37da032" }
  ]
}
```

If the integration requires additional configuration, for example if the
customer needs to deposit gas fees before you deploy smart contracts, reply
with:

```json
{
  "status": "CONFIGURATION_REQUIRED",
  "configure_integration_link": "<URL>"
}
```

Conduit redirects the customer to the configuration page on your website where
they complete the setup. Conduit also shows the configuration link permanently
on the integration page, in case the customer closes the window.

If the integration needs additional time to process, for example if you're
indexing the chain logs, reply with:

```json
{
  "status": "INSTALLING",
  "eta_seconds": 300,
  "manage_integration_link": "<URL>"
}
```

When the installation completes, POST the installation payload to
`https://api.conduit.xyz/public/integrations/status/update` with your
integration secret in the `X-Conduit-Integration-Secret` header. The `id` is
the network slug from the event:

```json
{
  "id": "<NETWORK SLUG>",
  "status": "INSTALLED",
  "manage_integration_link": "<URL>"
}
```

## Fetch network information

You can fetch a network's information at any time, for example if you weren't
able to process an installation event. Send an unauthenticated GET request to
`https://api.conduit.xyz/public/integrations/network/:network_slug`, where
`:network_slug` is the `id` from the event:

```bash
curl https://api.conduit.xyz/public/integrations/network/zora-mainnet-0
```

The response follows the same schema as the event payload, with two
serialization differences:

* Field names use camelCase (`chainId`, `nativeCurrency`, `logoUrl`) instead
  of snake_case.
* Empty and default-valued fields are included instead of omitted, so expect
  empty strings, empty objects, and `false` booleans. The `event` field is
  always `DEFAULT_EVENT_TYPE`, and `installationId` is empty because the
  response describes the network, not an installation.

```json
{
  "event": "DEFAULT_EVENT_TYPE",
  "id": "zora-mainnet-0",
  "chainId": "7777777",
  "parentChainId": "1",
  "type": "OPTIMISM",
  "nativeCurrency": {
    "name": "Ether",
    "symbol": "ETH",
    "decimals": 18,
    "contract": "0x0000000000000000000000000000000000000000"
  },
  "name": "Zora Mainnet",
  "rpc": "https://rpc.zora.energy",
  "ws": "wss://rpc.zora.energy",
  "explorer": "https://explorer.zora.energy",
  "logoUrl": "https://storage.googleapis.com/conduit-prd-apps-web-cdn/zora.png",
  "iconUrl": "https://storage.googleapis.com/conduit-prd-apps-web-cdn/zora.png",
  "brandColor": "",
  "fileOptimismContracts": "https://api.conduit.xyz/file/v1/optimism/contracts/zora-mainnet-0",
  "fileOptimismGenesis": "https://api.conduit.xyz/file/v1/optimism/genesis/zora-mainnet-0",
  "fileOptimismRollup": "https://api.conduit.xyz/file/v1/optimism/rollup/zora-mainnet-0",
  "fileArbitrumCore": "",
  "fileArbitrumChaininfo": "",
  "installationId": "",
  "arguments": {},
  "networkType": "MAINNET",
  "bundler": "",
  "contracts": {
    "AddressManager": "0xEF8115F2733fb2033a7c756402Fc1deaa56550Ef",
    "BondManager": "0x0000000000000000000000000000000000000000",
    "CanonicalTransactionChain": "0x0000000000000000000000000000000000000000",
    "DisputeGameFactory": "0xB0F15106fa1e473Ddb39790f197275BC979Aa37e",
    "FaultDisputeGame": "0x90Ac42EB72E6ae6f597E8c5a8816d9179a1456F8",
    "L1CrossDomainMessenger": "0xdC40a14d9abd6F410226f1E6de71aE03441ca506",
    "L1StandardBridge": "0x3e2Ea9B92B7E48A52296fD261dc26fd995284631",
    "L2OutputOracle": "0x0000000000000000000000000000000000000000",
    "OptimismPortal": "0x1a0ad011913A150f69f6A19DF447A0CfD9551054",
    "OptimismPortal2": "0x1a0ad011913A150f69f6A19DF447A0CfD9551054",
    "StateCommitmentChain": "0x0000000000000000000000000000000000000000"
  },
  "private": false
}
```

## Link accounts with OAuth 2.0

If your integration needs to link Conduit customers to accounts in your
service, implement the OAuth 2.0 authorization code flow:

#### Authorization request

Conduit issues an OAuth 2.0 request to your authorization endpoint:

```
GET https://integration.com/oauth/authorize
  ?response_type=code
  &client_id=CONDUIT_INTEGRATION_CLIENT_ID
  &redirect_uri=CONDUIT_OAUTH2_CALLBACK
  &scope=SCOPES
  &state=REQUEST_STATE
```

#### User authorization

Conduit redirects the customer to your login page, where they sign in and
authorize the app. After authorization, your service redirects back to
Conduit's callback URL with an authorization code.

#### Token exchange

Conduit handles the callback and exchanges the authorization code for an
access token with a POST request to your token endpoint:

```
POST https://integration.com/oauth/token
Content-Type: application/x-www-form-urlencoded

client_id=CONDUIT_INTEGRATION_CLIENT_ID
&client_secret=CONDUIT_INTEGRATION_CLIENT_SECRET
&grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=CONDUIT_OAUTH2_CALLBACK
```

#### Network information delivery

After obtaining the access token, Conduit sends an authenticated POST
request to your integration endpoint with the network information.

## TypeScript SDK

The [`@conduitxyz/integrations`](https://github.com/conduitxyz/integrations)
SDK provides TypeScript types and helpers for handling integration events:

```bash
npm install @conduitxyz/integrations
```

The SDK exports:

* **`IntegrationEvent`**: a discriminated union type for incoming webhook
  events, narrowed by the `event` field.
* **`IntegrationEventResponse`**, **`InstallationArtifact`**, and
  **`constructIntegrationEventResponse`**: a discriminated union type and
  helper for building type-safe responses, narrowed by the `status` field.
* **`SECRET_HEADER_NAME`**, **`isValidSecret`**, and **`assertValidSecret`**:
  the `X-Conduit-Integration-Secret` header name and helpers for validating it.
* **`API_KEY_HEADER_NAME`**, **`SIGNATURE_HEADER_NAME`**,
  **`isValidSignature`**, and **`assertValidSignature`**: header names and
  helpers for verifying HMAC-signed webhooks.
* **Constants**: `EventType`, `IntegrationStatus`, `StackType`, and
  `NetworkType`.

```typescript
import {
  SECRET_HEADER_NAME,
  isValidSecret,
  constructIntegrationEventResponse,
  type IntegrationEvent,
} from "@conduitxyz/integrations";

export async function handleWebhook(request: Request): Promise<Response> {
  const headerSecret = request.headers.get(SECRET_HEADER_NAME) ?? "";
  if (!isValidSecret(headerSecret, process.env.CONDUIT_INTEGRATION_SECRET!)) {
    return new Response("Unauthorized", { status: 401 });
  }

  const event = (await request.json()) as IntegrationEvent;

  switch (event.event) {
    case "INSTALLED": {
      // Start indexing, deploy contracts, and so on.
      const response = constructIntegrationEventResponse({
        id: event.id,
        status: "INSTALLED",
        manage_integration_link: "https://example.com/manage",
      });
      return Response.json(response);
    }
    case "UNINSTALLED": {
      // Clean up resources for event.id.
      return Response.json(
        constructIntegrationEventResponse({
          id: event.id,
          status: "NOT_INSTALLED",
        })
      );
    }
    default:
      return new Response("OK");
  }
}
```

## Deploy a webhook handler on Cloudflare Workers

The fastest way to stand up a webhook endpoint is a
[Cloudflare Worker](https://developers.cloudflare.com/workers/). Copy this
minimal project, deploy it with
[Wrangler](https://developers.cloudflare.com/workers/wrangler/), and you have
a public HTTPS endpoint that Conduit can send events to. The SDK secret and
signature helpers use the Web Crypto API, so they run on Workers without
extra configuration.

#### Create the project

Install the SDK, Wrangler, and TypeScript:

```bash
mkdir conduit-integration-webhook && cd conduit-integration-webhook
npm init -y
npm install @conduitxyz/integrations
npm install --save-dev wrangler typescript
```

#### Configure the Worker

Create `wrangler.jsonc`:

```jsonc
{
  "name": "conduit-integration-webhook",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-20",
  "compatibility_flags": ["nodejs_compat"],
  "observability": {
    "enabled": true,
    "logs": { "head_sampling_rate": 1 }
  }
}
```

#### Write the handler

Create `src/index.ts`:

```typescript
import {
  SECRET_HEADER_NAME,
  isValidSecret,
  constructIntegrationEventResponse,
  type IntegrationEvent,
} from "@conduitxyz/integrations";

export default {
  async fetch(request, env): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    const headerSecret = request.headers.get(SECRET_HEADER_NAME) ?? "";
    if (!isValidSecret(headerSecret, env.CONDUIT_INTEGRATION_SECRET)) {
      return new Response("Unauthorized", { status: 401 });
    }

    const event = (await request.json()) as IntegrationEvent;
    console.log(
      JSON.stringify({
        message: "integration event received",
        event: event.event,
        network: event.id,
      })
    );

    switch (event.event) {
      case "INSTALLED":
        // Start indexing, deploy contracts, and so on.
        return Response.json(
          constructIntegrationEventResponse({
            id: event.id,
            status: "INSTALLED",
            manage_integration_link: `https://example.com/manage/${event.id}`,
          })
        );
      case "UNINSTALLED":
      case "NETWORK_DELETED":
        // Clean up resources for event.id.
        return Response.json(
          constructIntegrationEventResponse({
            id: event.id,
            status: "NOT_INSTALLED",
          })
        );
      default:
        return new Response("OK");
    }
  },
} satisfies ExportedHandler<Env>;
```

If your integration uses signature verification instead of the secret
header, read the raw body before parsing and verify it with
`isValidSignature`:

```typescript
import {
  SIGNATURE_HEADER_NAME,
  isValidSignature,
} from "@conduitxyz/integrations";

const rawBody = await request.text();
const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? "";
if (!(await isValidSignature(signature, rawBody, env.CONDUIT_SIGNATURE_SECRET))) {
  return new Response("Unauthorized", { status: 401 });
}
const event = JSON.parse(rawBody) as IntegrationEvent;
```

#### Set the secret and run locally

For local development, put the secret in a `.dev.vars` file and exclude
it from version control. Then generate the `Env` type, which picks up the
secret, and start the development server:

```bash
echo 'CONDUIT_INTEGRATION_SECRET="<YOUR_INTEGRATION_SECRET>"' > .dev.vars
npx wrangler types
npx wrangler dev
```

Test the handler with a sample event:

```bash
curl http://localhost:8787 \
  -H "Content-Type: application/json" \
  -H "X-Conduit-Integration-Secret: <YOUR_INTEGRATION_SECRET>" \
  -d '{"event":"INSTALLED","id":"example-chain-a1b2c3d4e5","chain_id":"424242","name":"Example Chain"}'
```

#### Deploy

Store the secret in production and deploy:

```bash
npx wrangler secret put CONDUIT_INTEGRATION_SECRET
npx wrangler deploy
```

Share the deployed `*.workers.dev` URL (or a custom domain) with Conduit
as your webhook URL.