Skip to main content
This guide covers how to build a reliable webhook handler, with code examples in multiple languages and recommendations for production use.

Using the TypeScript SDK

If you’re using the TypeScript SDK (v0.2.0+), it provides typed payloads and helper functions that handle event parsing and state checks for you.
import {
  parseWebhookEvent,
  isAuthentic,
  isCounterfeit,
  needsResubmission,
  isCancelled,
} from 'legitmark';

app.post('/webhooks/legitmark', express.json(), (req, res) => {
  const event = parseWebhookEvent(req.body);

  if (isAuthentic(event)) {
    markItemAsAuthentic(event.reference_id);
  } else if (isCounterfeit(event)) {
    flagItem(event.reference_id);
  } else if (needsResubmission(event)) {
    requestNewPhotos(event.reference_id, event.sides);
  } else if (isCancelled(event)) {
    closeCase(event.sr_uuid);
  }

  res.status(200).send('OK');
});
parseWebhookEvent() validates the payload shape and returns a fully typed LegitmarkWebhookEvent. The helper functions check specific state combinations so you don’t need to memorize them:
HelperReturns true when
isAuthentic(event)COMPLETE + APPROVED — item is genuine
isCounterfeit(event)COMPLETE + REJECTED — item is not authentic
isCancelled(event)CANCELLED — SR was cancelled
needsResubmission(event)media_rejected — images need re-upload
isQcApproved(event)QC + APPROVED — photos passed quality review
isAuthenticationInProgress(event)UNDERWAY + ASSIGNED — authenticator working

Basic Handler

If you’re not using the TypeScript SDK, here are examples in multiple languages.
app.post('/webhooks/legitmark', express.json(), (req, res) => {
  const event = req.body;

  switch (event.event_type) {
    case 'state_change':
      if (event.state.primary === 'COMPLETE') {
        updateAuthResult(event.reference_id, event.state.supplement);
      }
      break;

    case 'media_rejected':
      event.sides.forEach(side => {
        flagImageForReupload(event.reference_id, side.side, side.reason);
      });
      break;

    case 'invalidate_sr':
      cancelItem(event.reference_id, event.invalidation_reason.message);
      break;
  }

  res.status(200).send('OK');
});

Best Practices

Acknowledge the webhook as fast as possible. If your processing logic is complex or calls external services, queue the work and return 200 first.
app.post('/webhooks/legitmark', express.json(), (req, res) => {
  // Acknowledge immediately
  res.status(200).send('OK');

  // Process asynchronously
  processWebhookAsync(req.body).catch(err => {
    console.error('Webhook processing failed:', err);
  });
});
You may receive the same event more than once. Build a deduplication key from the payload fields that uniquely identify the event:
  • state_change: sr_uuid + state.primary + state.supplement (the same SR emits multiple state changes)
  • media_rejected: sr_uuid + event_type
  • invalidate_sr: sr_uuid + event_type
const processed = new Set();

app.post('/webhooks/legitmark', express.json(), (req, res) => {
  const { sr_uuid, event_type, state } = req.body;
  const dedupKey = event_type === 'state_change'
    ? `${sr_uuid}:${event_type}:${state.primary}:${state.supplement}`
    : `${sr_uuid}:${event_type}`;

  if (processed.has(dedupKey)) {
    return res.status(200).send('Already processed');
  }

  processed.add(dedupKey);
  // Handle the event...

  res.status(200).send('OK');
});
In production, use a persistent store (database, Redis) instead of an in-memory Set so deduplication survives restarts.
Store the full webhook payload for debugging. When something goes wrong, having the original data makes diagnosis much faster.
app.post('/webhooks/legitmark', express.json(), (req, res) => {
  // Log first, process second
  console.log('Webhook received:', JSON.stringify(req.body));

  // Then handle the event...
  res.status(200).send('OK');
});
Don’t reject webhooks with unrecognized event_type values. New event types may be added in the future. Return 200 for any event, even ones you don’t handle yet.
app.post('/webhooks/legitmark', express.json(), (req, res) => {
  switch (req.body.event_type) {
    case 'state_change':
      // Handle known events...
      break;
    default:
      console.log('Unknown event type:', req.body.event_type);
      break;
  }

  // Always acknowledge
  res.status(200).send('OK');
});

Summary

Your webhook handler should:
  • Use an HTTPS endpoint and return 200 within 30 seconds
  • Be idempotent — use payload fields to deduplicate (not just sr_uuid, since one SR emits multiple events)
  • Accept unknown event types without failing (new events may be added)
  • Process complex logic asynchronously to avoid timeouts