# Implementation Guide

This cookbook walks you through building an endpoint to receive, verify, and process webhook events from PartsSource. By the end, you'll have a working receiver ready for production.

{% hint style="info" %}
Webhook configuration (URL and subscribed events) is set up by the PartsSource implementation and integration team. This guide assumes your webhook has already been configured and you have your signing secret.
{% endhint %}

Your responsibilities as a webhook consumer:

1. **Accept** the incoming POST request
2. **Verify** the signature to confirm it came from PartsSource
3. **Acknowledge** receipt by returning a `2xx` response within 30 seconds
4. **Process** the event data

**Prerequisites:** your webhook signing secret (provided during setup) and a publicly accessible HTTPS endpoint.

***

## Step 1: Build Your Endpoint

Your endpoint must accept `POST` requests with `Content-Type: application/json`, return `200 OK` (or any `2xx`) within 30 seconds, and verify the signature before processing. Handle duplicate deliveries idempotently using the envelope fields.

### Node.js (Express)

```javascript
const express = require('express');
const app = express();

app.post('/webhooks/partssource', express.json(), (req, res) => {
  // 1. Verify signature (see Step 2)
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Process the event
  const event = req.body;
  console.log(`Received: ${event.event_type}`);

  switch (event.event_type) {
    case 'order.created':
      handleOrderCreated(event.payload);
      break;
    case 'order.shipment.shipped':
      handleShipment(event.payload);
      break;
    case 'order.line.backordered':
      handleBackorder(event.payload);
      break;
    default:
      console.log(`Unhandled event type: ${event.event_type}`);
  }

  // 3. Acknowledge receipt
  res.status(200).send('OK');
});

app.listen(3000);
```

### C# (ASP.NET)

```csharp
[ApiController]
[Route("webhooks/partssource")]
public class WebhookController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> ReceiveWebhook()
    {
        // Read raw body for signature verification
        using var reader = new StreamReader(Request.Body);
        var body = await reader.ReadToEndAsync();

        // 1. Verify signature (see Step 2)
        var signature = Request.Headers["X-PS-Signature"].ToString();
        var timestamp = Request.Headers["X-PS-Timestamp"].ToString();

        if (!WebhookVerifier.Verify(_secret, signature, timestamp, body))
            return Unauthorized();

        // 2. Parse and process event
        var webhookEvent = JsonSerializer.Deserialize<WebhookEventRequest>(body);

        switch (webhookEvent.EventType)
        {
            case "order.created":
                await HandleOrderCreated(webhookEvent.Payload);
                break;
            case "order.shipment.shipped":
                await HandleShipment(webhookEvent.Payload);
                break;
            case "order.line.backordered":
                await HandleBackorder(webhookEvent.Payload);
                break;
        }

        // 3. Acknowledge receipt
        return Ok();
    }
}
```

### Python (Flask)

```python
from flask import Flask, request
import hmac, hashlib, time

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"

@app.route("/webhooks/partssource", methods=["POST"])
def receive_webhook():
    # 1. Verify signature (see Step 2)
    if not verify_signature(request):
        return "Invalid signature", 401

    # 2. Process the event
    event = request.get_json()
    event_type = event["event_type"]

    if event_type == "order.created":
        handle_order_created(event["payload"])
    elif event_type == "order.shipment.shipped":
        handle_shipment(event["payload"])
    elif event_type == "order.line.backordered":
        handle_backorder(event["payload"])

    # 3. Acknowledge receipt
    return "OK", 200
```

***

## Step 2: Verify Signatures

Always verify the `X-PS-Signature` header to confirm the webhook came from PartsSource and wasn't modified in transit.

PartsSource constructs a signed payload as `{timestamp}.{request_body}`, computes HMAC-SHA256 using your webhook secret, and sends the result as `sha256={hex_digest}`. To verify:

1. Extract the `X-PS-Signature` and `X-PS-Timestamp` headers
2. Check that the timestamp is within 5 minutes of current time
3. Construct the signed payload: `{timestamp}.{raw_request_body}`
4. Compute HMAC-SHA256 with your webhook secret
5. Compare signatures using constant-time comparison

### Node.js

```javascript
const crypto = require('crypto');

function verifySignature(req) {
  const secret = process.env.WEBHOOK_SECRET;
  const signature = req.headers['x-ps-signature'];
  const timestamp = req.headers['x-ps-timestamp'];
  const body = JSON.stringify(req.body);

  // Check timestamp freshness (5 minute tolerance)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${body}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

### C\#

```csharp
using System.Security.Cryptography;
using System.Text;

