# Guide to Redo Webhooks: Features and Best Practices

Redo webhooks tell your systems when a return is created or changes. Redo runs returns and exchanges for e-commerce stores, and its webhooks let a warehouse system, 3PL, ERP or support tool react to each return as it moves through processing, instead of polling the API for changes.

This guide covers the return events and payload, how to subscribe and authenticate, how backfill and replay work, and how to receive Redo webhooks reliably.

## What are Redo webhooks?

Redo webhooks are HTTP `POST` requests with a JSON body, sent to an HTTPS URL you register through the Redo API. You subscribe to topics, and when a return in your store is created or updated, Redo sends the whole return and its order.

Redo does not sign webhooks. When you create a subscription you supply a secret of your own, and Redo sends it on every delivery as `Authorization: Bearer <secret>`. Your receiver compares that header with the value it expects.

## Redo webhook features

| Feature | Details |
| --- | --- |
| Configuration | API: `POST /v2.2/stores/{storeId}/webhooks` |
| Topics | `return.created`, `return.updated` |
| Body `type` values | `created`, `updated`, `backfill` |
| Method and format | `POST`, JSON |
| Endpoint | HTTPS required |
| Authentication | `Authorization: Bearer <secret>`, with a secret you generate |
| Signature and timestamp | None |
| Expected response | 2xx within 5 seconds |
| Retries | Multiple times before the event is discarded; count and schedule not published |
| Ordering | In order per return, not across returns |
| Replay | `POST /v2.2/webhooks/{webhookId}/replay` with a time range |

## Redo webhook events

You subscribe with topic names, and each delivery tells you what happened in its `type` field:

| `type` | Meaning |
| --- | --- |
| `created` | A customer or team member created a new return |
| `updated` | An existing return changed, for example its status or items |
| `backfill` | A historical event sent during webhook setup or replay |

The `backfill` type matters from day one. Redo sends historical events during webhook setup, so a new receiver can get `backfill` requests for returns that already exist before any live event arrives. Redo's docs tell you to handle all three types.

## The payload

This trimmed example is based on the one in Redo's documentation:

```json
{
  "type": "created",
  "at": "2023-08-18T12:00:00Z",
  "merchantName": "Example Merchant",
  "return": {
    "id": "64df65d4c5a4ca3eff4b4e43",
    "status": "pending",
    "createdAt": "2023-08-18T12:00:00Z",
    "updatedAt": "2023-08-18T12:00:00Z",
    "order": { "id": "shopify-order-123" },
    "items": [
      {
        "id": "item-1",
        "orderItem": { "id": "order-123/1" },
        "quantity": 1,
        "reason": "Too big",
        "upc": "012345678905",
        "processingImages": [],
        "customerImages": []
      }
    ],
    "totals": {
      "refund": { "amount": { "amount": "50.00", "currency": "USD" } }
    }
  },
  "order": {
    "id": "shopify-order-123",
    "items": [
      {
        "id": "order-123/1",
        "quantity": 1,
        "product": { "externalId": "product-456", "name": "T-Shirt" }
      }
    ]
  }
}

```

The fields you will use most:

