Sentinel API Reference

The Sentinel API allows you to programmatically create and manage sentinels.

Requests need to be authenticated with a bearer token, which is negotiated from the Team API Key with the corresponding capability. Refer to the authentication section for info on how to negotiate it.

We recommend you use the @openzeppelin/defender-sentinel-client npm package for simplifying interactions with the Sentinel API.
It is not recommended to use the @openzeppelin/defender-sentinel-client npm package in a browser environment as sensitive keys would be exposed publicly.
const { SentinelClient } = require('@openzeppelin/defender-sentinel-client');
const creds = { apiKey: ADMIN_API_KEY, apiSecret: ADMIN_API_SECRET };
const client = new SentinelClient(creds);

Notifications Endpoint

A sentinel requires a notification configuration to alert the right channels in case an event is triggered. In order to do so, you can either use an existing notification ID (from another sentinel for example), or create a new one.

The following notification channels are available:

  • email

  • slack

  • discord

  • telegram

  • datadog

List Notifications

To list all notification channels, you can call the listNotificationChannels function on the client, which returns a NotificationResponse[] object.

// List existing notification channels
// This returns a `NotificationResponse[]` object.
const notificationChannels = await client.listNotificationChannels()
const { notificationId, type } = notificationChannels[0];

Alternatively, via a curl request:

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/notifications"

An example response:

[
    {
        "notificationId": "14e99e8f-e2be-4eb1-a543-b2d90b68cdd8",
        "config": {
            "emails": [
                "john@example.com"
            ]
        },
        "updatedAt": "2021-11-04T12:21:22.858Z",
        "tenantId": "0c1585a9-1b86-4f9d-8468-28227802ac1d",
        "paused": false,
        "name": "MyEmailNotification",
        "type": "email"
    },
]

Create Notifications

To create a new notification channel, you can call the createNotificationChannel function on the client.

The createNotificationChannel function requires the CreateNotificationRequest object and returns a NotificationResponse object.

// create a new notification
const notification = await client.createNotificationChannel({
  type: 'email',
  name: 'MyEmailNotification',
  config: {
    emails: ['john@example.com'],
  },
  paused: false,
});

const notification = await client.createNotificationChannel({
  type: 'slack',
  name: 'MySlackNotification',
  config: {
    url: 'https://slack.com/url/key',
  },
  paused: false,
});

const notification = await client.createNotificationChannel({
  type: 'telegram',
  name: 'MyTelegramNotification',
  config: {
    botToken: 'abcd',
    chatId: '123',
  },
  paused: false,
});

const notification = await client.createNotificationChannel({
  type: 'discord',
  name: 'MyDiscordNotification',
  config: {
    url: 'https://discord.com/url/key',
  },
  paused: false,
});

const notification = await client.createNotificationChannel({
  type: 'datadog',
  name: 'MyDatadogNotification',
  config: {
    apiKey: 'abcd',
    metricPrefix: 'prefix',
  },
  paused: false,
});

Update a Notification

To update an existing notification channel, you can call the updateNotificationChannel function on the client, which returns a NotificationResponse object. The function takes in the UpdateNotificationRequest object as a parameter.

const notificationChannel = await client.updateNotificationChannel(
{
  type: 'email',
  notificationId: '14e99e8f-e2be-4eb1-a543-b2d90b68cdd8',
  config: {
    emails: ["john@example.com"]
  },
  paused: false,
  name: "MyUpdatedEmailNotification"
});

Get a Notification

To retrieve a notification channel, you can call the getNotificationChannel function on the client, which returns a NotificationResponse object. The function takes in the GetNotificationRequest object as a parameter.

const notificationToRetrieve = {type: 'email', notificationId: '14e99e8f-e2be-4eb1-a543-b2d90b68cdd8'}
const notificationChannel = await client.getNotificationChannel(notificationToRetrieve);

Delete Notifications

To delete a notification channel, you can call the deleteNotificationChannel function on the client, which returns a string if successful. The function takes in the DeleteNotificationRequest object as a parameter.

const notificationToDelete = {type: 'email', notificationId: '14e99e8f-e2be-4eb1-a543-b2d90b68cdd8'}
const deleted = await client.deleteNotificationChannel(notificationToDelete);

Sentinels Endpoint

List Sentinels

To list existing sentinels, you can call the list function on the client, which returns a ListSentinelResponse object:

await client.list();

The subscribers endpoint is used to retrieve a list of existing sentinels via a GET request.

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/subscribers"

An example response:

[
    {
        "notifyConfig": {
            "notifications": [
                {
                    "type": "email",
                    "notificationId": "68e494d7-3b5a-4ffe-bd12-d4e483aa4995"
                }
            ],
            "timeoutMs": 0
        },
        "tenantId": "0c1585a9-1b86-4f9d-8468-28227802ac1d",
        "createdAt": "2021-11-15T16:04:13.936Z",
        "addressRules": [
            {
                "conditions": [],
                "abi": "[...]",
                "addresses": ["0xf664FA8aB9AA8021E2c08F45fEeA817D5730A713"]
            }
        ],
        "blockWatcherId": "rinkeby-1",
        "subscriberId": "abebeda6-f670-4e3c-a65b-a34c840e9a5e",
        "paused": false,
        "name": "test",
        "network": "rinkeby"
    }
]

