Skip to content

feat: add webhook delivery retry - #18

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
Collaborator

What changed

Adds webhooks().deliveries().retry(webhook_id, delivery_id) for the new POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry endpoint, and regenerates the types from the latest API spec.

  • retry sends the delivery's original payload to the webhook again now. The retry counts as a delivery attempt. If it fails with a retryable error, automatic retries continue.
  • It uses the team token, sends no request body and returns RetryWebhookDeliveryResponse { message, delivery_id } (HTTP 202).
  • It sits next to retrieve on the webhooks().deliveries() sub-client, where the other delivery calls already are.
  • HTTP 409 maps to the existing Error::Conflict, and error.code() returns WEBHOOK_DISABLED, WEBHOOK_DELIVERY_NOT_RETRYABLE or WEBHOOK_DELIVERY_RETRY_IN_PROGRESS. 404, 429 (Retry-After), 401 and 403 map to the existing error variants. There is no new error type.
  • The operation takes no Idempotency-Key, so the method has no options argument (same as webhooks().test).
  • The README's Team API table lists retry, with a one-line note on what it does.

Non-breaking. Suggested bump: minor.

API changes in the regenerated types

Where Change
POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry New operation ops::RetryWebhookDelivery (team token, no body, no Idempotency-Key). 202 returns the new RetryWebhookDeliveryResponse { message: String, delivery_id: String }. 409 is an ApiError with code WEBHOOK_DISABLED, WEBHOOK_DELIVERY_NOT_RETRYABLE or WEBHOOK_DELIVERY_RETRY_IN_PROGRESS. 404, 401, 403 and 429 are also documented.
WebhookDeliveryData, WebhookDeliveryListData New next_retry_at: Option<Option<String>>: the date-time of the next automatic retry. It is Some(None) when no retry is scheduled and None when the key is missing.
WebhookDeliveryData payload, response_body, response_headers, error_message Types unchanged. The doc comments now say these fields are always included in requests made with a team API token.
Webhook delivery list/retrieve, retry, regenerate-secret 401, 403 and 429 error bodies are documented as either { "message" } or { "error": { "code", "message", ... } }. 429 is now described inline and no longer uses the shared TooManyRequests response. The SDK already parses both shapes. A test covers a plain-message 429 on retry.
OPERATIONS 59 → 60 operations.

Usage

use lettermint::{Error, Lettermint};

let lettermint = Lettermint::builder().team_token(std::env::var("LETTERMINT_TEAM_TOKEN")?).build()?;

match lettermint.webhooks().deliveries().retry("webhook-id", "delivery-id").await {
    Ok(retried) => println!("{}: {}", retried.delivery_id, retried.message),
    Err(Error::Conflict(error)) => println!("not retried: {:?}", error.code()), // e.g. WEBHOOK_DELIVERY_NOT_RETRYABLE
    Err(error) => return Err(error.into()),
}

let delivery = lettermint.webhooks().deliveries().retrieve("webhook-id", "delivery-id").await?;
if let Some(Some(at)) = &delivery.next_retry_at {
    println!("next automatic retry at {at}");
}

Checks

All of these were run on Rust 1.98.0 (MSRV) and stable, and all passed:

  • scripts/generate.sh --check: generated files up to date
  • cargo fmt --all -- --check: passed
  • cargo clippy --all-targets --all-features --locked -- -D warnings: no warnings
  • cargo test --all-features --locked: 135 passed, 0 failed (1 ignored, as before), including the README doctests
  • cargo build --no-default-features --locked: passed
  • RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps --locked: passed

Other checks:

  • cargo +stable package --locked: passed

  • cargo audit: no advisories (exit 0)

  • CI secret-pattern grep over src tests README.md UPGRADE.md CHANGELOG.md Cargo.toml: no matches

  • cargo semver-checks check-release --baseline-rev v2.1.0: 228 of 229 checks pass. The one failure, constructible_struct_adds_field, is for:

    • WebhookDeliveryData.next_retry_at (this PR)
    • WebhookDeliveryListData.next_retry_at (this PR)
    • MessageData.resent_from_message_id (already on main, not released yet)

    These are fields added to generated response types. Earlier minor releases shipped the same kind of change, so this is a minor change.

New tests in tests/team_api.rs:

  • retry sends a POST to /webhooks/w1/deliveries/x1/retry with the team Authorization header. It sends no body, no content type, no X-Lettermint-Token and no Idempotency-Key, and it parses the 202 response.
  • Bad path parameters (.., empty) and a client that only has a sending token both return Error::Config, and no request is sent.
  • A 409 WEBHOOK_DELIVERY_NOT_RETRYABLE becomes Error::Conflict with that code and the message.
  • A 429 with a plain { "message" } body becomes Error::RateLimit and reads Retry-After.
  • next_retry_at on a retrieved delivery decodes as a time, as null and when the key is missing.

The coverage test in src/tests.rs reaches all 60 operations.

Not run locally

  • The CI matrix on Windows and macOS stable. Locally, tests ran on macOS with 1.98.0 and stable.

Suggested release tag

v2.2.0. This release would also include the unreleased messages resend and per-email batch idempotency keys from main.

🤖 Generated with Claude Code

bjarn and others added 2 commits October 18, 2026 22:51
- New operation POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry
  (no body; 202 returns RetryWebhookDeliveryResponse with message and
  delivery_id; 409 ApiError when the webhook is disabled, the delivery
  cannot be retried or a retry is already in progress).
- WebhookDeliveryData and WebhookDeliveryListData gain next_retry_at
  (optional; null when no automatic retry is scheduled).
- WebhookDeliveryData payload, response_body, response_headers and
  error_message keep their types; their descriptions now say they are
  always included with a team API token.
- Webhook delivery endpoints and regenerate-secret document 401, 403
  and 429 bodies as either ErrorMessage or ApiError.

Co-Authored-By: Claude <noreply@anthropic.com>
- webhooks().deliveries().retry(webhook_id, delivery_id) calls
  POST /webhooks/{webhookId}/deliveries/{deliveryId}/retry with the team
  token and no body, and returns RetryWebhookDeliveryResponse. It sends
  the delivery's original payload to the webhook again now; the retry
  counts as a delivery attempt, and automatic retries continue if it
  fails with a retryable error.
- A disabled webhook, a delivery that cannot be retried or a retry in
  progress is Error::Conflict, with WEBHOOK_DISABLED,
  WEBHOOK_DELIVERY_NOT_RETRYABLE or WEBHOOK_DELIVERY_RETRY_IN_PROGRESS
  as its code.
- Test next_retry_at on deliveries (a time, null or absent).
- List retry in the README's Team API table.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn requested a review from a team as a code owner October 18, 2026 20:59
@bjarn
bjarn merged commit f3a4131 into main Oct 19, 2026
6 checks passed
@bjarn
bjarn deleted the feat/webhook-delivery-retry branch October 19, 2026 17:55
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