CodeMotion.Labs
Backend & API Security Technical Specification • 12 Min Read

Handling Instagram Comment Webhooks with ASP.NET Core & Meta Graph API

Building an automated direct messaging system requires processing inbound Meta webhooks in real time. This engineering guide details the initial GET subscription verification handshake, cryptographic HMAC-SHA256 payload authentication, and non-blocking background queue ingestion using ASP.NET Core.

1. Architectural Ingestion Lifecycle

When an Instagram follower comments on a reel with a designated trigger phrase (such as "CODE" or "ROADMAP"), Meta servers dispatch an asynchronous HTTPS POST webhook within milliseconds. The receiving ASP.NET Core backend must fulfill three operational requirements:

1. Subscription Handshake

Instantly echo the hub.challenge parameter upon receiving a valid verify token during initial endpoint registration.

2. Cryptographic Audit

Verify the X-Hub-Signature-256 header using the Application Secret to reject unauthorized third-party spoofing.

3. Sub-200ms ACK

Return an immediate HTTP 200 OK to Meta servers before dispatching the downstream messaging task to prevent automatic retries.

2. The GET Subscription Handshake (hub.challenge)

During Meta App Dashboard configuration, Meta issues an HTTP GET request to verify domain ownership. The controller must validate the incoming hub.verify_token against server configuration and return the verbatim hub.challenge token as plain text:

[HttpGet]
[Produces("text/plain")]
public IActionResult VerifyWebhook(
    [FromQuery(Name = "hub.mode")] string? mode,
    [FromQuery(Name = "hub.verify_token")] string? verifyToken,
    [FromQuery(Name = "hub.challenge")] string? challenge)
{
    var configuredToken = _configuration["MetaSettings:VerifyToken"];

    if (mode == "subscribe" && verifyToken == configuredToken)
    {
        _logger.LogInformation("Meta Webhook verification handshake successful.");
        return Content(challenge ?? string.Empty, "text/plain");
    }

    _logger.LogWarning("Unauthorized webhook verification attempt rejected.");
    return Forbid();
}

3. HMAC-SHA256 Cryptographic Payload Verification

Every legitimate inbound webhook from Meta includes an X-Hub-Signature-256 HTTP header formatted as sha256={hash}. This hash is generated using HMAC-SHA256 computed over the raw payload bytes using your Meta Application Secret.

To prevent timing attacks, always compare byte arrays using constant-time comparison methods:

using System.Security.Cryptography;
using System.Text;

public static class MetaSignatureValidator
{
    public static bool VerifySignature(byte[] payloadBytes, string? signatureHeader, string appSecret)
    {
        if (string.IsNullOrWhiteSpace(signatureHeader) || !signatureHeader.StartsWith("sha256="))
            return false;

        var expectedHashHex = signatureHeader["sha256=".Length..];
        var secretBytes = Encoding.UTF8.GetBytes(appSecret);

        using var hmac = new HMACSHA256(secretBytes);
        var computedHash = hmac.ComputeHash(payloadBytes);
        var computedHex = Convert.ToHexString(computedHash).ToLowerInvariant();

        // Constant-time execution prevents timing side-channel exploits
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(computedHex),
            Encoding.UTF8.GetBytes(expectedHashHex.ToLowerInvariant())
        );
    }
}

4. Controller Implementation with Non-Blocking Ingestion

Meta sets a strict 5-second timeout window. Attempting to parse comment text, perform database lookups, and invoke the Graph API directly inside the HTTP request loop risks connection termination and duplicate webhook storms.

Instead, read the raw body, validate the signature, push the raw payload into an in-memory Channel<WebhookEvent>, and return HTTP 200 OK immediately:

[HttpPost]
public async Task<IActionResult> ReceiveWebhook(
    [FromServices] IWebhookQueue queue,
    CancellationToken ct)
{
    // Read raw body stream for cryptographic verification
    using var reader = new StreamReader(Request.Body, Encoding.UTF8);
    var rawBody = await reader.ReadToEndAsync(ct);
    var rawBytes = Encoding.UTF8.GetBytes(rawBody);

    var signature = Request.Headers["X-Hub-Signature-256"].FirstOrDefault();
    var appSecret = _configuration["MetaSettings:AppSecret"]!;

    if (!MetaSignatureValidator.VerifySignature(rawBytes, signature, appSecret))
    {
        _logger.LogWarning("Webhook HMAC signature validation failed.");
        return Unauthorized();
    }

    // Enqueue event to background processing queue
    await queue.EnqueueAsync(rawBody, ct);

    // Return instant 200 OK to satisfy Meta delivery standards
    return Ok();
}

5. Automated Comment DM Dispatch Worker

A registered BackgroundService reads queued events asynchronously. It parses the Instagram comment node, extracts the sender's user ID, checks for keyword matches, and dispatches an automated DM via the Meta Graph API (https://graph.facebook.com/v21.0/me/messages):

public class WebhookProcessingService : BackgroundService
{
    private readonly IWebhookQueue _queue;
    private readonly IHttpClientFactory _clientFactory;

    public WebhookProcessingService(IWebhookQueue queue, IHttpClientFactory clientFactory)
    {
        _queue = queue;
        _clientFactory = clientFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var rawPayload in _queue.ReadAllAsync(stoppingToken))
        {
            try
            {
                // Parse Instagram Comment JSON Payload
                var json = JsonDocument.Parse(rawPayload);
                // Extract comment text & sender ID...
                // Execute downstream DM API POST request...
            }
            catch (Exception ex)
            {
                // Log and gracefully handle telemetry errors
            }
        }
    }
}