Skip to content

feat: add webhook delivery retry - #39

Merged
bjarn merged 2 commits into
mainfrom
feat/webhook-delivery-retry
Oct 19, 2026
Merged

bjarn merged 2 commits into
mainfrom
feat/webhook-delivery-retry

Conversation

@bjarn

@bjarn bjarn commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

What changed

Adds client.webhooks.deliveries.retry(webhook_id, delivery_id). It sends a delivery's original payload to the webhook again now and returns a RetryWebhookDeliveryResponse. The retry counts as a delivery attempt; if it fails with a retryable error, automatic retries continue. It uses the team token and sends no request body and no Idempotency-Key header. Sync and async clients both have it.

It sits next to webhooks.deliveries.retrieve, like every other /webhooks/{webhookId}/deliveries operation in this SDK. A 409 raises the existing ConflictError, with the API error code in .code (WEBHOOK_DISABLED, WEBHOOK_DELIVERY_NOT_RETRYABLE or WEBHOOK_DELIVERY_RETRY_IN_PROGRESS). No new error class.

The types are regenerated from the latest API spec in their own commit.

Not a breaking change. Suggested version bump: minor.

API changes in the regenerated types

Change Where Notes
New operation POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry operations.py, new RetryWebhookDeliveryResponse (message, delivery_id) Team token, no request body. 202 on success; 404 when the webhook or delivery is not found; 409 ApiErrorBody when it cannot be retried; 429 with Retry-After; 401/403
New next_retry_at WebhookDeliveryData, WebhookDeliveryListData NotRequired[str | None]: when the next automatic retry is due, None when none is scheduled
Description note WebhookDeliveryData.payload, response_body, response_headers, error_message They now say they are always included with a team API token. Their types do not change
Error body shapes GET /webhooks/{webhookId}/deliveries, GET /webhooks/{webhookId}/deliveries/{deliveryId}, POST /webhooks/{webhookId}/regenerate-secret 401, 403 and 429 list either ErrorMessage ({message}) or ApiErrorBody ({error: {code, message, ...}}). 429 no longer points to the shared TooManyRequests response. The error parser already handled both shapes; the new tests cover both on the retry call

Usage

from lettermint import ConflictError, Lettermint

lettermint = Lettermint(team_token=os.environ["LETTERMINT_TEAM_TOKEN"])

delivery = lettermint.webhooks.deliveries.retrieve(webhook_id, delivery_id)
print(delivery["status"], delivery.get("next_retry_at"))

try:
    queued = lettermint.webhooks.deliveries.retry(webhook_id, delivery_id)
    print(queued["message"], queued["delivery_id"])
except ConflictError as error:
    print(error.code)  # e.g. "WEBHOOK_DELIVERY_NOT_RETRYABLE"

Checks

  • python -m ruff check src tests scripts: all checks passed
  • python -m ruff format --check src tests scripts: 27 files already formatted
  • python -m mypy: no issues in 34 source files
  • python scripts/unasync.py --check: src/lettermint/_sync is up to date
  • python scripts/generate.py --check: generated files up to date
  • python -m pytest tests/: 264 passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14
  • python -m build, then python -m twine check --strict dist/*: both artifacts passed
  • Wheel smoke test from the CI build job (fresh venv, UPGRADE.md and py.typed in the wheel, client repr): passed. webhooks.deliveries.retry is present in the installed wheel

New tests (tests/test_webhooks.py):

  • path, POST, team Authorization header, empty body, no Content-Type or Idempotency-Key
  • 202 parsed as RetryWebhookDeliveryResponse
  • the signature takes no idempotency key
  • a sending-only client raises LettermintConfigError before any request
  • both path parameters are encoded and validated
  • 409 WEBHOOK_DELIVERY_NOT_RETRYABLE raises ConflictError with the code
  • 403, 404 and 429 in both body shapes, including retry_after
  • the async client
  • next_retry_at on deliveries.retrieve and deliveries.list, as a value and as None

tests/test_surface.py now covers the new operation and the webhooks.deliveries layout.

Not run locally

Nothing. The CI matrix and the build job were reproduced locally.

Suggested release tag

v3.2.0 (minor). main also has the unreleased messages.resend and per-email batch idempotency keys since v3.1.0.

🤖 Generated with Claude Code

bjarn and others added 2 commits October 18, 2026 22:51
- New operation: POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry
  (team token, no request body). Returns 202 with a
  RetryWebhookDeliveryResponse (message, delivery_id); 409 with an
  ApiErrorBody when the delivery cannot be retried.
- WebhookDeliveryData and WebhookDeliveryListData gain an optional,
  nullable next_retry_at.
- WebhookDeliveryData payload, response_body, response_headers and
  error_message note that they are always included with a team API token.
- The webhook delivery and regenerate-secret operations list 401, 403 and
  429 bodies as either ErrorMessage or ApiErrorBody.

Co-Authored-By: Claude <noreply@anthropic.com>
- webhooks.deliveries.retry(webhook_id, delivery_id, *, timeout=None)
  sends a delivery's original payload to the webhook again now and
  returns a RetryWebhookDeliveryResponse. It uses the team token and
  sends no body and no Idempotency-Key header.
- A 409 (webhook disabled, delivery not retryable, retry in progress)
  raises ConflictError with the error code.
- Document it in the README and test the request, the token, both path
  parameters, the 409 and the 403, 404 and 429 errors in either body
  shape, and next_retry_at on deliveries.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn requested a review from a team as a code owner October 18, 2026 20:56
@bjarn
bjarn merged commit b8f589b into main Oct 19, 2026
7 checks passed
@bjarn
bjarn deleted the feat/webhook-delivery-retry branch October 19, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants