Gen-Endpoint
Back to Blog

Testing Webhooks and HTTP Request Payloads with an Echo API: The Complete Developer Guide

By Kunal Burangi•Published on November 1, 2026•
Developer Tools & Testing
#Webhooks
#Echo API
#Debugging
#HTTP
#Security
#REST API

Testing Webhooks and HTTP Request Payloads with an Echo API: The Complete Developer Guide

When building webhook dispatchers, payment gateway handlers (like Stripe, Shopify, or GitHub webhooks), or custom third-party API clients, observing outbound HTTP traffic is often frustratingly opaque.

Developers frequently find themselves wrestling with difficult questions:

  • Did my HTTP client send the Bearer authentication token in the expected header?
  • Did the body serialize as application/json, application/x-www-form-urlencoded, or raw buffer?
  • Did URL encoding scramble complex nested query parameters?
  • Is our cryptographic HMAC-SHA256 signature calculated over the raw string buffer or parsed JSON?
  • How does our webhook emitter handle network retries, timeouts, and replay attacks?

An Echo API solves this fundamental visibility problem by reflecting everything it receives directly back in the HTTP response: the HTTP verb, incoming request headers, query string parameters, raw body buffer, and client connection IP.

In this comprehensive guide, we explore webhook debugging strategies, explain how to compute and verify HMAC signatures, mitigate replay attacks with timestamp windows, and demonstrate how to use Gen-Endpoint's free Echo API in automated integration test suites.


1. How an Echo API Works

When you send an HTTP request to an Echo endpoint, the server does not store the data in a database or execute business logic. Instead, it inspects the incoming Request stream, extracts every header and body field, and returns a structured JSON receipt:

curl -X POST https://gen-endpoint.com/api/echo \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72" \
  -H "X-Client-Version: 2.4.0" \
  -d '{
    "event": "invoice.paid",
    "data": {
      "id": "in_98231",
      "amount": 4900,
      "currency": "usd",
      "customer": "cus_Alice"
    }
  }'

Sample Echo Response Payload:

{
  "type": "POST",
  "receivedBody": {
    "event": "invoice.paid",
    "data": {
      "id": "in_98231",
      "amount": 4900,
      "currency": "usd",
      "customer": "cus_Alice"
    }
  },
  "message": "JSON body echoed successfully.",
  "headers": {
    "content-type": "application/json",
    "x-webhook-signature": "sha256=9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72",
    "x-client-version": "2.4.0",
    "user-agent": "curl/8.7.1",
    "host": "gen-endpoint.com"
  },
  "timestamp": "2026-11-01T14:30:00.000Z"
}

With this output, you can verify that your custom headers arrived intact, the content-type was correct, and your body structure was properly formed.


2. Validating Cryptographic Signatures (HMAC-SHA256)

In production webhook architectures, the receiver must verify that the incoming payload genuinely originated from your server and was not forged or altered in transit by an attacker (a Man-in-the-Middle attack).

This is achieved by signing the payload with an HMAC-SHA256 signature using a shared secret key:

// utils/webhookSigner.ts
import crypto from 'crypto';

export function signWebhookPayload(payloadString: string, secretKey: string): string {
  const hmac = crypto.createHmac('sha256', secretKey);
  hmac.update(payloadString, 'utf8');
  return 'sha256=' + hmac.digest('hex');
}

