Tutorial: receive webhooks on your machine

Draft. Written from the repository's documents and code; not yet reviewed by the owner.

Payrudder tells your application what happened (an invoice was paid, a subscription ended) by posting a signed JSON message to an address you register. This tutorial gets one such message to a handler on your own computer and shows how to see what went wrong when it does not arrive. The scheme itself (event types, headers, retries) is in Webhooks; here you do it.

You need a key with the webhook permissions and the command line, signed in (payrudder login).

1. A handler that verifies

Write the endpoint your application will have: read the raw body, verify the Payrudder-Signature header with the endpoint's secret, answer 2xx quickly, and ignore an event id you have seen before (deliveries are at least once). The code for your language is at the bottom of this page, made by the same generator as the Connect page of the console. Start it on localhost:3000, at /webhooks/payrudder.

2. Get deliveries to your machine

Payrudder posts to a registered address, so that address must reach your computer. Pick one:

The receiver verifies each delivery before it forwards it, and forwards it unchanged (body and headers), so your handler's own check runs on the real signature. What it prints:

Listening on http://127.0.0.1:4242 for Payrudder deliveries.
10:21:40  ping   0192…  attempt 1  signature ok  -> HTTP 200 (4ms)

3. Send an event

payrudder webhooks trigger ping

sends the one event the API can send on request, a ping, and waits for the answer of the endpoint. For a real event, make the thing happen: in the sandbox, close a period and the invoice's events (invoice.finalized, invoice.paid) follow.

4. When it does not arrive

payrudder webhooks deliveries --watch

lists the deliveries with their state, the number of attempts and the HTTP status your endpoint answered, and keeps printing each one that appears or changes. A delivery that was not accepted is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then given up; the console's Webhooks tab can send one again by hand. The usual causes:

You see Likely cause
REFUSED: … signature does not match in the receiver the wrong secret, or the body was changed before verification (a framework that parsed and re-encoded the JSON): verify the raw bytes
REFUSED: … outside the tolerance your computer's clock is more than five minutes off
-> … connection refused your handler is not running on the --forward-to address
HTTP 4xx or 5xx from your handler your handler failed: Payrudder gets the same status and will retry
nothing at all the registered address does not reach this computer (a tunnel that stopped, a firewall), or the endpoint is paused or subscribed to other event types

5. Before you go live

Register the endpoint of your deployed application (https://, a public address) for the live environment; the secret is per endpoint and per environment. You can keep the verification code: it is the same.

The code

The handler in each language, built from the SDKs of this repository (the install lines are the honest ones; the SDKs are not on a registry yet, see SDKs):

# Nothing to install: any HTTP client works.
export PAYRUDDER_API_KEY=qk_test_
# A delivery carries Payrudder-Signature: t=<unix time>,v1=<hex>. The hex is HMAC-SHA256 of "<t>.<raw body>" under your endpoint's secret.
# Check it on the raw body before parsing; the SDKs do it for you (see the other tabs).
# Not on npm yet: build the package from a checkout of the repository, then install the tarball it makes.
(cd /path/to/payrudder/sdk/typescript && npm ci && npm run build && npm pack)   # writes payrudder-sdk-0.0.0.tgz
npm install /path/to/payrudder/sdk/typescript/payrudder-sdk-0.0.0.tgz
import { constructEvent, WebhookVerificationError } from "@payrudder/sdk";

// Express: app.post("/webhooks/payrudder", express.raw({ type: "application/json" }), handler)
async function handler(req, res) {
  try {
    const event = await constructEvent(req.body, req.header("Payrudder-Signature") ?? "", process.env.PAYRUDDER_WEBHOOK_SECRET!);
    console.log(event.id, event.type);
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
}
# Not on PyPI yet: install from a checkout of the repository.
pip install /path/to/payrudder/sdk/python
from payrudder import WebhookSignatureError, construct_event

@app.post("/webhooks/payrudder")  # Flask
def hook():
    try:
        event = construct_event(request.get_data(), request.headers.get("Payrudder-Signature"), WEBHOOK_SECRET)
    except WebhookSignatureError:
        return "", 400
    print(event["id"], event["type"])
    return "", 200
# Not on a module proxy yet: point your module at a checkout of the repository.
go mod edit -require=github.com/nafrucom/payrudder-sdks/go@v0.0.0
go mod edit -replace=github.com/nafrucom/payrudder-sdks/go=/path/to/payrudder/sdk/go
ev, err := payrudder.ConstructEvent(body, r.Header.Get(payrudder.WebhookSignatureHeader), os.Getenv("PAYRUDDER_WEBHOOK_SECRET"), 0)
if err != nil {
	http.Error(w, "bad signature", http.StatusBadRequest)
	return
}
fmt.Println(ev.ID, ev.Type)
# Not on Packagist yet: use a checkout of the repository as a path repository.
composer config repositories.payrudder path /path/to/payrudder/sdk/php
composer require payrudder/payrudder-php:"*@dev"
use Payrudder\Webhook\Webhook;

$payload = file_get_contents('php://input');
try {
    $event = Webhook::constructEvent($payload, $_SERVER['HTTP_PAYRUDDER_SIGNATURE'] ?? '', getenv('PAYRUDDER_WEBHOOK_SECRET'));
} catch (\Payrudder\Exception\WebhookException $e) {
    http_response_code(400);
    exit;
}
echo $event->id;