Gareth Wilson Gareth Wilson

Guide to Etsy Webhooks: Features and Best Practices

Published


Etsy webhooks tell your app when something happens in a shop it is authorised for: an order is paid, cancelled, shipped or delivered, a listing changes, or a product's inventory runs out. If you sync Etsy orders into a fulfilment system or keep listings and stock consistent across channels, webhooks replace repeated polling of the Open API.

This guide covers how Etsy webhooks are configured and signed, the events and their payloads, the retry schedule and portal tools, and the practices that keep an Etsy receiver reliable in production.

What are Etsy webhooks?

Etsy webhooks are part of the Etsy Open API v3 and are available to both commercial and personal apps. You configure them in Etsy's Webhook portal, one endpoint at a time, by choosing a callback URL and the events to subscribe to. When an event fires, Etsy sends a JSON POST to that URL.

The payloads are deliberately small. Each one names the event, the shop, and a resource_url that your app calls with its OAuth token to get the order, listing or product.

Every delivery is signed. Three headers, webhook-id, webhook-timestamp and webhook-signature, carry a delivery ID, the time and a base64 HMAC-SHA256 of the ID, timestamp and body. The construction matches the Standard Webhooks specification, although Etsy does not mention it by name.

Etsy webhook features

FeatureDetails
AvailabilityCommercial and personal apps
ConfigurationWebhook portal only. No subscription API is documented
Subscription modelCallback URL plus the events you choose, per endpoint
Headerswebhook-id, webhook-timestamp, webhook-signature
SchemeHMAC-SHA256, base64
Signed content{webhook-id}.{webhook-timestamp}.{raw body}
SecretOne per endpoint, whsec_ followed by base64, shown masked in the portal
Timestamp checkEtsy suggests rejecting timestamps more than 300 seconds out, in your receiver
RetriesImmediately, then 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h
Manual recoveryRetry a message, or "Recover" all failed messages since a date
TestingTesting tab per endpoint in the portal
Response windowNot documented
Source IPsNot published

Common events

Etsy documents 11 events across three domains. The event_type field names the one that fired.

DomainEventFires when
Orderorder.paidAn order receives payment
Orderorder.canceledA seller cancels an order
Orderorder.shippedShipping information is created for a product on a receipt
Orderorder.deliveredAn order is marked as delivered
Listinglisting.createdA listing is created
Listinglisting.updatedA listing's title, description, personalisation, taxonomy, images, attributes or state changes
Listinglisting.deletedA listing is deleted
Inventoryinventory.createdA product is created
Inventoryinventory.updatedA product's price, readiness state, SKU or state changes
Inventoryinventory.deletedA product is deleted
Inventoryinventory.out_of_stockA product's quantity reaches 0

In Etsy's terms, a listing is a container (a t-shirt) and a product is a purchasable variant of it (a small blue t-shirt). Etsy says more events are coming and announces them in the etsy/open-api GitHub discussions.

Every payload has event_type, resource_url and shop_id. Listing events add listing_id, and inventory events add listing_id and product_id. This is Etsy's order.paid example, with its placeholders:

{
  "event_type": "order.paid",
  "resource_url": "https://api.etsy.com/v3/application/shops/{YOUR_SHOP_ID}/receipts/{RECEIPT_ID}",
  "shop_id": "{YOUR_SHOP_ID}"
}

resource_url points at the matching Open API endpoint: getShopReceipt for orders, getListing with ?includes=Images,Personalization for listings, and getListingProduct for inventory. Etsy's examples are all placeholders, so they do not show whether the IDs arrive as JSON strings or numbers. Accept either.

There is no event ID or timestamp in the body; both are in the headers.

Setting up Etsy webhooks

  1. In the Etsy Developer Portal, open Manage your apps.
  2. Open the dropdown for your app and choose Go to Webhook portal.
  3. Click + Add Endpoint, enter your callback URL, choose the events, and click Create.
  4. Open the endpoint's details and reveal its signing secret. Store it, including the whsec_ prefix, for example as ETSY_WEBHOOK_SECRET.
  5. Use the endpoint's Testing tab to send test events.

Your app also needs an OAuth 2.0 access token with the right scopes to call each resource_url.

From the portal you can disable an endpoint (it stops receiving events and can be re-enabled) or delete it (permanent, including its data). The endpoint's detail view shows delivery stats, every message attempt and recent payloads.

Securing Etsy webhooks

Etsy's steps are:

  1. Build signed_content as webhook-id + "." + webhook-timestamp + "." + raw_body.
  2. Remove the whsec_ prefix from the secret and base64-decode the rest. Those bytes are the HMAC key.
  3. Compute an HMAC-SHA256 of signed_content and base64-encode it.
  4. Accept the request if any entry in webhook-signature matches.

Etsy does not show what a webhook-signature value looks like. In the Standard Webhooks format that this construction follows, the header is a space-separated list of entries such as v1,<base64 signature>, so the code below strips a v1, prefix before comparing.

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

function verifyEtsySignature(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatureHeader = headers['webhook-signature'];
  if (!id || !timestamp || !signatureHeader || !secret || !secret.startsWith('whsec_')) {
    return false;
  }

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest('base64');
  const computed = Buffer.from(expected, 'utf8');

  // Optional: Etsy suggests rejecting timestamps more than 300 seconds from now.
  // if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  return signatureHeader.split(' ').some((entry) => {
    const signature = entry.includes(',') ? entry.slice(entry.indexOf(',') + 1) : entry;
    const received = Buffer.from(signature, 'utf8');
    return received.length === computed.length && crypto.timingSafeEqual(received, computed);
  });
}

const app = express();

