[solved] Streams API doesn't call webhook endpoint

I was following this tutorial: https://moralis.io/nodejs-telegram-bot-tutorial-build-a-telegram-bot-using-nodejs/

I have finished setting up the Streams API up and running, i.e. I get the Transfer() event when an ERC-20 token transfer happens, below are the specifications for my Streams API:

Here is the payload I got from the Streams API UI:

{
    "confirmed": true,
    "chainId": "0x61",
    "abi": [
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "owner",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "spender",
                    "type": "address"
                },
                {
                    "indexed": false,
                    "internalType": "uint256",
                    "name": "value",
                    "type": "uint256"
                }
            ],
            "name": "Approval",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "previousOwner",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "newOwner",
                    "type": "address"
                }
            ],
            "name": "OwnershipTransferred",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "bytes32",
                    "name": "role",
                    "type": "bytes32"
                },
                {
                    "indexed": true,
                    "internalType": "bytes32",
                    "name": "previousAdminRole",
                    "type": "bytes32"
                },
                {
                    "indexed": true,
                    "internalType": "bytes32",
                    "name": "newAdminRole",
                    "type": "bytes32"
                }
            ],
            "name": "RoleAdminChanged",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "bytes32",
                    "name": "role",
                    "type": "bytes32"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "account",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "sender",
                    "type": "address"
                }
            ],
            "name": "RoleGranted",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "bytes32",
                    "name": "role",
                    "type": "bytes32"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "account",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "sender",
                    "type": "address"
                }
            ],
            "name": "RoleRevoked",
            "type": "event"
        },
        {
            "anonymous": false,
            "inputs": [
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "from",
                    "type": "address"
                },
                {
                    "indexed": true,
                    "internalType": "address",
                    "name": "to",
                    "type": "address"
                },
                {
                    "indexed": false,
                    "internalType": "uint256",
                    "name": "value",
                    "type": "uint256"
                }
            ],
            "name": "Transfer",
            "type": "event"
        }
    ],
    "streamId": "719473ee-96d1-4a85-9be1-6245deb73eae",
    "tag": "demo",
    "retries": 0,
    "block": {
        "number": "30126385",
        "hash": "0x78ff6516294eee39394d1a2e9634817343b5faeab005eb12be3d2bfe8ef5da6c",
        "timestamp": "1685072485"
    },
    "logs": [
        {
            "logIndex": "27",
            "transactionHash": "0xb7989e32038addacbf2a54585c1124e7444d1fb8134764de782da437f8fa07dd",
            "address": "0x6f162ff7ef464858606e05855c762eeb751f56b3",
            "data": "0x00000000000000000000000000000000000000000000000000000000000003ce",
            "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "topic1": "0x000000000000000000000000f0355b92e55adfc147fff04517a17806e02dd969",
            "topic2": "0x000000000000000000000000fcdda93b6251beefe41e003e6dd4070ac79f5e2c",
            "topic3": null
        }
    ],
    "txs": [
        {
            "hash": "0xb7989e32038addacbf2a54585c1124e7444d1fb8134764de782da437f8fa07dd",
            "gas": "149828",
            "gasPrice": "10000000000",
            "nonce": "90",
            "input": "0x7ff36ab5000000000000000000000000000000000000000000000000000000000000028d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000fcdda93b6251beefe41e003e6dd4070ac79f5e2c0000000000000000000000000000000000000000000000000000000064702f050000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ae13d989dac2f0debff460ac112a837c89baa7cd0000000000000000000000006f162ff7ef464858606e05855c762eeb751f56b3",
            "transactionIndex": "3",
            "fromAddress": "0xfcdda93b6251beefe41e003e6dd4070ac79f5e2c",
            "toAddress": "0xde2db97d54a3c3b008a097b2260633e6ca7db1af",
            "value": "1000000000000000",
            "type": "0",
            "v": "230",
            "r": "44706814527472766401393300369193026493864446188771623932550659802944090446380",
            "s": "34284722724265255571958074559638893216924720874449973321969154869317097889584",
            "receiptCumulativeGasUsed": "1493587",
            "receiptGasUsed": "110874",
            "receiptContractAddress": null,
            "receiptRoot": null,
            "receiptStatus": "1"
        }
    ],
    "txsInternal": [
        {
            "from": "0xde2db97d54a3c3b008a097b2260633e6ca7db1af",
            "to": "0xae13d989dac2f0debff460ac112a837c89baa7cd",
            "value": "1000000000000000",
            "gas": "110182",
            "transactionHash": "0xb7989e32038addacbf2a54585c1124e7444d1fb8134764de782da437f8fa07dd",
            "internalTransactionIndex": "0"
        }
    ],
    "erc20Transfers": [
        {
            "transactionHash": "0xb7989e32038addacbf2a54585c1124e7444d1fb8134764de782da437f8fa07dd",
            "logIndex": "27",
            "contract": "0x6f162ff7ef464858606e05855c762eeb751f56b3",
            "from": "0xf0355b92e55adfc147fff04517a17806e02dd969",
            "to": "0xfcdda93b6251beefe41e003e6dd4070ac79f5e2c",
            "value": "974",
            "tokenName": "TEST",
            "tokenSymbol": "TEST",
            "tokenDecimals": "3",
            "valueWithDecimals": "0.974",
            "possibleSpam": true
        }
    ],
    "erc20Approvals": [],
    "nftTokenApprovals": [],
    "nftApprovals": {
        "ERC721": [],
        "ERC1155": []
    },
    "nftTransfers": [],
    "nativeBalances": []
}

