Webhook Endpoint
A self-hosted PayPal webhook receiver runs on the opshell server and is exposed through the existing Cloudflare tunnel.
Status
| Item | State |
|---|---|
| Receiver service | Running (paypal-webhook), local health OK |
| Local bind | 127.0.0.1:5055 → container 5055 |
| Public URL | https://paypal-webhook.id86.net — returns 404 until the tunnel route is added |
| DNS CNAME | Created (paypal-webhook.id86.net → f379d8e1-….cfargotunnel.com, proxied) |
| Tunnel ingress | Pending — remote (dashboard) config; API token lacks Cloudflare Tunnel: Edit |
| PayPal webhook registration | Pending public route + webhook ID |
| Signature verification | Disabled (REQUIRE_SIGNATURE=false) during the connectivity phase |
Architecture
Service
| Field | Value |
|---|---|
| Location | /home/rezriz/github/automation/paypal-webhook/ |
| Runtime | Python 3.12 + Flask (Docker) |
| Container | paypal-webhook |
| Network | external app-network (shared with cloudflared) |
| Internal port | 5055 |
| Host bind | 127.0.0.1:5055 (loopback only) |
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /healthz | Liveness + configuration probe |
| GET | / | Minimal service info |
| POST | /webhook/paypal | PayPal webhook receiver |
Data flow
- PayPal
POSTs the event with transmission headers. - The receiver reads the raw JSON body.
- If
REQUIRE_SIGNATURE=true, it calls PayPalverify-webhook-signaturewith the transmission headers and the event. Anything other thanSUCCESSreturns401. - Accepted and rejected events are appended to
data/events/events-YYYY-MM-DD.jsonl, including the full payload.
Configuration
Credentials are loaded from ~/.ssh/paypal/.env (see
Sandbox Testing); app settings live in the service
.env.
| Key | Purpose |
|---|---|
REQUIRE_SIGNATURE | Verify deliveries with PayPal (true in production) |
PAYPAL_WEBHOOK_ID | Webhook ID returned by PayPal |
PORT / DATA_DIR | Listen port / event log directory |
Routing
DNS is already created. The remote tunnel configuration must add:
- hostname: paypal-webhook.id86.net
service: http://paypal-webhook:5055
Local tunnel files (/opt/docker-data/tunnel/config/config.yml and
config-v2.yml) were updated for reference, but the running tunnel uses the
remote (dashboard) configuration. Add the rule in Zero Trust → Networks →
Tunnels → VPS-GSM16 → Public Hostname. The current API token lacks
Cloudflare Tunnel: Edit and returns 1001 Not authorized on the tunnel
config endpoints.
Operations
cd /home/rezriz/github/automation/paypal-webhook
docker compose build
docker compose up -d
docker compose logs -f paypal-webhook
curl -s http://127.0.0.1:5055/healthz | jq
Security
- Signature verification is mandatory once
PAYPAL_WEBHOOK_IDis set. - The host bind is loopback-only; public access is exclusively through the tunnel.
.envanddata/are gitignored; credentials stay in~/.ssh/paypal/.env.