Create Sentinels

To create a new sentinel, you need to provide the network, name, pause-state, conditions, alert threshold and notification configuration. This request is exported as type CreateSentinelRequest.

type CreateSentinelRequest =
  | ExternalCreateBlockSubscriberRequest
  | ExternalCreateFortaSubscriberRequest;

interface ExternalCreateBlockSubscriberRequest {
  type: 'BLOCK';
  name: string;
  addresses: string[];
  paused?: boolean;
  alertThreshold?: Threshold;
  autotaskCondition?: string;
  autotaskTrigger?: string;
  alertTimeoutMs?: number;
  alertMessageBody?: string;
  notificationChannels: string[];
  network: string;
  confirmLevel?: number; // blockWatcherId
  abi?: string;
  eventConditions?: EventCondition[];
  functionConditions?: FunctionCondition[];
  txCondition?: string;
}

interface ExternalCreateFortaSubscriberRequest {
  type: 'FORTA';
  name: string;
  paused?: boolean;
  alertThreshold?: Threshold;
  autotaskCondition?: string;
  autotaskTrigger?: string;
  alertTimeoutMs?: number;
  alertMessageBody?: string;
  notificationChannels: string[];
  network?: string;
  fortaLastProcessedTime?: string;
  addresses?: Address[];
  agentIDs?: string[];
  fortaConditions: FortaConditionSet;
  privateFortaNodeId?: string;
}

An example for a contract (BLOCK) sentinel is provided below. This sentinel will be named My New Sentinel and will be monitoring the renounceOwnership function on the 0x0f06aB75c7DD497981b75CD82F6566e3a5CAd8f2 contract on the Rinkeby network. The alert threshold is set to 2 times within 1 hour, and the user will be notified via email.

const requestParameters = {
  type: 'BLOCK',
  network: 'rinkeby',
  // optional
  confirmLevel: 1, // if not set, we pick the blockwatcher for the chosen network with the lowest offset
  name: 'My New Sentinel',
  addresses: ['0x0f06aB75c7DD497981b75CD82F6566e3a5CAd8f2'],
  abi: '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{...}]',
  // optional
  paused: false,
  // optional
  eventConditions: [],
  // optional
  functionConditions: [{ functionSignature: 'renounceOwnership()' }],
  // optional
  txCondition: 'gasPrice > 0',
  // optional
  autotaskCondition: '3dcfee82-f5bd-43e3-8480-0676e5c28964',
  // optional
  autotaskTrigger: undefined,
  // optional
  alertThreshold: {
    amount: 2,
    windowSeconds: 3600,
  },
  // optional
  alertTimeoutMs: 0,
  notificationChannels: [notification.notificationId],
};

If you wish to trigger the sentinel based on additional events, you could add another EventCondition or FunctionCondition object, for example:

functionConditions: [{ functionSignature: 'renounceOwnership()' }],
eventConditions: [
  {
    eventSignature: "OwnershipTransferred(address,address)",
    expression: "\"0xf5453Ac1b5A978024F0469ea36Be25887EA812b5,0x6B9501462d48F7e78Ba11c98508ee16d29a03412\""
  }
]

You could also apply a transaction condition by modifying the txCondition property: Possible variables: value, gasPrice, maxFeePerGas, maxPriorityFeePerGas, gasLimit, gasUsed, to, from, nonce, status ('success', 'failed' or 'any'), input, or transactionIndex.

txCondition: 'gasPrice > 0',

You can also construct a request for a Forta (FORTA) sentinel as follows:

const requestParameters = {
  type: 'FORTA',
  name: 'MyNewFortaSentinel',
  // optional
  addresses: ['0x0f06aB75c7DD497981b75CD82F6566e3a5CAd8f2'],
  // optional
  agentIDs: ['0x8fe07f1a4d33b30be2387293f052c273660c829e9a6965cf7e8d485bcb871083'],
  fortaConditions: {
    // optional
    alertIDs: undefined, // string[]
    minimumScannerCount: 1, // default is 1
    // optional
    severity: 2, // (unknown=0, info=1, low=2, medium=3, high=4, critical=5)
  },
  // optional
  paused: false,
  // optional
  autotaskCondition: '3dcfee82-f5bd-43e3-8480-0676e5c28964',
  // optional
  autotaskTrigger: undefined,
  // optional
  alertThreshold: {
    amount: 2,
    windowSeconds: 3600,
  },
  // optional
  alertTimeoutMs: 0,
  notificationChannels: [notification.notificationId],
};

To create a Forta Local Mode Sentinel specify the scanner node address with privateFortaNodeId

  requestParameters.privateFortaNodeId: '0x0f06aB75c7DD497981b75CD82F6566e3a5CAd8f2'

Once all required parameters are populated, you can create a sentinel by calling the create function on the client. This will return a CreateSentinelResponse object.

await client.create(requestParameters);

Additionally, the sentinel could invoke an autotask to further evaluate. Documentation around this can be found here: https://docs.openzeppelin.com/defender/sentinel#autotask_conditions.

