NIP-52
Calendar Events - defines date-based calendar events (kind 31922), time-based calendar events (kind 31923), calendars (kind 31924), and calendar event RSVPs (kind 31925).
Import
import { nip52 } from 'nostr-core'
// or import individual functions
import {
createDateBasedCalendarEventTemplate,
createDateBasedCalendarEvent,
parseDateBasedCalendarEvent,
createTimeBasedCalendarEventTemplate,
createTimeBasedCalendarEvent,
parseTimeBasedCalendarEvent,
createCalendarTemplate,
createCalendarEvent,
parseCalendar,
createCalendarEventRSVPTemplate,
createCalendarEventRSVP,
parseCalendarEventRSVP,
buildCalendarEventAddress,
buildAddressableAddress,
parseAddressableAddress,
calendarEventDays,
isCalendarEvent,
} from 'nostr-core'CalendarEventParticipant Type
type CalendarEventParticipant = {
pubkey: string
relay?: string
role?: string
}NIP-52 participant tags are positional: ["p", pubkey, relay, role]. When a role is present the relay slot is always emitted, empty when unknown, so the role never lands in the relay position:
{ pubkey, role: 'host' } // -> ['p', pubkey, '', 'host']
{ pubkey, relay: 'wss://r.example', role: 'host' } // -> ['p', pubkey, 'wss://r.example', 'host']
{ pubkey, relay: 'wss://r.example' } // -> ['p', pubkey, 'wss://r.example']CalendarReference Type
type CalendarReference = {
address: string
relayHint?: string
}NIP-52 allows an optional relay URL in the third slot of a calendar's member a tags and an RSVP's a / e tags. For events that only live on their author's own relay, the hint is often the difference between a reference resolving and not resolving.
Every parsed object exposes both forms: calendarAddresses / eventAddresses hold plain strings, calendarRefs / eventRefs hold the same entries with their hints. On create, the hinted list wins when both are supplied, so a parse -> create round trip keeps its hints without duplicating tags.
const calendar = nip52.createCalendarTemplate({
identifier: 'work', title: 'Work',
eventRefs: [{ address: '31923:abc:standup', relayHint: 'wss://relay.example' }],
})
// -> ['a', '31923:abc:standup', 'wss://relay.example']Unknown Tag Passthrough
All four parsers collect tags they do not recognize into extraTags, and all four templates re-emit them. A create -> parse -> create round trip is therefore lossless, including the D day-granularity tags kind 31923 requires and any app-specific tags a client carries.
const parsed = nip52.parseTimeBasedCalendarEvent(event)
parsed.days // [19675, 19676, ...] from the D tags
parsed.extraTags // [['x-app-custom', 'v1'], ...]
nip52.createTimeBasedCalendarEventTemplate(parsed) // emits both againDeprecated name Tag Fallback
NIP-52 deprecated name in favour of title in 2023 but keeps it as a read fallback, and clients in the wild still write only name (Coracle's nostrtime for 31923 events, Flockstr for 31924 calendars). The parsers read title ?? name, so those events no longer parse with an empty title. Templates always emit title.
DateBasedCalendarEvent Type
type DateBasedCalendarEvent = {
identifier: string
title: string
start: string // YYYY-MM-DD
end?: string // YYYY-MM-DD (exclusive)
content?: string
summary?: string
image?: string
locations?: string[]
geohash?: string
participants?: CalendarEventParticipant[]
hashtags?: string[]
references?: string[]
calendarAddresses?: string[]
calendarRefs?: CalendarReference[] // `a` tags with relay hints
extraTags?: string[][] // unrecognized tags, preserved
}TimeBasedCalendarEvent Type
type TimeBasedCalendarEvent = {
identifier: string
title: string
start: number // Unix timestamp (seconds)
end?: number // Unix timestamp (seconds, exclusive)
startTzid?: string // IANA timezone identifier
endTzid?: string // IANA timezone identifier
content?: string
summary?: string
image?: string
locations?: string[]
geohash?: string
participants?: CalendarEventParticipant[]
hashtags?: string[]
references?: string[]
calendarAddresses?: string[]
calendarRefs?: CalendarReference[] // `a` tags with relay hints
days?: number[] // `D` day-granularity tags
extraTags?: string[][] // unrecognized tags, preserved
}Calendar Type
type Calendar = {
identifier: string
title: string
content?: string
eventAddresses?: string[] // references to kind 31922 or 31923
eventRefs?: CalendarReference[] // the same, with optional relay hints
extraTags?: string[][] // unrecognized tags, preserved
}CalendarEventRSVP Type
type CalendarEventRSVP = {
identifier: string
calendarEventAddress: string
status: 'accepted' | 'declined' | 'tentative'
calendarEventAddressRelayHint?: string // slot 2 of the `a` tag
eventId?: string
eventIdRelayHint?: string // slot 2 of the `e` tag
freebusy?: 'free' | 'busy'
calendarEventAuthor?: string
content?: string
extraTags?: string[][] // unrecognized tags, preserved
}nip52.createDateBasedCalendarEventTemplate
function createDateBasedCalendarEventTemplate(event: DateBasedCalendarEvent): EventTemplateCreates an unsigned kind 31922 date-based calendar event template for all-day or multi-day events.
const template = nip52.createDateBasedCalendarEventTemplate({
identifier: 'company-holiday-2026',
title: 'Company Holiday Party',
start: '2026-12-20',
end: '2026-12-21',
locations: ['123 Main St, NYC'],
hashtags: ['holiday', 'party'],
})nip52.createDateBasedCalendarEvent
function createDateBasedCalendarEvent(event: DateBasedCalendarEvent, secretKey: Uint8Array): NostrEventCreates and signs a kind 31922 date-based calendar event.
nip52.parseDateBasedCalendarEvent
function parseDateBasedCalendarEvent(event: NostrEvent): DateBasedCalendarEventParses a kind 31922 date-based calendar event.
nip52.createTimeBasedCalendarEventTemplate
function createTimeBasedCalendarEventTemplate(event: TimeBasedCalendarEvent): EventTemplateCreates an unsigned kind 31923 time-based calendar event template. Automatically calculates the D (day-granularity) tags.
const template = nip52.createTimeBasedCalendarEventTemplate({
identifier: 'standup-2026-03-17',
title: 'Daily Standup',
start: 1742212800,
end: 1742214600,
startTzid: 'America/New_York',
locations: ['https://meet.example.com/standup'],
participants: [
{ pubkey: 'abc123...', relay: 'wss://relay.example.com', role: 'speaker' },
],
})nip52.createTimeBasedCalendarEvent
function createTimeBasedCalendarEvent(event: TimeBasedCalendarEvent, secretKey: Uint8Array): NostrEventCreates and signs a kind 31923 time-based calendar event.
nip52.parseTimeBasedCalendarEvent
function parseTimeBasedCalendarEvent(event: NostrEvent): TimeBasedCalendarEventParses a kind 31923 time-based calendar event.
nip52.createCalendarTemplate
function createCalendarTemplate(calendar: Calendar): EventTemplateCreates an unsigned kind 31924 calendar (collection) event template.
const template = nip52.createCalendarTemplate({
identifier: 'work-calendar',
title: 'Work Calendar',
eventAddresses: [
'31922:pubkey:company-holiday-2026',
'31923:pubkey:standup-2026-03-17',
],
})nip52.createCalendarEvent
function createCalendarEvent(calendar: Calendar, secretKey: Uint8Array): NostrEventCreates and signs a kind 31924 calendar event.
nip52.parseCalendar
function parseCalendar(event: NostrEvent): CalendarParses a kind 31924 calendar event.
nip52.createCalendarEventRSVPTemplate
function createCalendarEventRSVPTemplate(rsvp: CalendarEventRSVP): EventTemplateCreates an unsigned kind 31925 RSVP event template.
const template = nip52.createCalendarEventRSVPTemplate({
identifier: 'rsvp-holiday',
calendarEventAddress: '31922:pubkey:company-holiday-2026',
status: 'accepted',
freebusy: 'busy',
calendarEventAuthor: organizerPk,
content: 'I will be there!',
})nip52.createCalendarEventRSVP
function createCalendarEventRSVP(rsvp: CalendarEventRSVP, secretKey: Uint8Array): NostrEventCreates and signs a kind 31925 RSVP event.
nip52.parseCalendarEventRSVP
function parseCalendarEventRSVP(event: NostrEvent): CalendarEventRSVPParses a kind 31925 RSVP event.
nip52.buildCalendarEventAddress
function buildCalendarEventAddress(
kind: 31922 | 31923 | 31924 | 31925,
pubkey: string,
identifier: string,
): stringBuilds an a tag address string for any addressable NIP-52 kind - the two event kinds, calendars (31924) and RSVPs (31925).
const address = nip52.buildCalendarEventAddress(31923, myPubkey, 'standup-2026-03-17')
// '31923:abc123...:standup-2026-03-17'
// An RSVP referencing its calendar
nip52.buildCalendarEventAddress(31924, hostPubkey, 'work-calendar')nip52.buildAddressableAddress
function buildAddressableAddress(kind: number, pubkey: string, identifier: string): stringBuilds a coordinate for any addressable (parameterized-replaceable) kind in the 30000-39999 range. Throws for kinds outside it.
nip52.buildAddressableAddress(30078, pubkey, 'my-app/prefs')
// '30078:abc123...:my-app/prefs'
nip52.buildAddressableAddress(1, pubkey, 'x') // throws: kind 1 is not addressablenip52.parseAddressableAddress
function parseAddressableAddress(address: string): {
kind: number
pubkey: string
identifier: string
}Splits a coordinate back into its parts. Identifiers containing colons round trip correctly.
nip52.calendarEventDays
function calendarEventDays(start: number, end?: number): number[]Computes the D day-granularity tags (floor(unix_seconds / 86400)) an event spans - one per day. createTimeBasedCalendarEventTemplate calls this for you when days is omitted.
nip52.isCalendarEvent
function isCalendarEvent(event: NostrEvent): booleanReturns true if the event kind is 31922, 31923, 31924, or 31925.
Full Example
import { generateSecretKey, getPublicKey, nip52, RelayPool } from 'nostr-core'
const sk = generateSecretKey()
const pk = getPublicKey(sk)
const pool = new RelayPool()
// Create a time-based meeting
const meeting = nip52.createTimeBasedCalendarEvent({
identifier: 'bitcoin-meetup',
title: 'Bitcoin Meetup',
start: Math.floor(Date.now() / 1000) + 86400,
end: Math.floor(Date.now() / 1000) + 86400 + 7200,
startTzid: 'America/New_York',
content: 'Monthly Bitcoin meetup at the coffee shop',
locations: ['Bitcoin Coffee, 456 Oak St'],
participants: [{ pubkey: friendPk, role: 'attendee' }],
hashtags: ['bitcoin', 'meetup'],
}, sk)
await pool.publish(['wss://relay.example.com'], meeting)
// Create a calendar and add the event
const calendar = nip52.createCalendarEvent({
identifier: 'my-events',
title: 'My Events',
eventAddresses: [nip52.buildCalendarEventAddress(31923, pk, 'bitcoin-meetup')],
}, sk)
await pool.publish(['wss://relay.example.com'], calendar)
// RSVP to the meeting
const rsvp = nip52.createCalendarEventRSVP({
identifier: 'rsvp-bitcoin-meetup',
calendarEventAddress: `31923:${pk}:bitcoin-meetup`,
status: 'accepted',
freebusy: 'busy',
calendarEventAuthor: pk,
}, friendSk)
await pool.publish(['wss://relay.example.com'], rsvp)
pool.close()How It Works
- Kind 31922 is a parameterized replaceable event for date-based calendar events (all-day events, holidays)
- Kind 31923 is a parameterized replaceable event for time-based calendar events with Unix timestamps and timezone support
- Kind 31924 is a calendar collection that references calendar events via
atags - Kind 31925 is a parameterized replaceable RSVP event with status (
accepted,declined,tentative) and free/busy indicator - Time-based events include
Dtags (day-granularity:floor(unix_seconds / 86400)) for efficient date-range queries - Multiple
locationtags are supported for events with multiple venues - Participants are tagged with
ptags including optional relay hints and roles - Any user may RSVP, even if they were not tagged on the calendar event
- Recurring events are not natively supported; create separate events for each occurrence