Build a Native Integration

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.

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.

FieldTypeDescription
eventstring, requiredEvent 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.
idstring, requiredA unique identifier for the network: the network slug, for example example-chain-a1b2c3d4e5. Use it when calling the public integration endpoints.
installation_idstringInstallation ID for the integration.
chain_idstringThe chain ID of the network. Sent on INSTALLED, UNINSTALLED, and NETWORK_DELETED.
parent_chain_idstringThe chain ID of the parent (settlement) chain. INSTALLED only.
typestringStack type: OPTIMISM or ARBITRUM. INSTALLED only.
native_currencyobjectThe network’s native currency: name, symbol, decimals, and contract (the contract address on the settlement layer). INSTALLED only.
namestringThe name of the network. INSTALLED only.
rpcstringThe RPC endpoint URL for the network. INSTALLED only.
wsstringThe WebSocket endpoint URL for the network. INSTALLED only.
explorerstringThe URL of the network’s block explorer. INSTALLED only.
logo_urlstringThe URL of the network’s logo, when set. INSTALLED only.
icon_urlstringThe URL of the network’s icon, when set. INSTALLED only.
brand_colorstringThe brand color of the network, when set. INSTALLED only.
contractsobjectA mapping of contract names to their addresses. INSTALLED only.
privatebooleanWhether the network is private. Hide private networks in your app. Omitted when false. INSTALLED only.
network_typestringMAINNET or TESTNET. INSTALLED only.
argumentsobjectConfiguration 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_contractsstringThe file URL for OP Stack contracts. INSTALLED only, OP Stack chains.
file_optimism_genesisstringThe file URL for the OP Stack genesis file. INSTALLED only, OP Stack chains.
file_optimism_rollupstringThe file URL for the OP Stack rollup configuration. INSTALLED only, OP Stack chains.
file_arbitrum_corestringThe file URL for Arbitrum core contracts. INSTALLED only, Arbitrum chains.
file_arbitrum_chaininfostringThe 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:

1{
2 "event": "INSTALLED",
3 "id": "example-chain-a1b2c3d4e5",
4 "chain_id": "424242",
5 "parent_chain_id": "11155111",
6 "type": "OPTIMISM",
7 "native_currency": {
8 "name": "Ether",
9 "symbol": "ETH",
10 "decimals": 18,
11 "contract": "0x0000000000000000000000000000000000000000"
12 },
13 "name": "Example Chain",
14 "rpc": "https://rpc-example-chain-a1b2c3d4e5.t.conduit.xyz",
15 "ws": "wss://rpc-example-chain-a1b2c3d4e5.t.conduit.xyz",
16 "explorer": "https://explorer-example-chain-a1b2c3d4e5.t.conduit.xyz",
17 "logo_url": "https://cdn.example.com/example-chain-logo.svg",
18 "icon_url": "https://cdn.example.com/example-chain-icon.svg",
19 "brand_color": "#6C4BF4",
20 "file_optimism_contracts": "https://api.conduit.xyz/file/v1/optimism/contracts/example-chain-a1b2c3d4e5",
21 "file_optimism_genesis": "https://api.conduit.xyz/file/v1/optimism/genesis/example-chain-a1b2c3d4e5",
22 "file_optimism_rollup": "https://api.conduit.xyz/file/v1/optimism/rollup/example-chain-a1b2c3d4e5",
23 "installation_id": "11111111-2222-3333-4444-555555555555",
24 "network_type": "TESTNET",
25 "arguments": {
26 "example_configuration_key": "example value"
27 },
28 "contracts": {
29 "AddressManager": "0x1111111111111111111111111111111111111111",
30 "BondManager": "0x0000000000000000000000000000000000000000",
31 "CanonicalTransactionChain": "0x0000000000000000000000000000000000000000",
32 "L1CrossDomainMessenger": "0x2222222222222222222222222222222222222222",
33 "L1StandardBridge": "0x3333333333333333333333333333333333333333",
34 "L2OutputOracle": "0x4444444444444444444444444444444444444444",
35 "OptimismPortal": "0x5555555555555555555555555555555555555555",
36 "StateCommitmentChain": "0x0000000000000000000000000000000000000000"
37 }
38}

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:

