public class SparkWebhookEndpoint
public static async Task<Results<Ok, UnauthorizedHttpResult>> Handle(
ILogger<SparkWebhookEndpoint> logger)
var req = context.Request;
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var requestBody = ms.ToArray();
if (!VerifySignature(req, requestBody, "my-secret"))
return TypedResults.Unauthorized();
var sparkEventNotification = JsonSerializer.Deserialize<SparkEventNotification>(requestBody,
new JsonSerializerOptions
(JsonSerializerDefaults.Web) // web defaults to handle camelCase properties
new JsonStringEnumConverter(), // handle enums as strings
new SparkEventPayloadConverter() // handle polymorphic payload
// Do not perform long processing before returning a 200 OK response
// Instead, save the event for later processing or enqueue it
await SaveEventForLaterProcessingAsync(sparkEventNotification);
// Important to return a 200 OK response to acknowledge receipt of event
return TypedResults.Ok();
private static Task SaveEventForLaterProcessingAsync(SparkEventNotification sparkEventNotification)
// Save the event to a queue or database for later processing
// For example, you can save the event to a database for later processing
// using Entity Framework Core or Dapper
// or enqueue the event to a message broker like Azure Service Bus or RabbitMQ
return Task.CompletedTask;
private static bool VerifySignature(HttpRequest req, byte[] requestBody, string secretKey)
// Retrieve the payload signature from the request headers
var providedSignature = req.Headers["x-payload-signature"].FirstOrDefault();
// Compute the expected payload signature
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
var expectedSignatureBytes = hmac.ComputeHash(requestBody);
var expectedSignature = Convert.ToBase64String(expectedSignatureBytes);
// Compare the provided signature with the expected signature
return string.Equals(providedSignature, expectedSignature, StringComparison.OrdinalIgnoreCase);
internal record SparkEventNotification
public string NotificationId { get; set; }
public string EventId { get; set; }
public DateTimeOffset EventCreatedAtUtc { get; set; }
public DateTimeOffset NotificationSentUtc { get; set; }
public NotificationType NotificationType { get; set; }
public SparkEventPayload Payload { get; set; }
internal record SparkEventPayload
public SparkEventPayloadType? PayloadType { get; set; }
internal record UserEligibilityPayload : SparkEventPayload
public DateTime LastUpdated { get; set; }
internal record PriceCurvePayload : SparkEventPayload
public PriceArea? PriceArea { get; set; }
public List<PriceCurveTarget> Targets { get; set; }
public PriceCurve PriceCurveDelta { get; set; }
internal record PriceCurveTarget
public string ResourceId { get; set; }
public string LocationId { get; set; }
internal record PriceCurve
public EnergyUnit? EnergyUnit { get; set; }
public Currency? Currency { get; set; }
public List<PriceCurvePoint> Points { get; set; }
public TimeSpan Resolution { get; set; }
internal record PriceCurvePoint
public decimal Price { get; set; }
public DateTimeOffset Timestamp { get; set; }
internal enum NotificationType
internal enum SparkEventPayloadType
internal class SparkEventPayloadConverter : JsonConverter<SparkEventPayload>
public override SparkEventPayload? Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
// Deserialize the JSON as a JObject.
var jo = JsonSerializer.Deserialize<JsonElement>(ref reader);
// Determine the specific derived type to use based on the "Type" property.
var typeProp = jo.GetProperty("payloadType");
SparkEventPayloadType? typeValue = Enum.Parse<SparkEventPayloadType>(typeProp.GetString() ?? string.Empty);
SparkEventPayloadType.UserEligibility =>
JsonSerializer.Deserialize<UserEligibilityPayload>(jo.GetRawText(), options),
SparkEventPayloadType.PriceCurve =>
JsonSerializer.Deserialize<PriceCurvePayload>(jo.GetRawText(), options),
_ => throw new JsonException($"Unknown payload type {typeProp}")
public override void Write(Utf8JsonWriter writer, SparkEventPayload value, JsonSerializerOptions options)
JsonSerializer.Serialize(writer, value, value.GetType(), options);