app.post('/webhooks/etsy', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyEtsySignature(req.body, req.headers, process.env.ETSY_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on the webhook-id header, then fetch event.resource_url asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac
import os


def verify_etsy_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    if not (msg_id and timestamp and signature_header and secret and secret.startswith("whsec_")):
        return False
    key = base64.b64decode(secret[len("whsec_"):])
    signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
    for entry in signature_header.split(" "):
        signature = entry.split(",", 1)[1] if "," in entry else entry
        if hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False


# In FastAPI: raw_body = await request.body()
# verify_etsy_signature(raw_body, request.headers, os.environ["ETSY_WEBHOOK_SECRET"])

Because the scheme is Standard Webhooks, the official standardwebhooks libraries should also work with Etsy's headers and whsec_ secret.

Etsy publishes no test vector. To check your code, use the secret whsec_ZXRzeS10ZXN0LXNpZ25pbmcta2V5LTMyLWJ5dGVzISE=, the webhook-id msg_2Lh9KQvnxBqXv5L7e6hYb3a1, the webhook-timestamp 1760000000, and this exact body:

{"event_type":"order.paid","resource_url":"https://api.etsy.com/v3/application/shops/12345678/receipts/987654321","shop_id":12345678}

The expected webhook-signature entry is:

v1,1wcMDLEGptAxntOl/aKjEoAONWJP1W9IWPeAb3Tucec=

Leave any timestamp check off while you run this test.

In Hookdeck, paste the signing secret into the source exactly as the portal shows it, with the whsec_ prefix. Hookdeck verifies it the Standard Webhooks way, and a secret pasted without the prefix will not match. Hookdeck does not apply a timestamp window, so retried and replayed events keep passing verification; Etsy's 300-second check is something you can add in your own receiver.

Make Etsy webhooks production-ready. Hookdeck Event Gateway verifies webhook-signature, can deduplicate on webhook-id, and queues every order and inventory event until your service has fetched what it needs from Etsy.

Etsy webhook limitations and pain points

Thin payloads mean an API call per event

The Problem: Every event needs a follow-up request before you can act on it.

Why It Happens: Payloads carry only IDs and a resource_url. The order, listing or product has to be fetched with your app's OAuth token, which counts against your API limits and fails if the token has expired.

Workarounds:

  • Acknowledge the webhook first, then fetch resource_url in a background job.
  • Refresh OAuth tokens before they expire, and retry the fetch on 401 after refreshing.
  • Batch or rate-limit fetches during busy periods.

How Hookdeck Can Help: Hookdeck acknowledges Etsy at once and controls the rate at which events reach your service, so a burst of orders becomes a steady flow your fetch workers can handle.

Duplicates and replays

The Problem: The same event can arrive more than once.

Why It Happens: Etsy retries failed deliveries, and you can retry or "Recover" messages by hand from the portal. webhook-id stays the same across retries of one message.

Workarounds:

  • Store each processed webhook-id and skip repeats.
  • Make the handler safe to run twice, since resource_url always returns current state.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries on the webhook-id header before they reach your service.

Signature format details are left to you

The Problem: A verifier written strictly from Etsy's page can reject every delivery.

Why It Happens: Etsy describes the computation but not the header's contents. It does not mention the v1, prefix or the delimiter between entries, and it never names Standard Webhooks, so a literal comparison of the whole entry with the computed signature fails.

Workarounds:

  • Strip the version prefix and split on spaces, as in the code above, or use a Standard Webhooks library.
  • Test against the portal's Testing tab before going live.

How Hookdeck Can Help: Hookdeck's Etsy source handles the Standard Webhooks header format, so your service only has to trust Hookdeck.

Retries end after about a day and a half

The Problem: A long outage loses events.

Why It Happens: Etsy makes eight attempts, with the last one roughly 27 and a half hours after the first if all fail. Disabling an endpoint also stops pending attempts. The success criterion and timeout are not documented.

Workarounds:

  • Use the portal's "Recover" to resend all failed messages since a date once you are back.
  • Reconcile orders from the Open API after an outage.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as Etsy sends it and stores it, then retries against your service on a schedule you control, and you can replay any event from its history.

No API for subscriptions

The Problem: You cannot script endpoint setup across environments or shops.

Why It Happens: Etsy documents only the Webhook portal for creating, disabling and deleting endpoints.

Workarounds:

  • Keep a written record of each endpoint's URL, events and secret.
  • Use separate endpoints for staging and production.

How Hookdeck Can Help: Point one Etsy endpoint at Hookdeck and change routing, filtering and destinations in Hookdeck instead of in the portal.

Best practices

Verify against the raw body

The signature covers "the exact bytes/string of the HTTP body before any JSON parsing". Read it with express.raw(), Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI.

Keep the whole secret

Store the secret with its whsec_ prefix and decode it in code. Stripping the prefix when copying it is a common cause of failed verification.

Treat the webhook as a signal

Use the payload to decide what to fetch, and act on what resource_url returns. That way duplicates and out-of-order deliveries cannot leave you with stale data.

Accept string or number IDs

Etsy does not document the JSON types of shop_id, listing_id and product_id. Convert them to strings before using them as keys.

Acknowledge fast and process asynchronously

Verify, record the webhook-id, return 200, and fetch from the API afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Etsy signs webhook-id, webhook-timestamp and the raw body with a whsec_ secret, the Standard Webhooks way, without saying so. Once verification passes, most of the work is in the follow-up: calling resource_url with a valid OAuth token, deduplicating on webhook-id, and recovering anything that failed while you were down.

Hookdeck Event Gateway sits in front of your receiver, checks every Etsy signature, can drop duplicate deliveries, and holds each event until your service has processed it, so an outage does not leave orders unsynced.

Get started with Hookdeck for free and handle Etsy webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.