The docs sample for verifying webhook_config.uris (dynamic) webhook deliveries shows JWT/RS256 verification against JWKS (extract kid, verify RS256, check aud). Actual deliveries don't send a JWT at all - real header/signature format differs entirely.
What's actually sent (confirmed against live deliveries)
Headers:
Webhook-Id: msg_
Webhook-Timestamp:
Webhook-Signature: v1a,
This is the Standard Webhooks header convention, but with the asymmetric v1a (Ed25519) scheme rather than the documented JWT approach. Notably, the official standardwebhooks pip package (shown in a separate doc sample as the verification method for registered webhooks) only implements symmetric v1 (HMAC) and silently ignores v1a signatures, so it can't verify dynamic webhook deliveries even though the header names match.
Correct verification (worked out empirically, not from any doc)
- Signed message: f"{msg_id}.{timestamp}.{raw_body}" (same construction as symmetric Standard Webhooks, just Ed25519-signed instead of HMAC'd).
- Verify against Google's public keys at https://generativelanguage.googleapis.com/.well-known/jwks.json — three OKP/Ed25519 keys are published there, not RSA keys as the JWT doc implies, and none carry a kid, so a verifier has to try each key in turn.
Payload envelope (also undocumented for this path, matches a different doc's example):
{"type": "interaction.completed", "data": {"id": "..."}, "id": "evt_...", "created_at": 0, "user_metadata": {...}}
Impact: Following the documented JWT/JWKS sample for dynamic webhooks fails outright (Not enough segments - the value isn't a JWT), with no indication of what the correct scheme is.
Ask: Update the dynamic webhook verification sample to reflect the real Webhook-Id/Webhook-Timestamp/Webhook-Signature: v1a,... Ed25519 scheme (or fix the backend to actually send what's documented, if the JWT approach was the intended design).
Environment details
- Programming language: Python
- OS: Mac
- Language runtime version: 3.12
- Package version: 2.20.0
The docs sample for verifying webhook_config.uris (dynamic) webhook deliveries shows JWT/RS256 verification against JWKS (extract kid, verify RS256, check aud). Actual deliveries don't send a JWT at all - real header/signature format differs entirely.
What's actually sent (confirmed against live deliveries)
Headers:
Webhook-Id: msg_
Webhook-Timestamp:
Webhook-Signature: v1a,
This is the Standard Webhooks header convention, but with the asymmetric v1a (Ed25519) scheme rather than the documented JWT approach. Notably, the official standardwebhooks pip package (shown in a separate doc sample as the verification method for registered webhooks) only implements symmetric v1 (HMAC) and silently ignores v1a signatures, so it can't verify dynamic webhook deliveries even though the header names match.
Correct verification (worked out empirically, not from any doc)
Payload envelope (also undocumented for this path, matches a different doc's example):
{"type": "interaction.completed", "data": {"id": "..."}, "id": "evt_...", "created_at": 0, "user_metadata": {...}}
Impact: Following the documented JWT/JWKS sample for dynamic webhooks fails outright (Not enough segments - the value isn't a JWT), with no indication of what the correct scheme is.
Ask: Update the dynamic webhook verification sample to reflect the real Webhook-Id/Webhook-Timestamp/Webhook-Signature: v1a,... Ed25519 scheme (or fix the backend to actually send what's documented, if the JWT approach was the intended design).
Environment details