Skip to content

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

ts
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

ts
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:

ts
{ 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

ts
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.

ts
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.

ts
const parsed = nip52.parseTimeBasedCalendarEvent(event)
parsed.days       // [19675, 19676, ...] from the D tags
parsed.extraTags  // [['x-app-custom', 'v1'], ...]

nip52.createTimeBasedCalendarEventTemplate(parsed)   // emits both again

Deprecated 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

ts
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

ts
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

ts
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

ts
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

ts
function createDateBasedCalendarEventTemplate(event: DateBasedCalendarEvent): EventTemplate

Creates an unsigned kind 31922 date-based calendar event template for all-day or multi-day events.

ts
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

ts
function createDateBasedCalendarEvent(event: DateBasedCalendarEvent, secretKey: Uint8Array): NostrEvent

Creates and signs a kind 31922 date-based calendar event.

nip52.parseDateBasedCalendarEvent

ts
function parseDateBasedCalendarEvent(event: NostrEvent): DateBasedCalendarEvent

Parses a kind 31922 date-based calendar event.

nip52.createTimeBasedCalendarEventTemplate

ts
function createTimeBasedCalendarEventTemplate(event: TimeBasedCalendarEvent): EventTemplate

Creates an unsigned kind 31923 time-based calendar event template. Automatically calculates the D (day-granularity) tags.

ts
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

ts
function createTimeBasedCalendarEvent(event: TimeBasedCalendarEvent, secretKey: Uint8Array): NostrEvent

Creates and signs a kind 31923 time-based calendar event.

nip52.parseTimeBasedCalendarEvent

ts
function parseTimeBasedCalendarEvent(event: NostrEvent): TimeBasedCalendarEvent

Parses a kind 31923 time-based calendar event.

nip52.createCalendarTemplate

ts
function createCalendarTemplate(calendar: Calendar): EventTemplate

Creates an unsigned kind 31924 calendar (collection) event template.

ts
const template = nip52.createCalendarTemplate({
  identifier: 'work-calendar',
  title: 'Work Calendar',
  eventAddresses: [
    '31922:pubkey:company-holiday-2026',
    '31923:pubkey:standup-2026-03-17',
  ],
})

nip52.createCalendarEvent

ts
function createCalendarEvent(calendar: Calendar, secretKey: Uint8Array): NostrEvent

Creates and signs a kind 31924 calendar event.

nip52.parseCalendar

ts
function parseCalendar(event: NostrEvent): Calendar

Parses a kind 31924 calendar event.

nip52.createCalendarEventRSVPTemplate

ts
function createCalendarEventRSVPTemplate(rsvp: CalendarEventRSVP): EventTemplate

Creates an unsigned kind 31925 RSVP event template.

ts
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

ts
function createCalendarEventRSVP(rsvp: CalendarEventRSVP, secretKey: Uint8Array): NostrEvent

Creates and signs a kind 31925 RSVP event.

nip52.parseCalendarEventRSVP

ts
function parseCalendarEventRSVP(event: NostrEvent): CalendarEventRSVP

Parses a kind 31925 RSVP event.

nip52.buildCalendarEventAddress

ts
function buildCalendarEventAddress(
  kind: 31922 | 31923 | 31924 | 31925,
  pubkey: string,
  identifier: string,
): string

Builds an a tag address string for any addressable NIP-52 kind - the two event kinds, calendars (31924) and RSVPs (31925).

ts
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

ts
function buildAddressableAddress(kind: number, pubkey: string, identifier: string): string

Builds a coordinate for any addressable (parameterized-replaceable) kind in the 30000-39999 range. Throws for kinds outside it.

ts
nip52.buildAddressableAddress(30078, pubkey, 'my-app/prefs')
// '30078:abc123...:my-app/prefs'

nip52.buildAddressableAddress(1, pubkey, 'x')   // throws: kind 1 is not addressable

nip52.parseAddressableAddress

ts
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

ts
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

ts
function isCalendarEvent(event: NostrEvent): boolean

Returns true if the event kind is 31922, 31923, 31924, or 31925.

Full Example

ts
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 a tags
  • Kind 31925 is a parameterized replaceable RSVP event with status (accepted, declined, tentative) and free/busy indicator
  • Time-based events include D tags (day-granularity: floor(unix_seconds / 86400)) for efficient date-range queries
  • Multiple location tags are supported for events with multiple venues
  • Participants are tagged with p tags 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

Released under the MIT License.