Topic subscriptions let a subscriber opt in to a topic with fine-grained rules. Each subscription can target specific workflows and include a JSON Logic condition that is evaluated when a workflow is triggered to the topic.
Conditions are evaluated at trigger time , not when the subscription is created. A subscription is always stored; Novu decides at delivery whether the condition matches the incoming event data.
Context-scoped subscriptions
Topic subscriptions can be scoped to a Context , the same metadata you pass when triggering a workflow. This lets one subscriber maintain separate subscriptions to the same topicKey for different tenants, apps, or regions without duplicating topics or workflows.
Novu uses exact-match Context filtering for subscriptions, the same rules as the Inbox :
Context on subscription Context on trigger Subscription included? { tenant: "acme-corp" }{ tenant: "acme-corp" }Yes { tenant: "acme-corp" }{ tenant: "globex" }No None (default) None (default) Yes None (default) { tenant: "acme-corp" }No { tenant: "acme-corp" }None (default) No
Both sides must match: key for key and value for value. Passing Context on only the subscription or only the trigger is not enough.
When you omit Context on a subscription or trigger, Novu treats it as the default (no Context) scope, not as a wildcard that matches all Contexts.
At subscription create time , Context is stored on the subscription record:
Server API : pass a context object in the create request body (same shape as trigger Context ).
@novu/js / @novu/react : pass context when initializing the Novu client or <NovuProvider>. The session inherits that Context for all subscription API calls.
At trigger time , pass the same Context on the workflow trigger. Novu resolves topic members whose stored Context matches the trigger Context, then evaluates each matching subscription’s JSON Logic condition.
If Novu auto-generates a subscription identifier and Context is present, the identifier includes a :ctx_ suffix so subscriptions in different Contexts remain unique.
See Contexts for structure, limits (up to five keys per trigger), and how Context differs from payload.
Choose an integration path
Path Auth Best for Topics v2 API API key (Authorization: ApiKey …) Backend provisioning: subscribe users from your server, set conditions programmatically Inbox API Subscriber JWT (via @novu/js / @novu/react) Subscriber self-service: follow buttons, preference centers, custom UIs @novu/jsSubscriber JWT Headless JavaScript/TypeScript apps @novu/reactSubscriber JWT React/Next.js apps with hooks or <Subscription /> components
A subscriber can have up to 10 subscriptions per topic , each with its own identifier and conditions. See the introduction for how subscriptions fit into the notification flow.
Server API (Topics v2)
Use the server API when your backend creates subscriptions on behalf of subscribers, for example during onboarding or when syncing preferences from your database.
Endpoint: POST /v2/topics/{topicKey}/subscriptions
The topic is created automatically if it does not exist. You can pass subscriber IDs directly or use custom subscription identifiers when a subscriber needs multiple subscriptions on the same topic.
Basic subscription
Node.js
Python
Go
PHP
.NET
Java
cURL
import { Novu } from '@novu/api' ;
const novu = new Novu ({ secretKey: process . env . NOVU_SECRET_KEY ! });
await novu . topics . subscriptions . create (
{
subscriptions: [{ identifier: 'user-123-alerts' , subscriberId: 'user-123' }],
preferences: [ 'product-update-workflow' ],
},
'product-updates' ,
);
import os
from novu_py import Novu
with Novu( secret_key = os.getenv( "NOVU_SECRET_KEY" , "" )) as novu:
novu.topics.subscriptions.create(
topic_key = "product-updates" ,
create_topic_subscriptions_request_dto = {
"subscriptions" : [
{ "identifier" : "user-123-alerts" , "subscriberId" : "user-123" }
],
"preferences" : [ "product-update-workflow" ],
},
)
_ , err := s . Topics . Subscriptions . Create ( ctx , "product-updates" ,
components . CreateTopicSubscriptionsRequestDto {
Subscriptions : [] components . TopicSubscriberIdentifierDto {
{ Identifier : "user-123-alerts" , SubscriberID : "user-123" },
},
Preferences : [] interface {}{ "product-update-workflow" },
}, nil )
$sdk -> topics -> subscriptions -> create (
topicKey : 'product-updates' ,
createTopicSubscriptionsRequestDto : new Components\ CreateTopicSubscriptionsRequestDto (
subscriptions : [
new Components\ TopicSubscriberIdentifierDto (
identifier : 'user-123-alerts' ,
subscriberId : 'user-123' ,
),
],
preferences : [ 'product-update-workflow' ],
),
);
await sdk . Topics . Subscriptions . CreateAsync ( "product-updates" , new CreateTopicSubscriptionsRequestDto {
Subscriptions = new List < TopicSubscriberIdentifierDto > {
new TopicSubscriberIdentifierDto {
Identifier = "user-123-alerts" ,
SubscriberId = "user-123" ,
},
},
Preferences = new List < object > { "product-update-workflow" },
});
novu . topics (). subscriptions (). create ( "product-updates" )
. body ( CreateTopicSubscriptionsRequestDto . builder ()
. subscriptions ( List . of (
TopicSubscriberIdentifierDto . builder ()
. identifier ( "user-123-alerts" )
. subscriberId ( "user-123" )
. build ()))
. preferences ( List . of ( "product-update-workflow" ))
. build ())
. call ();
curl -X POST 'https://api.novu.co/v2/topics/product-updates/subscriptions' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"subscriptions": [
{ "identifier": "user-123-alerts", "subscriberId": "user-123" }
],
"preferences": ["product-update-workflow"]
}'
Subscription with Context
Scope a subscription to a tenant (or any Context type) by passing context in the request body:
Node.js
Python
Go
PHP
.NET
Java
cURL
await novu . topics . subscriptions . create (
{
subscriptions: [{ identifier: 'user-123-acme-alerts' , subscriberId: 'user-123' }],
preferences: [ 'product-update-workflow' ],
context: {
tenant: { id: 'acme-corp' , data: { name: 'Acme Corporation' , plan: 'enterprise' } },
},
},
'product-updates' ,
);
novu.topics.subscriptions.create(
topic_key = "product-updates" ,
create_topic_subscriptions_request_dto = {
"subscriptions" : [
{ "identifier" : "user-123-acme-alerts" , "subscriberId" : "user-123" }
],
"preferences" : [ "product-update-workflow" ],
"context" : {
"tenant" : {
"id" : "acme-corp" ,
"data" : { "name" : "Acme Corporation" , "plan" : "enterprise" },
}
},
},
)
_ , err := s . Topics . Subscriptions . Create ( ctx , "product-updates" ,
components . CreateTopicSubscriptionsRequestDto {
Subscriptions : [] components . TopicSubscriberIdentifierDto {
{ Identifier : "user-123-acme-alerts" , SubscriberID : "user-123" },
},
Preferences : [] interface {}{ "product-update-workflow" },
Context : map [ string ] interface {}{
"tenant" : map [ string ] interface {}{
"id" : "acme-corp" ,
"data" : map [ string ] interface {}{
"name" : "Acme Corporation" ,
"plan" : "enterprise" ,
},
},
},
}, nil )
$sdk -> topics -> subscriptions -> create (
topicKey : 'product-updates' ,
createTopicSubscriptionsRequestDto : new Components\ CreateTopicSubscriptionsRequestDto (
subscriptions : [
new Components\ TopicSubscriberIdentifierDto (
identifier : 'user-123-acme-alerts' ,
subscriberId : 'user-123' ,
),
],
preferences : [ 'product-update-workflow' ],
context : [
'tenant' => [
'id' => 'acme-corp' ,
'data' => [ 'name' => 'Acme Corporation' , 'plan' => 'enterprise' ],
],
],
),
);
await sdk . Topics . Subscriptions . CreateAsync ( "product-updates" , new CreateTopicSubscriptionsRequestDto {
Subscriptions = new List < TopicSubscriberIdentifierDto > {
new TopicSubscriberIdentifierDto {
Identifier = "user-123-acme-alerts" ,
SubscriberId = "user-123" ,
},
},
Preferences = new List < object > { "product-update-workflow" },
Context = new Dictionary < string , object > {
[ "tenant" ] = new Dictionary < string , object > {
[ "id" ] = "acme-corp" ,
[ "data" ] = new Dictionary < string , object > {
[ "name" ] = "Acme Corporation" ,
[ "plan" ] = "enterprise" ,
},
},
},
});
novu . topics (). subscriptions (). create ( "product-updates" )
. body ( CreateTopicSubscriptionsRequestDto . builder ()
. subscriptions ( List . of (
TopicSubscriberIdentifierDto . builder ()
. identifier ( "user-123-acme-alerts" )
. subscriberId ( "user-123" )
. build ()))
. preferences ( List . of ( "product-update-workflow" ))
. context ( Map . of (
"tenant" , Map . of (
"id" , "acme-corp" ,
"data" , Map . of (
"name" , "Acme Corporation" ,
"plan" , "enterprise" ))))
. build ())
. call ();
curl -X POST 'https://api.novu.co/v2/topics/product-updates/subscriptions' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"subscriptions": [
{ "identifier": "user-123-acme-alerts", "subscriberId": "user-123" }
],
"preferences": ["product-update-workflow"],
"context": {
"tenant": {
"id": "acme-corp",
"data": { "name": "Acme Corporation", "plan": "enterprise" }
}
}
}'
When you trigger the workflow to the topic, pass the same Context so this subscription is included:
await novu . trigger ({
workflowId: 'product-update-workflow' ,
to: { type: 'Topic' , topicKey: 'product-updates' },
payload: { status: 'completed' },
context: {
tenant: { id: 'acme-corp' },
},
});
Subscription with a JSON Logic condition
Attach a condition to a workflow preference. When you trigger the workflow to the topic, only subscriptions whose condition evaluates to true receive the notification.
Node.js
Python
Go
PHP
.NET
Java
cURL
await novu . topics . subscriptions . create (
{
subscriptions: [{ identifier: 'user-123-premium' , subscriberId: 'user-123' }],
preferences: [
{
filter: { workflowIds: [ 'product-update-workflow' ] },
condition: {
'===' : [{ var: 'payload.tier' }, 'premium' ],
},
},
],
},
'product-updates' ,
);
novu.topics.subscriptions.create(
topic_key = "product-updates" ,
create_topic_subscriptions_request_dto = {
"subscriptions" : [
{ "identifier" : "user-123-premium" , "subscriberId" : "user-123" }
],
"preferences" : [
{
"filter" : { "workflowIds" : [ "product-update-workflow" ]},
"condition" : {
"===" : [{ "var" : "payload.tier" }, "premium" ]
},
}
],
},
)
_ , err := s . Topics . Subscriptions . Create ( ctx , "product-updates" ,
components . CreateTopicSubscriptionsRequestDto {
Subscriptions : [] components . TopicSubscriberIdentifierDto {
{ Identifier : "user-123-premium" , SubscriberID : "user-123" },
},
Preferences : [] interface {}{
map [ string ] interface {}{
"filter" : map [ string ] interface {}{
"workflowIds" : [] string { "product-update-workflow" },
},
"condition" : map [ string ] interface {}{
"===" : [] interface {}{
map [ string ] interface {}{ "var" : "payload.tier" },
"premium" ,
},
},
},
},
}, nil )
$sdk -> topics -> subscriptions -> create (
topicKey : 'product-updates' ,
createTopicSubscriptionsRequestDto : new Components\ CreateTopicSubscriptionsRequestDto (
subscriptions : [
new Components\ TopicSubscriberIdentifierDto (
identifier : 'user-123-premium' ,
subscriberId : 'user-123' ,
),
],
preferences : [
[
'filter' => [ 'workflowIds' => [ 'product-update-workflow' ]],
'condition' => [
'===' => [
[ 'var' => 'payload.tier' ],
'premium' ,
],
],
],
],
),
);
await sdk . Topics . Subscriptions . CreateAsync ( "product-updates" , new CreateTopicSubscriptionsRequestDto {
Subscriptions = new List < TopicSubscriberIdentifierDto > {
new TopicSubscriberIdentifierDto {
Identifier = "user-123-premium" ,
SubscriberId = "user-123" ,
},
},
Preferences = new List < object > {
new Dictionary < string , object > {
[ "filter" ] = new Dictionary < string , object > {
[ "workflowIds" ] = new [] { "product-update-workflow" },
},
[ "condition" ] = new Dictionary < string , object > {
[ "===" ] = new object [] {
new Dictionary < string , object > { [ "var" ] = "payload.tier" },
"premium" ,
},
},
},
},
});
novu . topics (). subscriptions (). create ( "product-updates" )
. body ( CreateTopicSubscriptionsRequestDto . builder ()
. subscriptions ( List . of (
TopicSubscriberIdentifierDto . builder ()
. identifier ( "user-123-premium" )
. subscriberId ( "user-123" )
. build ()))
. preferences ( List . of (
Map . of (
"filter" , Map . of ( "workflowIds" , List . of ( "product-update-workflow" )),
"condition" , Map . of (
"===" , List . of (
Map . of ( "var" , "payload.tier" ),
"premium" )))))
. build ())
. call ();
curl -X POST 'https://api.novu.co/v2/topics/product-updates/subscriptions' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"subscriptions": [
{ "identifier": "user-123-premium", "subscriberId": "user-123" }
],
"preferences": [
{
"filter": { "workflowIds": ["product-update-workflow"] },
"condition": {
"===": [{ "var": "payload.tier" }, "premium"]
}
}
]
}'
Other server endpoints
Method Endpoint Description GET/v2/topics/{topicKey}/subscriptionsList subscriptions on a topic GET/v2/topics/{topicKey}/subscriptions/{identifier}Get one subscription PATCH/v2/topics/{topicKey}/subscriptions/{identifier}Update name or preferences DELETE/v2/topics/{topicKey}/subscriptionsRemove subscriptions GET/v2/subscribers/{subscriberId}/subscriptionsList all topic subscriptions for a subscriber
API reference Full request and response schemas for topic subscription endpoints.
Inbox API
The Inbox API is the subscriber-facing surface that powers @novu/js and @novu/react. It uses a subscriber session token (created from your applicationIdentifier and subscriberId) instead of an API key.
Method Endpoint Description POST/v1/inbox/topics/{topicKey}/subscriptionsCreate a subscription for the authenticated subscriber GET/v1/inbox/topics/{topicKey}/subscriptionsList the subscriber’s subscriptions on a topic GET/v1/inbox/topics/{topicKey}/subscriptions/{identifier}Get one subscription PATCH/v1/inbox/topics/{topicKey}/subscriptions/{identifier}Update subscription DELETE/v1/inbox/topics/{topicKey}/subscriptions/{identifier}Delete subscription PATCH/v1/inbox/subscriptions/{identifier}/preferences/{workflowId}Update a single workflow preference
You typically do not call these endpoints directly. Use the SDKs below instead.
@novu/js
Initialize the client with the subscriber’s credentials. Pass context when the subscription belongs to a specific tenant or app scope:
import { Novu } from '@novu/js' ;
const novu = new Novu ({
subscriberId: 'user-123' ,
applicationIdentifier: 'YOUR_APPLICATION_IDENTIFIER' ,
context: {
tenant: { id: 'acme-corp' , data: { name: 'Acme Corporation' , plan: 'enterprise' } },
},
});
Then use novu.subscriptions:
Create a conditional subscription
const { data , error } = await novu . subscriptions . create ({
topicKey: 'product-updates' ,
identifier: 'user-123-premium-alerts' ,
preferences: [
{
workflowId: 'product-update-workflow' ,
condition: {
'===' : [{ var: 'payload.tier' }, 'premium' ],
},
},
],
});
Update a condition on an existing subscription
const { data : subscription } = await novu . subscriptions . get ({
topicKey: 'product-updates' ,
identifier: 'user-123-premium-alerts' ,
});
await subscription ?. updatePreference ({
workflowId: 'product-update-workflow' ,
value: {
'===' : [{ var: 'payload.tier' }, 'enterprise' ],
},
});
List, update, and delete
// List all subscriptions on a topic
const { data : subscriptions } = await novu . subscriptions . list ({ topicKey: 'product-updates' });
// Update subscription metadata and preferences
await novu . subscriptions . update ({
topicKey: 'product-updates' ,
identifier: 'user-123-premium-alerts' ,
preferences: [{ workflowId: 'product-update-workflow' , enabled: false }],
});
// Delete
await novu . subscriptions . delete ({
topicKey: 'product-updates' ,
identifier: 'user-123-premium-alerts' ,
});
@novu/js reference Full Subscriptions module API.
@novu/react
The React SDK exposes the same operations as hooks and pre-built components.
Headless hooks
import {
useSubscription ,
useCreateSubscription ,
useUpdateSubscription ,
useRemoveSubscription ,
} from '@novu/react' ;
function SubscriptionSettings () {
const topicKey = 'product-updates' ;
const identifier = 'user-123-premium-alerts' ;
const { subscription , isLoading } = useSubscription ({ topicKey , identifier });
const { create , isCreating } = useCreateSubscription ();
const handleSubscribe = async () => {
await create ({
topicKey ,
identifier ,
preferences: [
{
workflowId: 'product-update-workflow' ,
condition: {
'===' : [{ var: 'payload.tier' }, 'premium' ],
},
},
],
});
};
if ( isLoading ) return < div > Loading… </ div > ;
return (
< button onClick = { handleSubscribe } disabled = { isCreating || !! subscription } >
{ subscription ? 'Subscribed' : 'Subscribe to updates' }
</ button >
);
}
For the full hook reference, see Headless hooks .
Pre-built components
If you want a ready-made subscribe/preferences UI, use <Subscription />, <SubscriptionButton />, and <SubscriptionPreferences /> inside <NovuProvider>:
import { NovuProvider , Subscription , SubscriptionButton , SubscriptionPreferences } from '@novu/react' ;
export function SubscriptionSettings () {
return (
< NovuProvider
subscriber = "user-123"
applicationIdentifier = "YOUR_APPLICATION_IDENTIFIER"
context = { {
tenant: { id: 'acme-corp' , data: { name: 'Acme Corporation' , plan: 'enterprise' } },
} }
>
< Subscription topicKey = "product-updates" identifier = "user-123-premium-alerts" >
< SubscriptionButton />
< SubscriptionPreferences />
</ Subscription >
</ NovuProvider >
);
}
See the quickstart for setup steps. For Context on <NovuProvider>, see Inbox with Context .
JSON Logic conditions
Subscription conditions use JSON Logic , the same rule format as step conditions . Reference trigger payload fields with { "var": "payload.<path>" }.
Condition variables
Topic subscription conditions are evaluated against the workflow trigger payload only. Reference fields with { "var": "payload.<path>" }.
Namespace Source Examples payload.*Trigger payload payload.tier, payload.status, payload.category
Topic subscription conditions are evaluated at trigger time when fanning out a workflow to a topic. Only payload.* is in scope. For rules that depend on subscriber profile, workflow metadata, Context, or prior step results, use step conditions instead.
Do not confuse Context (the Novu feature used to scope subscriptions and Inbox sessions) with condition variables. Context matching happens before JSON Logic runs: Novu selects subscriptions whose stored Context exactly matches the trigger Context. Payload holds event-specific data for a single run and is what conditions read.
Syntax rules
Use dot paths in { "var": "..." }. For example, { "var": "payload.tier" }.
Do not use Liquid/template syntax ({{payload.tier}}) in conditions. That syntax is for workflow step content, not JSON Logic rules.
Do not omit the payload. prefix (for example { "var": "tier" }). Always use the full path such as payload.tier.
Examples
Tier is premium:
{
"===" : [{ "var" : "payload.tier" }, "premium" ]
}
Status and price combined:
{
"and" : [
{ "==" : [{ "var" : "payload.status" }, "completed" ] },
{ ">" : [{ "var" : "payload.price" }, 100 ] }
]
}
Multiple matching rules (OR):
{
"or" : [
{
"===" : [{ "var" : "payload.tier" }, "premium" ]
},
{
"===" : [{ "var" : "payload.tier" }, "enterprise" ]
}
]
}
condition vs enabled
Field Behavior conditionJSON Logic rule evaluated against the trigger payload. When present, enabled is ignored during evaluation. enabledSimple on/off toggle. Used only when no condition is set. Defaults to true if omitted.
When creating preferences, use any of these shapes:
// Workflow ID shorthand: enables the workflow with no condition
"workflow-identifier"
// Single workflow with condition or enabled flag
{
"workflowId" : "product-update-workflow" ,
"condition" : { "===" : [{ "var" : "payload.tier" }, "premium" ] }
}
// Group filter: applies to workflows matching IDs or tags
{
"filter" : { "workflowIds" : [ "workflow-mongo-id" ], "tags" : [ "alerts" ] },
"condition" : { "==" : [{ "var" : "payload.status" }, "active" ] }
}
filter.workflowIds accepts either the workflow MongoDB _id or the workflow trigger identifier.
End-to-end flow
Create a subscription with a condition
Subscribe a user to a topic and attach a JSON Logic rule to a workflow preference (server API, @novu/js, or @novu/react).
Trigger the workflow to the topic
Send the workflow to the topic topicKey. Include fields your condition references in payload, and pass the same Context as the subscription when the subscription is Context-scoped: await novu . trigger ({
workflowId: 'product-update-workflow' ,
to: { type: 'Topic' , topicKey: 'product-updates' },
payload: {
tier: 'premium' ,
status: 'completed' ,
category: 'billing' ,
},
context: {
tenant: { id: 'acme-corp' },
},
});
Novu matches Context, then evaluates conditions
Novu first selects topic subscriptions whose stored Context exactly matches the trigger Context (including the default/no-Context case). For each matching subscription, Novu evaluates the JSON Logic condition against the trigger payload only. Subscriptions that pass receive the notification; others are skipped.
Global and workflow channel preferences still apply. If a subscriber has disabled email globally, they will not receive email even when a subscription condition matches.
Limits
10 subscriptions per subscriber per topic (across all Context scopes)
512 characters max for a custom subscription identifier
100 subscribers per create request (batch limit)