NIP-01 event canonicalization

Every solution that signs a Nostr event computes the same canonical form, the same event id, and the same BIP-340 input. This is what makes “behaves the same when present” actually mean something across five very different signers.

The serialization

Per NIP-01, the canonical pre-image is a UTF-8 JSON array with no whitespace, in this exact order:

[0, pubkey, created_at, kind, tags, content]
  • 0 is the literal integer zero — the format version.
  • pubkey is the x-only public key, lowercase hex (32 bytes = 64 chars).
  • created_at is a Unix timestamp (seconds), integer.
  • kind is an integer event kind.
  • tags is an array of arrays of strings.
  • content is a JSON string (escaped per JSON rules).

JSON serialisation rules:

  • No additional whitespace anywhere.
  • Object keys MUST NOT appear (the structure is a positional array, not an object).
  • Tags MUST keep the order the user provided.
  • content MUST be valid UTF-8 and is JSON-string-escaped (", \, control chars ).

The event id

The event id is sha256(serialised_bytes), lowercase hex.

event_id = SHA-256(JSON.stringify(
  [0, pubkey, created_at, kind, tags, content],
  no-whitespace-form
))

The signature

The signature is BIP-340 Schnorr over secp256k1:

  • Message = the 32-byte event id (not the serialised bytes).
  • Key = the signer’s secret key whose x-only public key matches pubkey.

Worked example

Given the request:

{
  "kind": 1,
  "created_at": 1714838400,
  "content": "hello",
  "tags": [["t","intro"]]
}

And signer pubkey a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90:

The pre-image is:

[0,"a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",1714838400,1,[["t","intro"]],"hello"]

(no spaces). The event id is the SHA-256 of those bytes. The signature is BIP-340 over the event id with the matching secret key.

Why every solution agrees

The same canonical form is exercised by shared conformance vectors in nSealr/specs under vectors/. A signer that diverges — say it re-orders tags, or pads content, or sorts JSON keys — fails the shared tests; it cannot ship under the same contract_id.

What this contract does not cover

See also

  • Signing request v0 — the envelope that wraps the event template before signing.
  • QR envelope — how the canonical request travels over QR.
  • nSealr/specs under vectors/ — the deterministic test corpus.

Last updated 2026-05-17