Start learning free

LESSON 109 OF 173 · BACKEND: PAYMENTS AND OTHER SERVICES

Webhooks: The Provider Calls You

Practice

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: what happened in "type", and the payment it happened to in "data", an object inside the object. You read the body's bytes with io.ReadAll and fill a struct from them with json.Unmarshal; the file already gives you the struct PaymentEvent, with Type and Data. The same idea for a mail service that tells you an email bounced:

var bounced = []string{}

type MailEvent struct {
	Type string            `json:"type"`
	Data map[string]string `json:"data"`
}

app.HandleFunc("POST /webhooks/mail", func(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	var event MailEvent
	json.Unmarshal(body, &event)
	if event.Type == "email.bounced" {
		bounced = append(bounced, event.Data["to"])
	}
	writeJSON(w, 200, map[string]bool{"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: at the end of newApp, add the route POST /webhooks/payments. Read the body into a PaymentEvent, 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 event.Data, a map[string]any. Use the same UPDATE as in the pay route, with payment["id"] and payment["order_id"]. db.Exec saves the change at once: there is no commit to call.


STEP 1 OF 8
    Ctrl + Enter to check