Build and test your webhook endpoint for receiving PartsSource events
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.
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.
Your responsibilities as a webhook consumer:
Accept the incoming POST request
Verify the signature to confirm it came from PartsSource
Acknowledge receipt by returning a 2xx response within 30 seconds
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)
constexpress=require('express');constapp=express();app.post('/webhooks/partssource',express.json(),(req,res)=>{ // 1. Verify signature (see Step 2)if (!verifySignature(req)) {returnres.status(401).send('Invalid signature');} // 2. Process the eventconstevent=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 receiptres.status(200).send('OK');});app.listen(3000);
C# (ASP.NET)
Python (Flask)
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:
Extract the X-PS-Signature and X-PS-Timestamp headers
Check that the timestamp is within 5 minutes of current time
Construct the signed payload: {timestamp}.{raw_request_body}
Compute HMAC-SHA256 with your webhook secret
Compare signatures using constant-time comparison
Node.js
C#
Python
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:
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.
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:
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.
Testing Checklist
Before going live, verify the following:
Troubleshooting
Signature verification failing
Use the raw request body (not a re-serialized version) when computing the signature
Confirm you're using the correct secret (the one provided during setup, or the rotated one)
Verify your HMAC is using SHA-256 and outputting lowercase hex
Construct the signed payload as {timestamp}.{body} with a literal dot separator
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:
Your webhook is active
Your subscribed events match what you expect
Your endpoint is publicly accessible over HTTPS
They can also check delivery history and replay failed deliveries on your behalf.
[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();
}
}
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
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));
}
}