Some payments finish later, not during the request: the customer confirms in their bank's app a few minutes after checkout, or pays by bank transfer. Your shop can't wait for that. Instead, the provider calls your shop when it happens: it sends a POST to an address you gave it. That address is a webhook. For once, your shop is the server and the provider is the client.
Events: the provider sends an event as JSON in the request's body: what happened in "type", and the payment it happened to in "data", an object inside the object. Read the body as text with a StreamReader, then turn it into a JsonElement. Call it evt: event is a C# keyword. The same idea for a mail service that tells you an email bounced:
var bounced = new List<string>();
app.MapPost("/webhooks/mail", async (HttpRequest request) => {
var body = await new StreamReader(request.Body).ReadToEndAsync();
var evt = JsonSerializer.Deserialize<JsonElement>(body);
if (evt.GetProperty("type").GetString() == "email.bounced") {
bounced.Add(evt.GetProperty("data").GetProperty("to").GetString());
}
return Results.Json(new { received = true });
});
Answer {"received": true} to every event, even the ones you don't act on, like payment.failed: a provider resends an event until it gets a 2xx answer. The path has no id in it, so the order's id comes from the event, in "data":
POST /webhooks/payments {"type": "payment.failed", "data": {"id": "pay_7", "amount": 21, "order_id": 10}}
→ 200 {"received": true}
GET /orders/10 → 200 {…, "payment_id": null, "status": "pending", "total": 21}
POST /webhooks/payments {"type": "payment.succeeded", "data": {"id": "pay_8", "amount": 21, "order_id": 10}}
→ 200 {"received": true}
GET /orders/10 → 200 {…, "payment_id": "pay_8", "status": "paid", "total": 21}
Your task: add POST /webhooks/payments with an async lambda that takes HttpRequest request. Read the event, and when its type is "payment.succeeded", mark the order paid with the payment's id. Answer {"received": true} for every event.
Assignment
Follow the Coding Tutorial to build the webhook POST /webhooks/payments.
Hint
The payment is evt.GetProperty("data"). Use the same UPDATE as in the pay route, with the payment's "id" as a string (GetString()) and its "order_id" as an int (GetInt32()), then run it with ExecuteNonQuery(). SQLite saves the change at once: there is no commit to write.
Free plan: 30 tutor messages. PRO is unlimited.