I was also able to set up the ngrok successfully, i.e. when I used Postman to call to the ngrok endpoint (exactly the same as what I provided in the Streams API Webhook URL), I can see the console.log. Below is my code for the back-end:

require("dotenv").config();
const express = require("express");
const app = express();
const port = 3001;
app.use(express.json());
app.post("/webhook", async (req, res) => {
  console.log("Webhook endpoint called");
  const webhook = req.body;
  console.log(webhook);
  return res.status(200).json();
});
app.listen(port, () => {
  console.log(`Listening for NFT Transfers on ${port}`);
});

With all of that, when a Transfer() event is emitted and caught by the Streams API (what I got in the payload shown earlier), it doesn’t call to my ngrok webhook endpoint and I don’t see any error message.

Any help is greatly appreciated, if you need any more details, please let me know.

Hey @Noe315

So what’s the issue here :thinking:

You said your streams API is up and running here, so the call to webhook is fine?

Or do you get any other error?

Hi @YosephKS,

The issue is that the Streams API doesn’t call to the webhook endpoint, I get no error message or anything.

When I use Postman to call this endpoint (https://5fb5-27-74-241-82.ap.ngrok.io/webhook), I can see the console.log() message, but when I make a token transfer, the Streams API doesn’t seem to call my webhook endpoint.

Are you sure that you are using the right abi? The abi has to be exactly the same in term of parameter types. If the abi doesn’t match then you will not receive the events. You can check the parameter types of they are indexed or not the same in the abi and in the contract.

Hi @cryptokid,

I’m pretty sure I used the correct ABI, here it is for further reference:

[
	{
		"inputs": [
			{
				"internalType": "string",
				"name": "name",
				"type": "string"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "owner",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "spender",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "value",
				"type": "uint256"
			}
		],
		"name": "Approval",
		"type": "event"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "spender",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "approve",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "owner",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "burn",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "spender",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "subtractedValue",
				"type": "uint256"
			}
		],
		"name": "decreaseAllowance",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"internalType": "address",
				"name": "account",
				"type": "address"
			}
		],
		"name": "grantRole",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "spender",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "addedValue",
				"type": "uint256"
			}
		],
		"name": "increaseAllowance",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "mint",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "previousOwner",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "OwnershipTransferred",
		"type": "event"
	},
	{
		"inputs": [],
		"name": "renounceOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"internalType": "address",
				"name": "account",
				"type": "address"
			}
		],
		"name": "renounceRole",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"internalType": "address",
				"name": "account",
				"type": "address"
			}
		],
		"name": "revokeRole",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"indexed": true,
				"internalType": "bytes32",
				"name": "previousAdminRole",
				"type": "bytes32"
			},
			{
				"indexed": true,
				"internalType": "bytes32",
				"name": "newAdminRole",
				"type": "bytes32"
			}
		],
		"name": "RoleAdminChanged",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "account",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "sender",
				"type": "address"
			}
		],
		"name": "RoleGranted",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "account",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "sender",
				"type": "address"
			}
		],
		"name": "RoleRevoked",
		"type": "event"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "newAdmin",
				"type": "address"
			}
		],
		"name": "setAdmin",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "transfer",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "from",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "value",
				"type": "uint256"
			}
		],
		"name": "Transfer",
		"type": "event"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "from",
				"type": "address"
			},
			{
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"internalType": "uint256",
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "transferFrom",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "newOwner",
				"type": "address"
			}
		],
		"name": "transferOwnership",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "ADMIN_ROLE",
		"outputs": [
			{
				"internalType": "bytes32",
				"name": "",
				"type": "bytes32"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "owner",
				"type": "address"
			},
			{
				"internalType": "address",
				"name": "spender",
				"type": "address"
			}
		],
		"name": "allowance",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "account",
				"type": "address"
			}
		],
		"name": "balanceOf",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "decimals",
		"outputs": [
			{
				"internalType": "uint8",
				"name": "",
				"type": "uint8"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "DEFAULT_ADMIN_ROLE",
		"outputs": [
			{
				"internalType": "bytes32",
				"name": "",
				"type": "bytes32"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			}
		],
		"name": "getRoleAdmin",
		"outputs": [
			{
				"internalType": "bytes32",
				"name": "",
				"type": "bytes32"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes32",
				"name": "role",
				"type": "bytes32"
			},
			{
				"internalType": "address",
				"name": "account",
				"type": "address"
			}
		],
		"name": "hasRole",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "name",
		"outputs": [
			{
				"internalType": "string",
				"name": "",
				"type": "string"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [
			{
				"internalType": "bytes4",
				"name": "interfaceId",
				"type": "bytes4"
			}
		],
		"name": "supportsInterface",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "symbol",
		"outputs": [
			{
				"internalType": "string",
				"name": "",
				"type": "string"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "totalSupply",
		"outputs": [
			{
				"internalType": "uint256",
				"name": "",
				"type": "uint256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	}
]

I used this code to deploy my contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Token is ERC20, AccessControl, Ownable {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");

    constructor(string memory name, uint256 amount) ERC20(name, name) {
        _mint(msg.sender, amount * (10**uint256(decimals())));
        setAdmin(msg.sender);
    }

    function decimals() public view virtual override returns (uint8) {
        return 3;
    }
    
    function setAdmin(address newAdmin) public onlyOwner {
        _setupRole(ADMIN_ROLE, newAdmin);
    }
    
    function mint(address to, uint amount) external onlyRole(ADMIN_ROLE) {
        _mint(to, amount);
    }

    function burn(address owner, uint amount) external onlyRole(ADMIN_ROLE) {
        _burn(owner, amount);
    }
}

And I get the ABI from Remix IDE.

can you paste only the abi for that event that you are trying to get?

Here it is:

{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "from",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "value",
				"type": "uint256"
			}
		],
		"name": "Transfer",
		"type": "event"
	}

I can get the event just fine, because in the Streams API UI, I can see the payload was displayed correctly, however, it seems like it doesn’t call to my ngrok webhook endpoint.

if you use webhook.site instead of ngorok just for testing, what happens?

Did you mean changing “https://5fb5-27-74-241-82.ap.ngrok.io/webhook” to “https://5fb5-27-74-241-82.ap.webhook.site.io/webhook” in the Streams API’s webhook URL? Or did you mean something else?

I mean something else, to completely change the webhook url with an webhook url from another service (webhook.site for example) to see if it works with that webhook url

Okay, I’ve changed the Streams API’s webhook URL to this: https://webhook.site/b27cb00b-2c6b-4482-b520-345a9da39549

And it still doesn’t work, I don’t see any request to that endpoint, whereas if I test the endpoint again with Postman, I can see the request correctly.

ok, then it could be an issue with the stream config if you don’t receive any webhook requests from Streams API

I think I found the culprit, it seems like it won’t call the webhook endpoint because I set it to “Demo” instead of “Prod”. Now the webhook.site is called and I can see the request from Streams API. However, when I update the Streams API’s webhook URL back to my ngrok endpoint, it still won’t call my endpoint. Do you have any idea where I should look? Because it’s weird that I can still call the endpoint using Postman

I don’t know now, try to check if there were any errors when the webhook url was changed. If it had the expected value, is the steam is prod and not demo. You can also check the stream stats to see if there are failed webhook requests, you can also look in history for failed webhook requests.

1 Like

Okay, somehow it worked eventually. Thank you so much for your help.

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.