nomiDocumentation
SupportBack to site
  • Getting started

    • Introduction
    • What is Nomi?
    • Quick start
    • Concepts
    • Authentication
  • Credentials

    • Create a credential
    • Issue a credential
    • Credential lifecycle
    • Revoke a credential
    • Verify a credential
    • QR verification
  • Distribution

    • Apple Wallet
    • Google Wallet
    • Email
    • Credential delivery
    • Bulk issuance
  • API

    • API overview
    • Authentication
    • Credentials API
    • Recipients
    • Verification
    • Revocation
    • Webhooks
    • Errors
  • Integrations

    • Moodle
    • WordPress
    • REST API
    • Webhooks
  • Security

    • Authentication
    • API keys
    • Webhook security
    • Data protection
    • Best practices
  • Resources

    • FAQ
    • Glossary
    • Changelog
  1. Documentation
  2. /
  3. API
  4. /
  5. Webhooks

Webhooks

Registering an endpoint, verifying the signature, and every event type the platform emits.

Webhooks are the event log, delivered to a URL you own, signed so your listener can prove it came from Nomi, and retried when your service is having a bad afternoon.

POST/v1/webhook_endpoints

Register a URL. The signing secret is returned once, here.

Registering

curl -X POST https://api.nomi-tech.com/v1/webhook_endpoints \
  -H "Authorization: Bearer nomi_sk_live_EXAMPLE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example/hooks/nomi",
    "description": "Production listener",
    "eventTypes": ["credential.*"]
  }'

An empty eventTypes means every event. credential.* matches a whole family, so a new event type does not require re-listing them all.

Verifying the signature

Each delivery carries a nomi-signature header of the form t=<unix seconds>,v1=<hex>. v1 is an HMAC-SHA256 over ${t}.${rawBody} using the endpoint's secret. The timestamp is inside the HMAC, so a valid signature cannot be replayed forever.

A verifier

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string) {
  const parts = new Map(
    header.split(",").map((p) => {
      const [k, ...rest] = p.trim().split("=");
      return [k, rest.join("=")] as const;
    }),
  );

  const t = parts.get("t");
  const v1 = parts.get("v1");
  if (!t || !v1) return false;

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");

  const given = Buffer.from(v1, "hex");
  const mine = Buffer.from(expected, "hex");
  if (given.length !== mine.length || !timingSafeEqual(given, mine)) return false;

  // Freshness. The signature proves origin; the timestamp proves it is not a replay.
  return Math.abs(Date.now() / 1000 - Number(t)) <= 300;
}
Verify the raw body, before parsing it. Re-serialising JSON reorders keys and changes whitespace, and a receiver that parses first will fail on a signature that is perfectly correct.

Event types

GET /v1/webhook_event_types returns the list your account can subscribe to, which is the one to build against. The families below are the ones an integration normally wants:

FamilyTypes
credential.*created, issued, updated, suspended, resumed, revoked, expired, verified, verify_refused, shared, viewed
credential.channel.*provisioned, installed, removed, failed
subject.*created, updated, deactivated, deleted
template.version.*published, archived, unarchived
achievement.*created, updated, version.published, version.archived
policy.*applied, halted, failed
group.*created, updated, deleted, members_added, members_removed
webhook.endpoint.*created, updated, deleted, disabled

Delivery and failure

A delivery your listener does not accept is retried, so an afternoon of downtime does not become a day of lost events. What your side has to get right is on this page and in Webhook security: verify the signature, answer quickly, and make the handler idempotent, because a retry means your service will see the same event twice.

The operational half — reviewing what was attempted, sending one again after a fix, sending a test delivery, and rotating the signing secret — is done from the console by whoever administers the organisation. None of it needs a deploy on your side.

An endpoint that keeps failing is eventually disabled, and that emits webhook.endpoint.disabled — so silence is a thing you can be told about rather than a thing you discover.

Webhook security

The listener's side.

Errors

When a call goes wrong.

PreviousRevocationNextErrors

Still stuck?

If this page did not answer it, the Help Center has the operational side of the same question — and a person reads what you send.

Go to the Help Center →

On this page

  • Verifying the signature
  • Event types
  • Delivery and failure
nomi

Digital credential infrastructure. Create, issue and manage credentials from the systems you already use.

Operated by

Country and currency

Platform

  • How it works
  • Templates
  • Lifecycle
  • Developers
  • Pricing
  • FAQ
  • Nomi Academic

Credentials

  • Memberships
  • Employee IDs
  • Student IDs
  • Events & loyalty

Developers

  • Documentation
  • Quick start
  • API reference
  • Webhooks
  • Integrations

Support

  • Help Center
  • Apple & Google Wallet
  • Verification
  • Contact support

Company

  • Request a demo
  • Talk to Nomi
  • Legal
  • Codingraph
PrivacyTermsCookiesSecurityContact

© 2026 Codingraph S.A. All rights reserved.

Nomi is a registered trademark used by Codingraph S.A. under licence.

Billed in USD

Apple Wallet and Google Wallet are trademarks of their respective owners.