SNS Notifications
Learn how to get real-time push notifications from the Lens protocol.
Lens utilizes Amazon Simple Notification Service (SNS) to push notification events, enabling easy integration for third-party providers. This service merely broadcasts data from the chain to your server without applying any filters.
Initial Setup
To set up SNS notifications, you need to provide the Lens Protocol team with two webhook URLs for SNS - one for Mainnet notifications and another for Testnet notifications. These URLs will be used to authenticate and initiate a handshake request. Ensure your code is configured to listen for incoming notifications. An example is provided below.
To get in touch with the Lens Protocol team, join the Lens Developer Garden Telegram group.
The example below demonstrates how to create a webhook using Express.js.
import bodyParser from "body-parser";import express from "express";import fetch from "node-fetch";
const app = express();const port = 8080;
app.use(bodyParser.urlencoded({ extended: false }));app.use(bodyParser.json());
app.post("/lens/notifications", async (req, res) => { const buffers = [];
for await (const chunk of req) { buffers.push(chunk); }
const data = Buffer.concat(buffers).toString(); // example https://docs.aws.amazon.com/connect/latest/adminguide/sns-payload.html const payload = JSON.parse(data);
// if you already done the handshake you will get a Notification type // example below: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html // { // "Type" : "Notification", // "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Subject" : "My First Message", // "Message" : "Hello world!", // "Timestamp" : "2012-05-02T00:54:06.655Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEw6JRN...", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", // "UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96" // } if (payload.Type === "Notification") { console.log("SNS message is a notification ", payload); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); res.sendStatus(200); return; }
// only need to do this the first time this is doing an handshake with the sns client // example below: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html // { // "Type" : "SubscriptionConfirmation", // "MessageId" : "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", // "Token" : "2336412f37...", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Message" : "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", // "SubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37...", // "Timestamp" : "2012-04-26T20:45:04.751Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem" // } if (payload.Type === "SubscriptionConfirmation") { const url = payload.SubscribeURL; const response = await fetch(url); if (response.status === 200) { console.log("Subscription confirmed"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); res.sendStatus(200); return; } else { console.error("Subscription failed"); res.sendStatus(500); return; } }
console.log("Received message from SNS", payload);
// if it gets this far it is a unsubscribe request // { // "Type" : "UnsubscribeConfirmation", // "MessageId" : "47138184-6831-46b8-8f7c-afc488602d7d", // "Token" : "2336412f37...", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Message" : "You have chosen to deactivate subscription arn:aws:sns:us-west-2:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", // "SubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb6...", // "Timestamp" : "2012-04-26T20:06:41.581Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEHXgJm...", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem" // }});
app.listen(port, () => console.log("SNS notification listening on port " + port + "!"));
Signatures
It's important to verify the authenticity of any notification, subscription confirmation, or unsubscribe confirmation message sent by Amazon SNS.
You can do this by using the information contained in the Amazon SNS message to recreate the string to sign and the signature. By matching the recreated signature with the one sent by Amazon SNS, you can verify the message's contents.
For more information, refer to the Verifying the signatures of Amazon SNS messages guide.
const bent = require("bent");const getBuffer = bent("buffer");const crypto = require("crypto");const debug = require("debug")("verify-aws-sns-signature");const parseUrl = require("parse-url");const assert = require("assert");
async function validatePayload(payload) { const { SigningCertURL, Signature, Message, MessageId, SubscribeURL, Subject, Timestamp, Token, TopicArn, Type, } = payload;
// validate SubscribeURL const url = parseUrl(SigningCertURL); assert.ok( /^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/.test(url.resource), `SigningCertURL host is not a valid AWS SNS host: ${SigningCertURL}` );
try { debug(`retrieving AWS certificate from ${SigningCertURL}`);
const x509 = await getBuffer(SigningCertURL); const publicKey = crypto.createPublicKey(x509); const signature = Buffer.from(Signature, "base64"); const stringToSign = ( "Notification" === Type ? [ { Message }, { MessageId }, { Subject }, { Timestamp }, { TopicArn }, { Type }, ] : [ { Message }, { MessageId }, { SubscribeURL }, { Timestamp }, { Token }, { TopicArn }, { Type }, ] ).reduce((acc, el) => { const key = el.keys()[0]; acc += key + "\n" + el[key] + "\n"; }, "");
debug(`string to sign: ${stringToSign}`); const verified = crypto.verify( "sha1WithRSAEncryption", Buffer.from(stringToSign, "utf8"), publicKey, signature ); debug( `signature ${verified ? "has been verified" : "failed verification"}` ); return verified; } catch (err) { return false; }}
module.exports = { validatePayload };
Message Types
The Message field is the key property to pay attention to. This field contains the information that will be pushed to SNS. It is always an object, presented as a stringified JSON string.
All messages will adhere to the following structure:
export interface PublishMessage { type: SnsMessageTypes; data: any;}
The type corresponds to the response type that will be defined later in this guide.
Events
Referring to the Message structure outlined above, this section will focus on the data field. All examples will demonstrate the response type for this field, not the entire SNS object. For clarity, the guide will present JSON examples as if the Message has already been parsed (e.g., via JSON.parse).
This guide assumes that you already understand the Lens Primitives, the events emitted by the protocol, and the purpose of each data property.
Profile Events
FOLLOW_NFT_DEPLOYED
This event is triggered when the Follow NFT contract is deployed. Keep in mind that Follow NFT contracts are lazy-loaded, meaning they are only deployed when the first follow occurs.
METADATA_PROFILE_COMPLETE
A notification is emitted when the profile metadata has been snapshotted and indexed by the API.
For the exact metadata type definition, refer to the @lens-protocol/metadata package.
METADATA_PROFILE_FAILED
A notification is emitted when the snapshotting of the profile metadata fails. This could occur if the metadata is invalid or unreachable. The snapshot process will retry periodically for 20 minutes before marking it as failed. However, if the metadata is invalid, it won't retry and will immediately mark it as failed, providing an error reason.
PROFILE_BLOCKED
This event is emitted when one profile blocks another profile.
PROFILE_UNBLOCKED
This event is emitted when one profile unblocks another profile.
PROFILE_CREATED
This event is emitted when a new profile is created.
PROFILE_CREATOR_ALLOWLISTED
This event is emitted when a profile creator is added to the allowlist.
PROFILE_FOLLOWED
This event is emitted when a profile starts following another profile.
PROFILE_UNFOLLOWED
This event is emitted when a profile stops following another profile.
PROFILE_GUARDIAN_STATE_CHANGED
This event is emitted when the state of the profile guardian changes.
PROFILE_HANDLE_LINKED
This event is emitted when a handle is linked to a profile.
Type
interface TokenInfo { id: string; collection: string;}
interface Response { handle: TokenInfo; token: TokenInfo; transactionExecutor: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_HANDLE_MINTED
This event is emitted when a new handle is minted.
Type
interface Response { handle: string; namespace: string; handleId: string; to: number; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_HANDLE_UNLINKED
This event is emitted when a handle is unlinked from a profile.
Type
interface TokenInfo { id: string; collection: string;}
interface Response { handle: TokenInfo; token: TokenInfo; transactionExecutor: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_MANAGER_CHANGED
This event is emitted when the profile manager changes.
Type
interface Response { delegatorProfileId: string; configNumber: string; delegatedExecutors: string[]; approvals: boolean[]; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_MANAGER_CONFIG_APPLIED
This event is emitted when the profile manager configuration has been applied.
Type
interface Response { delegatorProfileId: string; configNumber: string[]; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_METADATA_SET
This event is emitted when someone updates the metadata for a profile.
Please note that the emission of this event does not guarantee that the updated data adheres to the Metadata Standards. The update will be indexed, but if the data does not comply with the Metadata Standards, it will not be updated and made available in the API.
You can use the METADATA_PROFILE_COMPLETE and METADATA_PROFILE_FAILED notifications to monitor these states.
PROFILE_TRANSFERRED
This event is emitted when the ownership of a profile is transferred to a different wallet.
Type
interface Response { from: string; to: string; tokenId: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROFILE_REPORTED
This event is emitted when a profile is reported.
Type
interface Response { profileId: string; reporterProfileId: string; timestamp: number; reason: string; subreason: string; additionalComments: string | null;}
Publication Events
POST_CREATED
This event is emitted when a new post is created.
Please note that the emission of this event does not guarantee that the posted data adheres to the Metadata Standards. The post will be indexed, but if the data does not comply with the Metadata Standards, it will not be updated and made available in the API.
You can use the METADATA_PUBLICATION_COMPLETE and METADATA_PUBLICATION_FAILED notifications to monitor these states.
COMMENT_CREATED
This event is emitted when someone comments on a publication.
Please note that the emission of this event does not guarantee that the comment data adheres to the Metadata Standards. The comment will be indexed, but if the data does not comply with the Metadata Standards, it will not be updated and made available in the API.
You can use the METADATA_PUBLICATION_COMPLETE and METADATA_PUBLICATION_FAILED notifications to monitor these states.
MIRROR_CREATED
This event is emitted when a profile mirrors a publication, inheriting the mirrored publication's metadata.
Please note that the emission of this event does not guarantee that the mirrored publication metadata adheres to the Metadata Standards. The mirrored publication will be indexed, but if the data does not comply with the Metadata Standards, it will not be updated and made available in the API.
You can use the METADATA_PUBLICATION_COMPLETE and METADATA_PUBLICATION_FAILED notifications to monitor these states.
Please note that a Mirror differs from a Post, Comment, or Quote because it doesn't have any associated metadata. To understand when it was snapshotted and made viewable, you must link the mirrored publication to the mirror itself.
QUOTE_CREATED
This event is emitted when someone quotes a publication.
Please note that the emission of this event does not guarantee that the quoted data adheres to the Metadata Standards. The quote will be indexed, but if the data does not comply with the Metadata Standards, it will not be updated and made available in the API.
You can use the METADATA_PUBLICATION_COMPLETE and METADATA_PUBLICATION_FAILED notifications to monitor these states.
METADATA_PUBLICATION_COMPLETE
A notification is emitted when the publication metadata has been snapshotted and indexed by the API.
For the exact metadata type definition, refer to the @lens-protocol/metadata package.
METADATA_PUBLICATION_FAILED
A notification is emitted when the snapshotting of the publication metadata fails. This could occur if the metadata is invalid or unreachable. The snapshot process will retry periodically for 20 minutes before marking it as failed. However, if the metadata is invalid, it won't retry and will immediately mark it as failed, providing an error reason.
PROFILE_MENTIONED
This event is emitted when a profile is mentioned in a publication. It serves to notify the mentioned profile about the reference in the publication.
Type
interface Response { profileIdPointed: string; pubIdPointed: string; // pub id are counters of the publication, so they clash with profiles // on the server, we build up our publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses // this will be the publication the mention was created on serverPubIdPointer: string; handle: string; profileId: string; content: string; contentURI: string; snapshotLocation: string; timestamp: Date;}
PUBLICATION_ACTED
This emits when a profile acted on a publication.
Type
interface Response { publicationActionParams: { publicationActedProfileId: string; publicationActedId: string; actorProfileId: string; referrerProfileIds: string[]; referrerPubIds: string[]; actionModuleAddress: string; actionModuleData: string; }; actionModuleReturnData: string; transactionExecutor: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PUBLICATION_COLLECTED
This event is emitted when a publication is collected.
Type
interface Response { // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string; collectedProfileId: string; collectedPubId: string; collectorProfileId: string; nftRecipient: string; collectActionData: string; collectActionResult: string; collectNFT: string; tokenId: string; collectNFT: string; collectNFT: string; transactionExecutor: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PUBLICATION_REACTION_ADDED
This event is emitted when a profile adds a reaction to a publication.
Please note that reactions function as a toggle. For instance, if a profile upvotes a publication and then downvotes it, the initial upvote is invalidated. A profile is only allowed to have one active reaction on a publication at a time.
Type
interface Response { profileId: string; // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string; type: string; // unix timestamp reactedAt: number;}
PUBLICATION_REACTION_REMOVED
This event is emitted when a profile removes a reaction from a publication.
Type
interface Response { profileId: string; // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string; type: string;}
This event is emitted when a profile hides a publication. Please note that while the content and media of the publication are hidden, the API still returns these publications to maintain thread integrity. However, these hidden publications do not appear in timelines, search results, profiles, or explore queries.
Type
interface Response { profileId: string; // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string;}
PUBLICATION_REPORTED
This event is emitted when a publication is reported.
Type
interface Response { // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string; profileId: string; reason: string; subreason: string; additionalComments: string | null;}
COLLECT_NFT_TRANSFERRED
This event is emitted when the collect NFT is transferred to a new owner. Please note that this event is also emitted when the NFT is minted.
Type
interface Response { profileId: string; pubId: string; // pub id are counters of the publication so they clash with profiles // on the server we build up our own publication id to allow it to be searchable // this is {profileId}-{pubId} and is used in all our API calls and responses serverPubId: string; // each collect NFT minted has a token id the first one is 1 then 2 just like a normal NFT project collectNFTId: string; from: string; to: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
Protocol Events
COLLECT_MODULE_REGISTERED
This event is emitted when a Collect Action Module is registered.
INDEXER_MODULE_GLOBALS_TREASURY_FEE_SET
This event is emitted when the global treasury fee for the module is set.
COLLECT_NFT_DEPLOYED
This event is emitted when the collect NFT is deployed. Keep in mind that Collect NFTs are lazy-loaded, meaning they are only deployed when the first collect occurs.
COLLECT_OPEN_ACTION_MODULE_ALLOWLISTED
This event is emitted when an Open Action Module is either added to or removed from the allowlist.
Type
interface Response { moduleName: string; collectModule: string; whitelist: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
CURRENCY_MODULE_ALLOWLISTED
This event is emitted when a currency module is either added to or removed from the allowlist.
FOLLOW_MODULE_ALLOWLISTED
This event is emitted when a Follow Module is allowlisted or removed from the allowlist.
FOLLOW_MODULE_SET
This event is emitted when a profile modifies their Follow Module.
REFERENCE_MODULE_ALLOWLISTED
This event is emitted when a Reference Module is allowlisted or removed from the allowlist.
OPEN_ACTION_MODULE_ALLOWLISTED
This event is emitted when a new Open Action Module is allowlisted or removed from the allowlist.
Type
interface Response { actionModule: string; openActionModuleName: string; id: string; whitelisted: boolean; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROTOCOL_LENS_HUB_ADDRESS_SIG_NONCE_UPDATED
This event is emitted when the signature nonce of a Lens Hub address is updated.
Type
interface Response { signer: string; nonce: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROTOCOL_TOKEN_HANDLE_ADDRESS_SIG_NONCE_UPDATED
This event is emitted when the signature nonce of a token handle address is updated.
Type
interface Response { signer: string; nonce: string; timestamp: number; logIndex: number; txHash: string; txIndex: number;}
PROTOCOL_STATE_CHANGED
This event is emitted when there's a change in the state of the protocol. The possible states are:
0: Unpaused
1: Publishing Paused
2: Paused
Media Events
MEDIA_SNAPSHOTTED
This event is emitted when media is successfully snapshotted. This can occur when a publication containing media is posted, or when a profile updates their metadata. In the case of a publication, the source will be the PublicationId. For a profile update, the source will be the ProfileId.
MEDIA_SNAPSHOT_FAILED
This event is emitted when the snapshotting of media fails. This can occur due to a timeout after all retry attempts, if the URL is invalid, or if the content is unavailable at the time (for example, due to a 404 error or unfinished IPFS pinning).