export function verifyWebhookSignature(
  payloadString: string,
  incomingSignature: string,
  secretKey: string
): boolean {
  const expectedSignature = signWebhookPayload(payloadString, secretKey);
  
  // Use crypto.timingSafeEqual to prevent side-channel timing attacks
  const incomingBuffer = Buffer.from(incomingSignature);
  const expectedBuffer = Buffer.from(expectedSignature);

  if (incomingBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(incomingBuffer, expectedBuffer);
}

The #1 Webhook Signature Trap: JSON Stringification

A subtle bug that causes valid webhook signatures to be rejected is payload re-serialization. If you sign the raw request string buffer, but the receiving server parses the body as JSON and re-serializes it via JSON.stringify(body), whitespace or key ordering differences will result in a completely different hash!

Always calculate and verify signatures over the raw byte buffer, before JSON parsing occurs.


3. Mitigating Replay Attacks with Timestamp Tolerances

Even if an attacker cannot forge an HMAC signature, they could intercept a valid payment.success webhook and resend it 100 times to credit the customer's account repeatedly. This is known as a Replay Attack.

To prevent replay attacks:

  1. Include an exact timestamp in the signature header: X-Signature: t=1730462400,v1=9b71d2...
  2. Include the timestamp in the signed string: ${timestamp}.${rawBody}
  3. The receiver rejects any webhook whose timestamp is older than 300 seconds (5 minutes):
// middleware/verifyWebhookWithTolerance.ts
import crypto from 'crypto';

export function verifyWebhookWithTimestamp(
  rawBody: string,
  signatureHeader: string,
  secretKey: string,
  toleranceSeconds: number = 300
): { isValid: boolean; reason?: string } {
  // 1. Parse header: "t=1730462400,v1=abcd..."
  const elements = signatureHeader.split(',');
  const timestampPart = elements.find((el) => el.startsWith('t='));
  const signaturePart = elements.find((el) => el.startsWith('v1='));

  if (!timestampPart || !signaturePart) {
    return { isValid: false, reason: 'Malformed signature header' };
  }

  const timestamp = parseInt(timestampPart.replace('t=', ''), 10);
  const incomingSignature = signaturePart.replace('v1=', '');

  // 2. Enforce timestamp tolerance
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    return { isValid: false, reason: 'Webhook timestamp outside allowed tolerance window' };
  }

  // 3. Compute expected hash over "timestamp.rawBody"
  const signedPayload = `${timestamp}.${rawBody}`;
  const hmac = crypto.createHmac('sha256', secretKey);
  hmac.update(signedPayload, 'utf8');
  const expectedSignature = hmac.digest('hex');

  const incomingBuffer = Buffer.from(incomingSignature);
  const expectedBuffer = Buffer.from(expectedSignature);

  if (incomingBuffer.length !== expectedBuffer.length) {
    return { isValid: false, reason: 'Invalid signature length' };
  }

  const matches = crypto.timingSafeEqual(incomingBuffer, expectedBuffer);
  return { isValid: matches, reason: matches ? undefined : 'Signature mismatch' };
}

4. Implementing Idempotency with Idempotency-Key

When sending webhooks across the public internet, transient network failures happen. If your webhook sender times out after 5000ms, it must retry. But did the receiver fail to process the payment, or did it process the payment and the acknowledgment packet dropped?

To guarantee safe retries, send an Idempotency Key:

POST /api/webhooks HTTP/1.1
Host: api.yourcompany.com
Content-Type: application/json
Idempotency-Key: evt_ord_98721_attempt_1
X-Webhook-Event: order.fulfilled

{
  "orderId": "ord_98721",
  "status": "shipped"
}

The receiving server checks whether it has already processed Idempotency-Key: evt_ord_98721_attempt_1 within the last 24 hours. If so, it returns the cached response immediately without executing duplicate business logic.


5. Automated Webhook Emitter Tests with Vitest / Jest

You can write automated unit tests for your webhook dispatcher against Gen-Endpoint's live Echo API:

// __tests__/webhookDispatcher.test.ts
import { describe, it, expect } from 'vitest';

async function dispatchWebhook(url: string, event: string, data: object) {
  const payload = JSON.stringify({ event, data, timestamp: Date.now() });
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-App-Event': event,
      'X-Signature-Version': '1.0',
    },
    body: payload,
  });

  return await response.json();
}

describe('Webhook Dispatcher Service', () => {
  it('correctly dispatches serialized event to target endpoint', async () => {
    const echoUrl = 'https://gen-endpoint.com/api/echo';
    const result = await dispatchWebhook(echoUrl, 'user.created', { id: 'usr_42' });

    expect(result.type).toBe('POST');
    expect(result.receivedBody.event).toBe('user.created');
    expect(result.receivedBody.data.id).toBe('usr_42');
    expect(result.headers['x-app-event']).toBe('user.created');
    expect(result.headers['content-type']).toContain('application/json');
  });
});

Summary & Next Steps

An Echo API eliminates guesswork when developing API wrappers, SDKs, and webhook dispatchers.

Free Developer Tooling

Ready to Test These Endpoints?

Access 23+ free mock REST APIs, test file uploads, and generate Next.js API handlers with zero signup.