Skip to content

Implementing your webhook-endpoints

Best Practices

When implementing your webhook endpoint, please follow these best practices:

  • Do not perform lengthy processing before returning a 2XX response: Instead, save the event for later processing or enqueue it on a message bus, and return 2XX immediately. It is essential to avoid congesting Spark webhook endpoint calls. This also ensures you avoid timeouts by quickly acknowledging receipt of the event and ensuring we do not retry and send the event more than once. This is also how GitHub recommends handling webhook events.

  • Always return a 2XX response: This is important to acknowledge receiving the event.

  • Handle retries: If your endpoint returns a status code other than 2XX, the event will be retried up to three times with an exponential backoff strategy. If the event is not successfully delivered after three retries, there are no further attempts to deliver it. You can still fetch it via the /events endpoint. Make sure you handle duplicate events in your system to avoid processing the same event multiple times.

  • Verify signature: Preferably, use the webhookSecret you provided during webhook registration to verify the event’s signature. This will ensure the integrity and origin of the event coming from Spark so that you can trust it.

Generate types from OpenAPI

The API is available in the Swagger UI and as an OpenAPI 3.0 file here.

You can use tools like OpenAPI Generator or Swagger Codegen to generate types (and, in some cases, API implementation) from your favorite language. This will help you work with the API in a strongly typed manner.

For example, to generate C# types from the OpenAPI file with openapi-generator, you can use the following command:

Terminal window
openapi-generator generate -i https://api.sandbox.voluespark.com/energy-coordination/v1/swagger/Partner/swagger.json -g csharp --skip-validate-spec

Or TypeScript

Terminal window
openapi-generator generate -i https://api.sandbox.voluespark.com/energy-coordination/v1/swagger/Partner/swagger.json -g typescript --skip-validate-spec

C# Implementation Example

Here is an example implementation of a C# webhook implementation that includes how to verify the signature of incoming events:

SparkWebhookEndpoint.cs
public class SparkWebhookEndpoint
{
public static async Task<Results<Ok, UnauthorizedHttpResult>> Handle(
HttpContext context,
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
{
Converters =
{
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 Currency
{
NOK = 1,
EUR = 2,
SEK = 3,
DKK = 4
}
internal enum EnergyUnit
{
KWh = 1,
MWh = 2
}
internal enum PriceArea
{
NO1,
NO2,
NO3,
NO4,
NO5,
FI,
DK1,
DK2,
SE1,
SE2,
SE3,
SE4
}
internal enum NotificationType
{
PriceCurveCreated,
UserEligibilityUpdated
}
internal enum SparkEventPayloadType
{
UserEligibility,
PriceCurve
}
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);
return typeValue switch
{
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);
}
}
}