repurch/ docs
Dashboard ↗repurch.com ↗

Events

Webhooks

Webhooks notify your back-end when a Repurch event needs downstream action. Two event families ship today: redemption.created for Exchange payouts, and collection.scheduled for Clearance pickups. Both deliver to the same endpoint; route on the type field. Repurch pays customers who choose cash directly; for customers who choose credit, Repurch settles the sale proceeds to you and you honour the credit at your checkout.

Setting up an endpoint

Configure your HTTPS endpoint via PATCH /v1/partner/settings/integrations/webhook, or from Settings → Integrations in the partner dashboard. The endpoint must:

  • Use HTTPS with a valid TLS certificate.
  • Respond with 2xx within 5 seconds.
  • Tolerate duplicate deliveries — Repurch retries on timeout or 5xx.

Retries follow exponential back-off for up to 24 hours. After that, the event is parked on the dead-letter queue and surfaced in the dashboard for manual replay.

Payout choice

When a customer's old sofa sells, they pick how they are paid. Your dashboard at Settings → Exchange controls which options are offered to your customers — most retailers enable all three.

credit

Store credit (boosted)

The customer's payout is store credit with you, pre-boosted by whatever your team configured (e.g. 1.10× for a 10% top-up). Repurch settles the base sale proceeds to your wallet; you fund the boost and honour the credit. The credit_destination field tells your handler whether to apply it to an existing order or whether the customer took a single-use code for later.

cash

Cash to bank (unboosted)

Repurch pays the base sale price (no boost) straight to the customer's bank. The customer submits their bank details to Repurch in the Exchange flow; you never handle them. No action is needed from your handler, and cash sales don't appear on your remittance.

When customer_choice is credit, credit_destination determines where the credit lands:

existing_order

Apply to existing order

credit_destination = existing_order. Credit is applied as a partial refund to the order in original_order_id (typically the new sofa they bought from you under a trade-in plan). Your handler refunds to the original payment method via your payment processor.

code

Save as a single-use code

credit_destination = code. Customer wants to spend the credit later. Repurch generates the single-use code (value + expires_at) and gives it to the customer. At checkout, validate and spend it with POST /v1/partner/credits/redeem; optionally mirror it into your gift card or loyalty system.

redemption.created

Fired once the customer's trade-in sells and they confirm their payout choice. For credit, the payload carries everything you need to honour it; for cash it is informational, because Repurch pays the customer.

Example delivery

http
POST /repurch/redemptions HTTP/1.1
Host: furnitureco.example
Content-Type: application/json
User-Agent: Repurch-Webhooks/1.0
X-Repurch-Event: redemption.created
X-Repurch-Event-Id: evt_2d8f3a17c4b9e62a
X-Repurch-Signature: t=1749548531,v1=2c0f9...

Payload (apply-to-existing-order example)

json
{
  "id": "evt_2d8f3a17c4b9e62a",
  "type": "redemption.created",
  "created_at": "2026-06-10T09:42:11+00:00",
  "data": {
    "id": "rdm_8c2f1a9d4e7b6035",
    "brand_id": "furniture-co",
    "customer_id": "cust_4a91c2e5",
    "customer_choice": "credit",
    "credit_destination": "existing_order",
    "value_pence": 52800,
    "value": 528.00,
    "currency": "GBP",
    "code": null,
    "expires_at": null,
    "original_order_id": "ORD-2026-0042",
    "related_listing_id": 12345,
    "sale_completed_at": "2026-06-09T14:00:00+00:00"
  }
}

Fields

id
string
Unique event identifier. Use it to deduplicate retries.
type
string
Always redemption.created for this event.
created_at
string
ISO 8601 UTC timestamp the event was emitted.
data.id
string
Stable redemption ID. Reference this in any logs or downstream records so a customer-support query can be traced back.
data.brand_id
string
The brand the redemption was issued against. Matches your workspace.
data.customer_id
string
Stable Repurch customer identifier.
data.customer_choice
string
One of credit | cash. Drives the top-level payout flow.
data.credit_destination
string | null
When customer_choice is credit, one of existing_order | code. null when customer_choice is cash.
data.value_pence
integer
Payout value in pence. Pre-boosted for credit; unboosted for cash.
data.value
number
Same value as GBP decimal. Always equal to value_pence / 100.
data.code
string | null
Single-use code, present when credit_destination is code. null otherwise.
data.expires_at
string | null
ISO 8601 expiry, present when credit_destination is code. null otherwise.
data.original_order_id
string | null
Order to credit against, present when credit_destination is existing_order. null otherwise.
data.related_listing_id
integer
The Exchange listing whose sale triggered this payout. Useful for support context.
data.sale_completed_at
string
ISO 8601 UTC timestamp the underlying sale completed (delivery to the buyer).

Handler skeleton