* `type`: `created`, `updated` or `backfill`.
* `at`: when the event occurred, in ISO 8601.
* `merchantName`: the merchant team that owns the return, useful when one receiver handles several merchants.
* `return`: the complete return, including `source` (the customer's contact and address), `destination`, `giftCards` and `totals` for refund, exchange, store credit and charge.
* `return.items[]`: each returned item, with `upc` (`null` if no barcode was recorded), `processingImages` added by the merchant or warehouse, and `customerImages` uploaded by the customer.
* `order`: the original order.

> **Inspect Redo webhooks as they arrive.** Create a subscription with a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the backfill and live events Redo sends, with no account or setup required.

## Setting up Redo webhooks

Subscriptions are managed through the Redo API, authenticated with your Redo API secret.

1. Generate a long random subscriber secret, for example with `openssl rand -hex 32`. This is separate from your API secret.
2. Create the subscription:

```bash
curl -X POST "https://api.getredo.com/v2.2/stores/$STORE_ID/webhooks" \
  -H "Authorization: Bearer $REDO_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/redo",
    "secret": "YOUR_LONG_RANDOM_SUBSCRIBER_SECRET",
    "topics": ["return.created", "return.updated"]
  }'

```

All three fields are required, and the URL must use HTTPS. To manage subscriptions afterwards:

* List them with `GET /v2.2/stores/{storeId}/webhooks`.
* Update one with `PATCH /v2.2/webhooks/{webhookId}`. Redo's example changes `topics`.
* Delete one with `DELETE /v2.2/webhooks/{webhookId}`.

To route through Hookdeck, create a Hookdeck source with the **Redo** type and use its URL as the subscription `url`. The source has one field, **Subscription Secret**. Paste the `secret` you set on the subscription. Hookdeck expects Redo's `Bearer ` prefix in front of it, so you don't need to type that, and pasting the full `Bearer 3f9a...` value works too. Hookdeck answers Redo with a `200` either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service. The source accepts `POST`, which is the method Redo uses.

## Securing Redo webhooks

Redo's own example compares the header with ``Bearer ${WEBHOOK_SECRET}`` using `!==`. The check is right, but a constant-time comparison is better:

```javascript
const crypto = require('crypto');
const express = require('express');

// The whole header value: "Bearer " followed by your subscriber secret.
const EXPECTED = process.env.REDO_AUTHORIZATION;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/redo', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Queue it and answer within Redo's 5-second window.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

EXPECTED = os.environ.get("REDO_AUTHORIZATION", "")

def verify_redo_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get("authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A Bearer secret authenticates the sender, not the body. It cannot detect a modified payload, and with no signature or timestamp a captured request can be replayed. Keep the endpoint on HTTPS, which Redo requires anyway, use a long random secret, and keep it out of logs. Redo documents updating a subscription's topics with `PATCH` but not changing its secret, so if the secret might have leaked and a `PATCH` with a new `secret` is not accepted, create a new subscription and delete the old one.

> **Make Redo webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Authorization value on every request, responds to Redo straight away so you stay inside the 5-second window, and retries delivery to your service on your schedule.

## Redo webhook limitations and pain points

### A five-second window and an unknown retry budget

**The Problem:** A slow or unavailable receiver can lose return events.

**Why It Happens:** Redo asks for a 2xx within 5 seconds and retries "multiple times before discarding the event". It does not say how many times or over what period.

**Workarounds:**

* Store the request and return `200` before doing any work.
* After an outage, use the replay endpoint to resend the affected time range.

**How Hookdeck Can Help:** Hookdeck accepts each request and responds to Redo immediately, then delivers to your service with [retries](/docs/retries) you configure. Events that still fail stay in Hookdeck, where you can fix the problem and retry them, instead of being discarded.

### Backfill on setup

**The Problem:** A new subscription can send historical events that a receiver built for live traffic may not expect, possibly many at once.

**Why It Happens:** Redo sends `backfill` events during webhook setup and replay.

**Workarounds:**

* Handle `backfill` explicitly, usually as an upsert that does not trigger customer-facing actions such as emails.
* Make sure the receiver can absorb a burst before you create the subscription.

**How Hookdeck Can Help:** A [filter](/docs/filters) on `type` can send `backfill` events to a separate [connection](/docs/connections), and a [max delivery rate](/docs/destinations#set-a-max-delivery-rate) spreads the burst out so it does not overload your service.

### Ordering only per return

**The Problem:** Events for different returns arrive interleaved, and processing them in parallel can apply an older update after a newer one for the same return.

**Why It Happens:** Redo delivers events in order for each return, but different returns may arrive in any order.

**Workarounds:**

* Compare `at` with the last value you stored for that return, and ignore anything older.
* Process events for the same `return.id` one at a time.

**How Hookdeck Can Help:** Hookdeck keeps the full history of each return's events, so you can see the order they arrived in and replay them if your handler got the sequence wrong.

### Duplicate deliveries

**The Problem:** The same event can arrive more than once, for example after a retry or a replay.

**Why It Happens:** Redo retries until it gets a 2xx, and replay resends events you may already have.

**Workarounds:**

* Use `return.id` plus `at` as the idempotency key, as Redo recommends. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

**How Hookdeck Can Help:** [Deduplication](/docs/deduplication) can drop requests whose `return.id` and `at` match a recent one.

## Best practices

### Store the whole Authorization value

Keep `Bearer ` and the secret together as one expected value, and compare it whole. That way your check matches exactly what Redo sends.

### Process by the at timestamp

Treat `at` as the order of truth for each return, and do not let an older event overwrite newer state.

### Handle every type, including unknown ones

Handle `created`, `updated` and `backfill`, and log and accept anything else rather than failing. Returning an error for an unexpected type only causes retries.

### Use replay to recover

After downtime, call `POST /v2.2/webhooks/{webhookId}/replay` with a `startTime` and `endTime` covering the gap. Replayed events arrive as `backfill`, and your idempotency key keeps them from being applied twice.

### Acknowledge fast and process asynchronously

Verify, store, return `200`, then update your systems. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Redo webhooks send the complete return and order whenever a return is created or updated, with a `backfill` type for history and a replay endpoint for recovery. They are authenticated only by a Bearer secret you choose, so compare the whole header in constant time over HTTPS, answer within five seconds, and make processing idempotent on `return.id` and `at`.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, answers Redo immediately, absorbs backfill bursts, and keeps failed events for retry instead of letting them be discarded.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Redo webhooks reliably in minutes.