1{
2 "event": "UNINSTALLED",
3 "id": "example-chain-a1b2c3d4e5",
4 "chain_id": "424242",
5 "installation_id": "11111111-2222-3333-4444-555555555555"
6}

Respond to events

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

FieldTypeDescription
idstring, requiredThe identifier for the network.
statusstring, requiredThe status to update to: NOT_INSTALLED, INSTALLING, INSTALLED, UNINSTALLING, FAILED_TO_INSTALL, or CONFIGURATION_REQUIRED.
configure_integration_linkstringThe URL where the customer configures the integration. Required for CONFIGURATION_REQUIRED.
eta_secondsnumberThe estimated time until installation completes, in seconds. Applies to INSTALLING.
manage_integration_linkstringThe URL where the customer manages the integration. Applies to INSTALLED.
failure_reasonstringA failure reason. Only valid with FAILED_TO_INSTALL.
artifactsarrayA 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:

1{
2 "status": "INSTALLED",
3 "manage_integration_link": "<URL>",
4 "artifacts": [
5 { "name": "Dashboard", "key": "dashboard", "type": "url", "value": "https://mydashboardlink/" },
6 { "name": "Entry Point", "key": "entry_point", "type": "address", "value": "0x0000000071727De22E5E9d8BAf0edAc6f37da032" }
7 ]
8}

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

1{
2 "status": "CONFIGURATION_REQUIRED",
3 "configure_integration_link": "<URL>"
4}

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:

1{
2 "status": "INSTALLING",
3 "eta_seconds": 300,
4 "manage_integration_link": "<URL>"
5}

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:

1{
2 "id": "<NETWORK SLUG>",
3 "status": "INSTALLED",
4 "manage_integration_link": "<URL>"
5}

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:

$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.
1{
2 "event": "DEFAULT_EVENT_TYPE",
3 "id": "zora-mainnet-0",
4 "chainId": "7777777",
5 "parentChainId": "1",
6 "type": "OPTIMISM",
7 "nativeCurrency": {
8 "name": "Ether",
9 "symbol": "ETH",
10 "decimals": 18,
11 "contract": "0x0000000000000000000000000000000000000000"
12 },
13 "name": "Zora Mainnet",
14 "rpc": "https://rpc.zora.energy",
15 "ws": "wss://rpc.zora.energy",
16 "explorer": "https://explorer.zora.energy",
17 "logoUrl": "https://storage.googleapis.com/conduit-prd-apps-web-cdn/zora.png",
18 "iconUrl": "https://storage.googleapis.com/conduit-prd-apps-web-cdn/zora.png",
19 "brandColor": "",
20 "fileOptimismContracts": "https://api.conduit.xyz/file/v1/optimism/contracts/zora-mainnet-0",
21 "fileOptimismGenesis": "https://api.conduit.xyz/file/v1/optimism/genesis/zora-mainnet-0",
22 "fileOptimismRollup": "https://api.conduit.xyz/file/v1/optimism/rollup/zora-mainnet-0",
23 "fileArbitrumCore": "",
24 "fileArbitrumChaininfo": "",
25 "installationId": "",
26 "arguments": {},
27 "networkType": "MAINNET",
28 "bundler": "",
29 "contracts": {
30 "AddressManager": "0xEF8115F2733fb2033a7c756402Fc1deaa56550Ef",
31 "BondManager": "0x0000000000000000000000000000000000000000",
32 "CanonicalTransactionChain": "0x0000000000000000000000000000000000000000",
33 "DisputeGameFactory": "0xB0F15106fa1e473Ddb39790f197275BC979Aa37e",
34 "FaultDisputeGame": "0x90Ac42EB72E6ae6f597E8c5a8816d9179a1456F8",
35 "L1CrossDomainMessenger": "0xdC40a14d9abd6F410226f1E6de71aE03441ca506",
36 "L1StandardBridge": "0x3e2Ea9B92B7E48A52296fD261dc26fd995284631",
37 "L2OutputOracle": "0x0000000000000000000000000000000000000000",
38 "OptimismPortal": "0x1a0ad011913A150f69f6A19DF447A0CfD9551054",
39 "OptimismPortal2": "0x1a0ad011913A150f69f6A19DF447A0CfD9551054",
40 "StateCommitmentChain": "0x0000000000000000000000000000000000000000"
41 },
42 "private": false
43}

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