public static class WebhookVerifier
{
    public static bool Verify(
        string secret,
        string signatureHeader,
        string timestampHeader,
        string body,
        int toleranceSeconds = 300)
    {
        // Check timestamp freshness
        if (!long.TryParse(timestampHeader, out var timestamp))
            return false;

        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (Math.Abs(now - timestamp) > toleranceSeconds)
            return false;

        // Compute expected signature
        var signedPayload = $"{timestamp}.{body}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
        var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();

        // Constant-time comparison
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signatureHeader));
    }
}
```

### Python

```python
import hmac, hashlib, time

def verify_signature(request):
    secret = WEBHOOK_SECRET.encode('utf-8')
    signature = request.headers.get('X-PS-Signature', '')
    timestamp = request.headers.get('X-PS-Timestamp', '')

    # Check timestamp freshness
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(time.time() - ts) > 300:
        return False

    # Compute expected signature
    signed_payload = f"{timestamp}.{request.get_data(as_text=True)}"
    expected = "sha256=" + hmac.new(
        secret, signed_payload.encode('utf-8'), hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(signature, expected)
```

### Secret Rotation

If your signing secret is rotated, there is a **24-hour grace period** where deliveries include both `X-PS-Signature` (new secret) and `X-PS-Signature-Previous` (old secret). Accept either signature during this window:

```javascript
function verifySignatureWithRotation(req) {
  if (verifyWithSecret(req, process.env.WEBHOOK_SECRET)) {
    return true;
  }
  // During rotation grace period, check previous signature
  const prevSignature = req.headers['x-ps-signature-previous'];
  if (prevSignature && process.env.WEBHOOK_SECRET_PREVIOUS) {
    return verifyWithSecret(req, process.env.WEBHOOK_SECRET_PREVIOUS);
  }
  return false;
}
```

***

## Step 3: Production Best Practices

### Process Asynchronously

Acknowledge the webhook immediately with `200 OK`, then process the event in a background queue. This keeps your response time fast and avoids timeouts.

```javascript
app.post('/webhooks/partssource', express.json(), (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // Enqueue for background processing
  eventQueue.push(req.body);

  // Acknowledge immediately
  res.status(200).send('OK');
});
```

### Deduplicate Events

Webhook delivery uses at-least-once semantics — the same event may be delivered more than once after a retry. Use the envelope fields to detect and skip duplicates:

```javascript
// Use a persistent store (database, Redis) in production
async function handleWebhook(event) {
  const dedupeKey = `${event.event_type}:${event.reference_id}:${event.occurred_at}`;
  const alreadyProcessed = await db.exists('processed_events', dedupeKey);
  if (alreadyProcessed) {
    return; // Already handled
  }

  await processEvent(event);
  await db.insert('processed_events', { key: dedupeKey, processed_at: new Date() });
}
```

### Store the Raw Payload

Log the full request body before processing. This helps with debugging and allows you to replay events locally if needed.

### Handle Unknown Event Types Gracefully

Your endpoint may receive event types you haven't implemented handlers for. Always acknowledge these with `200 OK` — returning an error triggers unnecessary retries.

```javascript
default:
  console.log(`Unhandled event type: ${event.event_type}`);
  // Still return 200 — don't trigger retries for events you don't handle
```

***

## Testing Checklist

Before going live, verify the following:

* [ ] Endpoint returns `200 OK` for valid requests
* [ ] Signature verification passes with your stored secret
* [ ] Signature verification rejects tampered payloads
* [ ] Stale timestamps (>5 min old) are rejected
* [ ] Duplicate events are handled idempotently
* [ ] Unknown event types don't cause errors (log and acknowledge them)
* [ ] Endpoint responds within 30 seconds

***

## Troubleshooting

### Signature verification failing

1. Use the **raw request body** (not a re-serialized version) when computing the signature
2. Confirm you're using the correct secret (the one provided during setup, or the rotated one)
3. Verify your HMAC is using SHA-256 and outputting lowercase hex
4. Construct the signed payload as `{timestamp}.{body}` with a literal dot separator
5. During secret rotation, check both `X-PS-Signature` and `X-PS-Signature-Previous`

### Receiving duplicate events

This is expected behavior. Webhook delivery uses at-least-once semantics. Deduplicate using the envelope fields. For production, use a persistent store (database, Redis) rather than an in-memory set.

### Not receiving webhooks

Contact your PartsSource integration team to verify:

1. Your webhook is active
2. Your subscribed events match what you expect
3. Your endpoint is publicly accessible over HTTPS

They can also check delivery history and replay failed deliveries on your behalf.