javascript
// Node.js / Express
//
// Cash: Repurch pays the customer straight to their bank. Nothing
// for you to do, and the sale is not on your remittance.
// Credit: Repurch settles the sale proceeds to your wallet (48h after
// delivery) and you honour the credit, so this webhook tells your
// back-end where that credit lands.
app.post("/repurch/redemptions", express.json(), async (req, res) => {
  const event = req.body;

  if (event.type === "redemption.created") {
    const { id, customer_choice, value_pence, customer_id,
            credit_destination, original_order_id, code,
            expires_at } = event.data;

    if (customer_choice === "cash") {
      // Informational only. Repurch has already collected the
      // customer's bank details and pays them directly.
      await crm.logTradeIn({ customer_id, reference: id, payout: "cash" });
    } else if (customer_choice === "credit") {
      // value_pence already includes whatever boost your team has
      // configured. Where it lands depends on credit_destination.
      if (credit_destination === "existing_order") {
        // Partial refund to the original payment method for the
        // order the customer already placed with you.
        await stripe.refunds.create({
          payment_intent: getPaymentIntentForOrder(original_order_id),
          amount: value_pence,
          reason: "requested_by_customer",
          metadata: { repurch_redemption_id: id },
        });
      } else {
        // Repurch generated the single-use code and gave it to the
        // customer. Optionally mirror it into your gift card system;
        // at checkout, validate + spend it with
        // POST /v1/partner/credits/redeem.
        await giftCards.mirror({
          code,
          discount_pence: value_pence,
          customer_id,
          expires_at,
        });
      }
    }
  }

  // Acknowledge within 5 seconds. Repurch retries 5xx and timeouts
  // with exponential back-off for up to 24 hours.
  res.status(200).json({ ok: true });
});

collection.scheduled

Fired when a Clearance sale's collection date is confirmed in the partner dashboard. Your logistics provider integration uses the payload to dispatch a pickup from the chosen warehouse to the buyer. The buyer's delivery window is included so route planners can slot the delivery leg into the same vehicle run.

Example delivery

http
POST /repurch/collections HTTP/1.1
Host: furnitureco.example
Content-Type: application/json
User-Agent: Repurch-Webhooks/1.0
X-Repurch-Event: collection.scheduled
X-Repurch-Event-Id: evt_a8b14e932cd0f76e
X-Repurch-Signature: t=1749728922,v1=8d4a1...

Payload

json
{
  "id": "evt_a8b14e932cd0f76e",
  "type": "collection.scheduled",
  "created_at": "2026-06-12T11:08:42+00:00",
  "data": {
    "id": "col_5b209a17e4f6c81d",
    "brand_id": "furniture-co",
    "order_id": 12345,
    "order_reference": "#12345",
    "product_id": 67890,
    "scheduled_for": "2026-06-15",
    "scheduled_at": "2026-06-12T11:08:42+00:00",
    "warehouse_id": "f3c1a4b5-9d22-4e6f-8a01-7b2d4e8c9f10",
    "warehouse_label": "Nottingham hub",
    "buyer_window_start": "2026-06-12",
    "buyer_window_end": "2026-06-22"
  }
}

Fields

id
string
Unique event identifier. Use it to deduplicate retries.
type
string
Always collection.scheduled for this event.
created_at
string
ISO 8601 UTC timestamp the event was emitted.
data.id
string
Stable collection ID. Reference in your dispatch records for support traceability.
data.brand_id
string
The brand the collection belongs to. Matches your workspace.
data.order_id
integer
WooCommerce-side order ID for the sold item.
data.order_reference
string
Display-friendly order reference, e.g. #12345.
data.product_id
integer
Product ID of the line item being collected.
data.scheduled_for
string
The confirmed collection date (YYYY-MM-DD). Always inside the buyer's delivery window.
data.scheduled_at
string
ISO 8601 UTC timestamp the retailer pressed Schedule.
data.warehouse_id
string | null
The collection-source warehouse UUID. null if the brand has no warehouses configured.
data.warehouse_label
string
Display label for the warehouse, e.g. 'Nottingham hub'. Empty when no warehouse is assigned.
data.buyer_window_start
string
Start of the buyer's 10-day delivery window (YYYY-MM-DD). Useful for the delivery leg.
data.buyer_window_end
string
End of the buyer's 10-day delivery window (YYYY-MM-DD).

Delivery semantics are identical to redemption.created: same endpoint (configured at Settings → Integrations), same HMAC signature scheme, same retry policy. Route on the type field.

Pull-based alternative

If you'd rather poll than receive webhooks, the same data is available via GET /v1/redemptions?since=<timestamp>. That endpoint returns the same payload shape under the data envelope. Most partners pick one or the other — you can run both during cutover.

Signature verification

Every delivery carries an X-Repurch-Signature header in the Stripe-compatible t=<timestamp>,v1=<hmac> format. The HMAC is SHA-256 over <timestamp>.<raw body> using your endpoint's signing secret as the key.

Signature verification ships in a future iteration. Until then, treat the webhook URL itself as the shared secret: use a long unguessable path component and reject requests to any other path.