1

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
2

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.

3

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
4

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 SDK provides TypeScript types and helpers for handling integration events:

$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.
1import {
2 SECRET_HEADER_NAME,
3 isValidSecret,
4 constructIntegrationEventResponse,
5 type IntegrationEvent,
6} from "@conduitxyz/integrations";
7
8export async function handleWebhook(request: Request): Promise<Response> {
9 const headerSecret = request.headers.get(SECRET_HEADER_NAME) ?? "";
10 if (!isValidSecret(headerSecret, process.env.CONDUIT_INTEGRATION_SECRET!)) {
11 return new Response("Unauthorized", { status: 401 });
12 }
13
14 const event = (await request.json()) as IntegrationEvent;
15
16 switch (event.event) {
17 case "INSTALLED": {
18 // Start indexing, deploy contracts, and so on.
19 const response = constructIntegrationEventResponse({
20 id: event.id,
21 status: "INSTALLED",
22 manage_integration_link: "https://example.com/manage",
23 });
24 return Response.json(response);
25 }
26 case "UNINSTALLED": {
27 // Clean up resources for event.id.
28 return Response.json(
29 constructIntegrationEventResponse({
30 id: event.id,
31 status: "NOT_INSTALLED",
32 })
33 );
34 }
35 default:
36 return new Response("OK");
37 }
38}

Deploy a webhook handler on Cloudflare Workers

The fastest way to stand up a webhook endpoint is a Cloudflare Worker. Copy this minimal project, deploy it with 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.

1

Create the project

Install the SDK, Wrangler, and TypeScript:

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

Configure the Worker

Create wrangler.jsonc:

1{
2 "name": "conduit-integration-webhook",
3 "main": "src/index.ts",
4 "compatibility_date": "2026-07-20",
5 "compatibility_flags": ["nodejs_compat"],
6 "observability": {
7 "enabled": true,
8 "logs": { "head_sampling_rate": 1 }
9 }
10}
3

Write the handler

Create src/index.ts:

1import {
2 SECRET_HEADER_NAME,
3 isValidSecret,
4 constructIntegrationEventResponse,
5 type IntegrationEvent,
6} from "@conduitxyz/integrations";
7
8export default {
9 async fetch(request, env): Promise<Response> {
10 if (request.method !== "POST") {
11 return new Response("Method not allowed", { status: 405 });
12 }
13
14 const headerSecret = request.headers.get(SECRET_HEADER_NAME) ?? "";
15 if (!isValidSecret(headerSecret, env.CONDUIT_INTEGRATION_SECRET)) {
16 return new Response("Unauthorized", { status: 401 });
17 }
18
19 const event = (await request.json()) as IntegrationEvent;
20 console.log(
21 JSON.stringify({
22 message: "integration event received",
23 event: event.event,
24 network: event.id,
25 })
26 );
27
28 switch (event.event) {
29 case "INSTALLED":
30 // Start indexing, deploy contracts, and so on.
31 return Response.json(
32 constructIntegrationEventResponse({
33 id: event.id,
34 status: "INSTALLED",
35 manage_integration_link: `https://example.com/manage/${event.id}`,
36 })
37 );
38 case "UNINSTALLED":
39 case "NETWORK_DELETED":
40 // Clean up resources for event.id.
41 return Response.json(
42 constructIntegrationEventResponse({
43 id: event.id,
44 status: "NOT_INSTALLED",
45 })
46 );
47 default:
48 return new Response("OK");
49 }
50 },
51} 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:

1import {
2 SIGNATURE_HEADER_NAME,
3 isValidSignature,
4} from "@conduitxyz/integrations";
5
6const rawBody = await request.text();
7const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? "";
8if (!(await isValidSignature(signature, rawBody, env.CONDUIT_SIGNATURE_SECRET))) {
9 return new Response("Unauthorized", { status: 401 });
10}
11const event = JSON.parse(rawBody) as IntegrationEvent;
4

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:

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

Test the handler with a sample event:

$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"}'
5

Deploy

Store the secret in production and deploy:

$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.