> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alura.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks and events

> Receive Etsy's webhook events for your connected shops without an Etsy app of your own, and read the last 30 days of them

Get Etsy's events for your connected shops, such as a new paid order, at your own HTTPS endpoint within seconds. You don't need an Etsy app: Etsy sends webhooks only to app owners, and Alura relays the events it receives for your shops to your workspace.

Each delivery is signed with your endpoint's secret (see [Verifying webhooks](https://docs.alura.io/guides/verifying-webhooks.md)), and the last 30 days of events stay readable with `GET /v1/events`.

## What you receive

* **Etsy's payload, unchanged.** Each event carries what Etsy sent in `data`, byte for byte: today `event_type`, `resource_url` and `shop_id`. Alura adds nothing to it and doesn't keep a list of event types, so a type Etsy adds tomorrow reaches you too.
* **Your shops only.** An event reaches a workspace only when its shop is connected to that workspace.
* **From your first endpoint on.** Alura stores an event for your workspace when one of your enabled endpoints wants it. History starts with your first endpoint, and covers the event types and shops your endpoints subscribe to.
* **30 days.** Events are kept for 30 days.

`resource_url` points at Etsy's own API, which needs an Etsy app. Read the same data from Alura instead: for an `order.*` event, [the order](https://docs.alura.io/api-reference/workspace/orders/get.md) with `GET /v1/workspace/orders/{id}`, taking the receipt ID from the end of `resource_url`.

## Scopes

| Scope | Allows |
| - | - |
| `webhooks:write` | Creating, listing, changing and deleting webhook endpoints, their deliveries, test events and secret rotation |
| `events:read` | `GET /v1/events` and `GET /v1/events/{id}` |

Order events reveal that an order exists, so they also need `orders:read`: on the key that subscribes an endpoint to `*`, `order.*` or any `order.` type, or changes, tests or rotates such an endpoint, and on the key that reads them with `GET /v1/events`. A workspace owner or admin can create a key with these scopes.

## Create an endpoint

An endpoint is an HTTPS URL on a public host (not `localhost` or a private address) with the event types it wants: `*` for every type, an exact type such as `order.paid`, or a prefix such as `order.*`. Add `shop_ids` to limit it to some of your connected shops. The answer carries the endpoint's signing secret (`whsec_...`) once, so store it then. A workspace has at most 10 endpoints. Events start reaching a new endpoint within about 30 seconds of creating it (or enabling it, or connecting a shop).

```bash cURL theme={null}
# The answer carries the endpoint's signing secret once: store it.
curl --fail-with-body -sS https://api.alura.io/v1/webhooks \
  -H "Authorization: Bearer $ALURA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9b2f4c1e-7a3d-4e8b-b6c5-1d0e2f3a4b5c" \
  -d '{"url": "https://example.com/alura/webhooks", "event_types": ["order.*"], "description": "Order notifications"}'
```

```typescript TypeScript theme={null}
interface Endpoint {
  id: string;
  url: string;
  event_types: string[];
  status: string;
  secret: string;
}

const res = await fetch('https://api.alura.io/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ALURA_API_KEY}`,
    'Content-Type': 'application/json',
    // A fresh UUID for each new endpoint. Reuse it only to retry this same request.
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({ url: 'https://example.com/alura/webhooks', event_types: ['order.*'], description: 'Order notifications' }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const endpoint = (await res.json()) as Endpoint;

console.log(`${endpoint.id} (${endpoint.status}): ${endpoint.event_types.join(', ')} to ${endpoint.url}`);
// Shown once: keep it in your secret store, next to the code that verifies deliveries.
console.log(`secret starts ${endpoint.secret.slice(0, 6)}`);
```

```python Python theme={null}
import json
import os
import urllib.request
import uuid

body = {"url": "https://example.com/alura/webhooks", "event_types": ["order.*"], "description": "Order notifications"}
request = urllib.request.Request(
    "https://api.alura.io/v1/webhooks",
    method="POST",
    data=json.dumps(body).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['ALURA_API_KEY']}",
        "Content-Type": "application/json",
        # A fresh UUID for each new endpoint. Reuse it only to retry this same request.
        "Idempotency-Key": str(uuid.uuid4()),
    },
)
with urllib.request.urlopen(request) as response:
    endpoint = json.load(response)

print(f"{endpoint['id']} ({endpoint['status']}): {', '.join(endpoint['event_types'])} to {endpoint['url']}")
# Shown once: keep it in your secret store, next to the code that verifies deliveries.
print(f"secret starts {endpoint['secret'][:6]}")
```

To stop events for a while, set `status` to `disabled` with `PATCH /v1/webhooks/{id}`. Events that arrive while it is disabled aren't stored for it, retries still queued for it are cancelled, and enabling it again doesn't replay them.

## Deliveries

Alura `POST`s each event to every enabled endpoint that wants it, usually within a few seconds of Etsy's notification. Any `2xx` answer counts as delivered. Anything else, no answer within 15 seconds, or a redirect counts as a failure, and Alura retries 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours later: 8 attempts over 27 hours and 35 minutes. Every attempt carries the same `webhook-id` header, the event's ID.

* **Every attempt is listed.** `GET /v1/webhooks/{id}/deliveries` shows the last 30 days of attempts, newest first, with the HTTP status your endpoint answered (never its body), how long it took, an `error` when there was no usable answer, and when the next retry is due.

```bash cURL theme={null}
curl --fail-with-body -sS "https://api.alura.io/v1/webhooks/we_01JMOCK0000000000000000000/deliveries" \
  -H "Authorization: Bearer $ALURA_API_KEY"
