> ## Documentation Index
> Fetch the complete documentation index at: https://docs.legitmark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling Webhooks

> Code examples, best practices, and implementation guidance for processing Legitmark webhook events.

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](https://github.com/legitmark-eng/legitmark-typescript) (`v0.2.0+`), it provides typed payloads and helper functions that handle event parsing and state checks for you.

```typescript theme={null}
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:

| Helper                              | Returns `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.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  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');
  });
  ```

  ```python Python (Flask) theme={null}
  @app.route('/webhooks/legitmark', methods=['POST'])
  def handle_webhook():
      event = request.get_json()

      if event['event_type'] == 'state_change':
          if event['state']['primary'] == 'COMPLETE':
              update_auth_result(event['reference_id'], event['state']['supplement'])

      elif event['event_type'] == 'media_rejected':
          for side in event['sides']:
              flag_for_reupload(event['reference_id'], side['side'], side['reason'])

      elif event['event_type'] == 'invalidate_sr':
          cancel_item(event['reference_id'], event['invalidation_reason']['message'])

      return 'OK', 200
  ```

  ```ruby Ruby (Sinatra) theme={null}
  post '/webhooks/legitmark' do
    event = JSON.parse(request.body.read)

    case event['event_type']
    when 'state_change'
      if event.dig('state', 'primary') == 'COMPLETE'
        update_auth_result(event['reference_id'], event.dig('state', 'supplement'))
      end
    when 'media_rejected'
      event['sides'].each do |side|
        flag_for_reupload(event['reference_id'], side['side'], side['reason'])
      end
    when 'invalidate_sr'
      cancel_item(event['reference_id'], event.dig('invalidation_reason', 'message'))
    end

    status 200
    'OK'
  end
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Return 200 immediately">
    Acknowledge the webhook as fast as possible. If your processing logic is complex or calls external services, queue the work and return `200` first.

    ```javascript theme={null}
    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);
      });
    });
    ```
  </Accordion>

  <Accordion title="Handle duplicate deliveries">
    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`

    ```javascript theme={null}
    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');
    });
    ```

    <Note>
      In production, use a persistent store (database, Redis) instead of an in-memory `Set` so deduplication survives restarts.
    </Note>
  </Accordion>

  <Accordion title="Log the raw payload">
    Store the full webhook payload for debugging. When something goes wrong, having the original data makes diagnosis much faster.

    ```javascript theme={null}
    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');
    });
    ```
  </Accordion>

  <Accordion title="Handle unknown event types gracefully">
    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.

    ```javascript theme={null}
    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');
    });
    ```
  </Accordion>
</AccordionGroup>

## 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
