Migrating from v1 to v2

Guide for migrating legacy v1 chainhooks to Chainhook 2.0 Beta

Managing v1 chainhooks

Legacy v1 chainhooks remain read-only in the Platform UI; you manage them through the Platform API.

What you'll learn

Capture and analyze all existing v1 chainhooks.
Convert predicate-based filters into explicit v2 event definitions.
Register v2 chainhooks, test delivery, and retire the originals safely.

Prerequisites

  • API access to both the Platform API and Chainhook REST API (same HIRO_API_KEY).
  • Local SDK (@hirosystems/chainhooks-client) or curl for REST calls.
  • Environment variable HIRO_API_KEY set for the CLI and code samples.

Inventory v1 chainhooks

Use the Platform API to fetch every chainhook that still fires in production.

CLI

curl -sS "https://api.platform.hiro.so/v1/ext/$HIRO_API_KEY/chainhooks" \
-H "content-type: application/json"

TypeScript

export async function listV1Chainhooks() {
const response = await fetch(
`https://api.platform.hiro.so/v1/ext/${process.env.HIRO_API_KEY}/chainhooks`,
{ headers: { 'content-type': 'application/json' } }
);
if (!response.ok) throw new Error(response.statusText);
return (await response.json()) as any[];
}
const chainhooks = await listV1Chainhooks();
console.log(`Found ${chainhooks.length} v1 chainhooks`);

Inspect a chainhook

Pull the full definition for each UUID so you can convert custom filters and metadata.

CLI

curl -sS \
"https://api.platform.hiro.so/v1/ext/$HIRO_API_KEY/chainhooks/$CHAINHOOK_UUID" \
-H "content-type: application/json"

TypeScript

export async function getV1Chainhook(uuid: string) {
const response = await fetch(
`https://api.platform.hiro.so/v1/ext/${process.env.HIRO_API_KEY}/chainhooks/${uuid}`,
{ headers: { 'content-type': 'application/json' } }
);
if (!response.ok) throw new Error(response.statusText);
return await response.json();
}

Map configuration to v2

Translate v1 structures to v2 fields before provisioning new hooks.

v1 Conceptv2 TargetNotes
if_this.scopefilters.events[].typeReplace scope/action combos with a single event type.
if_this.actionstypetransfer maps to *_transfer.
then_that.http_post.urlaction.urlv2 handles secrets automatically.
networks.mainnetnetwork: "mainnet"Create one v2 hook per network.
authorization_headerWebhook secret managementUse rotateConsumerSecret() after registration.

Example Conversion

// v1 (Platform API)
{
"name": "stx-transfers",
"networks": {
"mainnet": {
"if_this": { "scope": "stx_event", "actions": ["transfer"] },
"then_that": { "http_post": { "url": "https://example.com/webhooks" } }
}
}
}
// v2 (SDK)
import { ChainhooksClient, CHAINHOOKS_BASE_URL } from '@hirosystems/chainhooks-client';
const client = new ChainhooksClient({
baseUrl: CHAINHOOKS_BASE_URL.mainnet,
apiKey: process.env.HIRO_API_KEY!,
});
await client.registerChainhook({
version: '1',
name: 'stx-transfers',
chain: 'stacks',
network: 'mainnet',
filters: {
events: [{ type: 'stx_transfer' }],
},
action: {
type: 'http_post',
url: 'https://example.com/webhooks',
},
options: {
decode_clarity_values: true,
enable_on_registration: true,
},
});

Create v2 chainhooks

Provision each chainhook with the SDK or REST API, mirroring the mapped filters.

REST

curl -sS -X POST "https://api.mainnet.hiro.so/chainhooks/v1/me/" \
-H "content-type: application/json" \
-H "x-api-key: $HIRO_API_KEY" \
-d @v2-chainhook.json

Chainhook SDK

const client = new ChainhooksClient({
baseUrl: CHAINHOOKS_BASE_URL[config.network],
apiKey: process.env.HIRO_API_KEY!,
});
const chainhook = await client.registerChainhook(config);

Validate and retire v1

Stream events through both versions, confirm delivery, then clean up the legacy definitions.

Best practices

Keep both v1 and v2 hooks active until you verify delivery parity.

Enablement Checks

const chainhook = await client.getChainhook(v2Uuid);
console.log(chainhook.status.enabled);

Delete with cURL

curl -sS -X DELETE \
"https://api.platform.hiro.so/v1/ext/$HIRO_API_KEY/chainhooks/$CHAINHOOK_UUID" \
-H "content-type: application/json"

Delete with SDK

export async function deleteV1Chainhook(uuid: string) {
const response = await fetch(
`https://api.platform.hiro.so/v1/ext/${process.env.HIRO_API_KEY}/chainhooks/${uuid}`,
{ method: 'DELETE', headers: { 'content-type': 'application/json' } }
);
if (!response.ok) throw new Error(response.statusText);
return await response.json();
}

Automation Template

Use this script as a starting point when you have many similar chainhooks to move.

Filter Translation Reference

Common scopes

v1 ScopeTypical Actionsv2 typeExtras
stx_eventtransferstx_transferInclude sender or recipient filters as needed.
contract_calln/acontract_callAdd contract_identifier and function_name.
ft_eventtransferft_transferSpecify asset_identifier.
nft_eventtransfer, mintnft_transfer · nft_mintProvide asset_identifier.

Example

// v1 filter
{
"scope": "stx_event",
"actions": ["transfer"]
}
// v2 filter
{
"events": [
{
"type": "stx_transfer",
"recipient": "SP3FBR2AGKQX0..."
}
]
}

How is this guide?