// If other conditions match, the sentinel will invoke this autotask to further evaluate.
autotaskCondition: '3dcfee82-f5bd-43e3-8480-0676e5c28964',
// Define autotask within the notification configuration
autotaskTrigger: '1abfee11-a5bc-51e5-1180-0675a5b24c61',

The subscribers endpoint is used to a create new sentinels via a POST request. If you wish to call the API directly, you will need to construct a CreateBlockSubscriberRequest object.

Defender currently only supports a limited subset of Sentinels (only a single addressRule), and we strongly suggest going through the JS client to avoid incompatibilities.
interface CreateBlockSubscriberRequest {
  name: string;
  paused: boolean;
  alertThreshold?: {
    amount: number;
    windowSeconds: number;
  };
  notifyConfig?: {
    notifications: [{
      notificationId: string;
      type: NotificationType;
    }];
    autotaskId?: string;
    messageBody?: string;
    timeoutMs: number;
  };
  addressRules: [{
    conditions: ConditionSet[];
    autotaskCondition?: {
      autotaskId: string;
    };
    addresses: string[];
    abi?: string;
  }];
  blockWatcherId: string;
  network: Network;
  type: 'BLOCK';
}

type NotificationType = 'slack' | 'email' | 'discord' | 'telegram' | 'datadog';

interface ConditionSet {
  eventConditions: EventCondition[];
  txConditions: TxCondition[];
  functionConditions: FunctionCondition[];
}
interface EventCondition {
  eventSignature: string;
  expression?: string | null;
}
interface TxCondition {
  status: 'success' | 'failed' | 'any';
  expression?: string | null;
}
interface FunctionCondition {
  functionSignature: string;
  expression?: string | null;
}
curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{...}' \
    "https://defender-api.openzeppelin.com/sentinel/subscribers"

Supported networks include:

type Network =
  | 'mainnet'
  | 'sepolia'
  | 'holesky'
  | 'xdai'
  | 'sokol'
  | 'fuse'
  | 'bsc'
  | 'bsctest'
  | 'fantom'
  | 'fantomtest'
  | 'moonbase'
  | 'moonriver'
  | 'moonbeam'
  | 'matic'
  | 'mumbai'
  | 'avalanche'
  | 'fuji'
  | 'arbitrum'
  | 'arbitrum-nova'
  | 'arbitrum-goerli'
  | 'arbitrum-sepolia'
  | 'optimism'
  | 'optimism-goerli'
  | 'optimis-sepolia'
  | 'celo'
  | 'alfajores'
  | 'harmony-s0'
  | 'harmony-test-s0'
  | 'aurora'
  | 'auroratest'
  | 'hedera'
  | 'hederatest'
  | 'zksync'
  | 'zksync-goerli'
  | 'base'
  | 'base-goerli'
  | 'base-sepolia'
  | 'linea-goerli'
  | 'linea'
  | 'mantle'
  | 'mantle-testnet'
  | 'scroll'
  | 'scroll-sepolia'
  | 'meld'
  | 'meld-kanazawa';

Retrieve a Sentinel

You can retrieve a sentinel by ID. This will return a CreateSentinelResponse object.

await client.get('8181d9e0-88ce-4db0-802a-2b56e2e6a7b1');

The subscribers/{id} endpoint is used to retrieve a sentinel via a GET request.

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/subscribers/{id}"

Update a Sentinel

To update a sentinel, you can call the update function on the client. This will require the sentinel ID and a UpdateSentinelRequest object as parameters:

await client.update('8181d9e0-88ce-4db0-802a-2b56e2e6a7b1', {name: 'My Updated Name', paused: true});

The subscribers/{id} endpoint is used to a update existing sentinels via a PUT request.

If you wish to call the API directly, you will need to construct a CreateBlockSubscriberRequest object.

curl \
  -X PUT \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{...}' \
    "https://defender-api.openzeppelin.com/sentinel/subscribers/{id}"

Delete a Sentinel

You can delete a sentinel by ID. This will return a DeletedSentinelResponse object.

await client.delete('8181d9e0-88ce-4db0-802a-2b56e2e6a7b1');

The subscribers/{id} endpoint is used to a delete a sentinel via a DELETE request.

curl \
  -X DELETE \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/subscribers/{id}"

An example response:

{
    "message": "subscriber deleted"
}

Pause or unpause a Sentinel

You can pause and unpause a sentinel by ID. This will return a CreateSentinelResponse object.

await client.pause('8181d9e0-88ce-4db0-802a-2b56e2e6a7b1');
await client.unpause('8181d9e0-88ce-4db0-802a-2b56e2e6a7b1');

If you wish to call the API directly, you can use the update endpoint and set pause to true or false accordingly.

List Networks

To list tenant enabled networks, you can call the listNetworks function on the client, which returns a Network[] object:

await client.listNetworks(); // lists all networks
await client.listNetworks({ networkType: 'production' }); // lists only production networks
await client.listNetworks({ networkType: 'test' }); // lists only test networks
curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/networks"

You can query for specific network types (production or test) by passing the type query parameter:

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/sentinel/networks?type=production"