```

```typescript TypeScript theme={null}
interface Delivery {
  id: string;
  event_id: string;
  attempt: number;
  status: 'pending' | 'succeeded' | 'failed';
  http_status: number | null;
  error: string | null;
  created_at: string;
  next_attempt_at: string | null;
}

const endpointId = 'we_01JMOCK0000000000000000000';
const res = await fetch(`https://api.alura.io/v1/webhooks/${endpointId}/deliveries`, { headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}` } });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const page = (await res.json()) as { data: Delivery[]; has_more: boolean };

// Newest first: one line per attempt, and the ones still queued.
for (const d of page.data) {
  const answer = d.http_status ?? d.error ?? 'not sent yet';
  console.log(`${d.created_at} ${d.event_id} attempt ${d.attempt}: ${d.status} (${answer})${d.next_attempt_at ? `, next at ${d.next_attempt_at}` : ''}`);
}
```

```python Python theme={null}
import json
import os
import urllib.request

endpoint_id = "we_01JMOCK0000000000000000000"
request = urllib.request.Request(
    f"https://api.alura.io/v1/webhooks/{endpoint_id}/deliveries",
    headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
)
with urllib.request.urlopen(request) as response:
    page = json.load(response)

# Newest first: one line per attempt, and the ones still queued.
for d in page["data"]:
    answer = d["http_status"] or d["error"] or "not sent yet"
    nxt = f", next at {d['next_attempt_at']}" if d["next_attempt_at"] else ""
    print(f"{d['created_at']} {d['event_id']} attempt {d['attempt']}: {d['status']} ({answer}){nxt}")
```

> [accordion] When an endpoint keeps failing
>
> After 5 days without a successful delivery and at least 20 failed attempts, Alura disables the endpoint: `status` becomes `disabled` with `disabled_reason: "failing"`. Fix it, then set `status` back to `enabled` with `PATCH /v1/webhooks/{id}`. Missed events aren't replayed to the endpoint; read them with `GET /v1/events`.

> [accordion] Public addresses only
>
> Alura resolves your endpoint's host before each attempt and connects only when every address it resolves to is public. Private, loopback, link-local and cloud metadata addresses are refused, and the attempt's `error` is `address_refused`.

## Read events

`GET /v1/events` lists your events newest first. Filter by `type` (exact or a prefix such as `order.*`), `shop_id` and `created_after`, and page with `cursor`. After an outage on your side, this is how you catch up.

```bash cURL theme={null}
curl --fail-with-body -sS "https://api.alura.io/v1/events?type=order.*" \
  -H "Authorization: Bearer $ALURA_API_KEY"
```

```typescript TypeScript theme={null}
interface Event {
  id: string;
  type: string;
  created_at: string;
  shop_id: string;
  data: { event_type: string; resource_url: string; shop_id: number };
}

const res = await fetch('https://api.alura.io/v1/events?type=order.*', { headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}` } });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const page = (await res.json()) as { data: Event[]; has_more: boolean };

// Newest first; `data` is Etsy's payload exactly as Etsy sent it.
for (const event of page.data) console.log(`${event.created_at} ${event.type} shop ${event.shop_id}: ${event.data.resource_url}`);
console.log(page.has_more ? 'more: pass next_cursor as cursor' : 'that is all');
```

```python Python theme={null}
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://api.alura.io/v1/events?type=order.*",
    headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
)
with urllib.request.urlopen(request) as response:
    page = json.load(response)

# Newest first; `data` is Etsy's payload exactly as Etsy sent it.
for event in page["data"]:
    print(f"{event['created_at']} {event['type']} shop {event['shop_id']}: {event['data']['resource_url']}")
print("more: pass next_cursor as cursor" if page["has_more"] else "that is all")
```

Each event looks like this:

```json theme={null}
{
  "object": "event",
  "id": "evt_01JMOCK0000000000000000000",
  "type": "order.paid",
  "created_at": "2026-10-22T09:15:02.000Z",
  "shop_id": "48911633",
  "data": {
    "event_type": "order.paid",
    "resource_url": "https://openapi.etsy.com/v3/application/shops/48911633/receipts/3456789012",
    "shop_id": 48911633
  }
}
```

`created_at` is when Alura received the event from Etsy. Etsy retries a delivery it isn't sure arrived, and Alura stores each Etsy message once per workspace, so a retry never shows up twice.

Webhook endpoints, deliveries and events are free and REST only: AI assistants don't get them as MCP tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.