# Webhooks

Register and manage your outbound webhook with the SDK, and parse incoming deliveries into typed, discriminated events.

Manage your outbound webhook and parse incoming deliveries into typed events. See the [Admin overview](/sdks/admin-overview) for how to create the `admin` client, and the [Webhooks API](/docs/webhooks) for the underlying event model.

Webhook management lives on the admin client (`admin.webhooks`); event parsing is a standalone helper exported from the package root.

## Register a webhook

A client may hold **one active webhook**. It receives every event Linked API emits.

```typescript
const webhook = await admin.webhooks.set({
  url: 'https://example.com/hooks/linkedapi',
  payloadMode: 'fat', // 'fat' inlines the workflow result; 'thin' sends a reference only
});

console.log(`Webhook ${webhook.id} → ${webhook.url}`);
```

```python
from linkedapi import SetWebhookParams

webhook = admin.webhooks.set(
    SetWebhookParams(
        url="https://example.com/hooks/linkedapi",
        payload_mode="fat",  # 'fat' inlines the workflow result; 'thin' sends a reference only
    )
)

print(f"Webhook {webhook.id} → {webhook.url}")
```

### Params

- `url` – HTTPS endpoint that will receive deliveries.
- `payloadMode` / `payload_mode` – optional, `"fat"` (default) or `"thin"`.

### Data

- `id` – webhook subscription id.
- `url` – the registered destination.
- `payloadMode` / `payload_mode` – current payload mode.
- `isActive` / `is_active` – whether the webhook is active.
- `createdAt` / `created_at` – ISO 8601 timestamp.

There is no update method – to change the destination, `delete` the webhook and `set` a new one.

## Get the active webhook

```typescript
const webhooks = await admin.webhooks.get();
console.log(webhooks[0]?.url ?? 'no active webhook');
```

```python
webhooks = admin.webhooks.get()
print(webhooks[0].url if webhooks else "no active webhook")
```

Returns an array with at most one entry.

## Change payload mode

```typescript
await admin.webhooks.setPayloadMode({ id: 'whs-...', payloadMode: 'thin' });
```

```python
from linkedapi import SetWebhookPayloadModeParams

admin.webhooks.set_payload_mode(
    SetWebhookPayloadModeParams(id="whs-...", payload_mode="thin")
)
```

## Delete the webhook

Soft delete: the delivery history is preserved and pending deliveries are dropped. You can register a fresh webhook afterwards.

```typescript
await admin.webhooks.delete({ id: 'whs-...' });
```

```python
from linkedapi import DeleteWebhookParams

admin.webhooks.delete(DeleteWebhookParams(id="whs-..."))
```

## Inspect deliveries

Return the most recent deliveries (newest first) as a debug feed.

```typescript
const deliveries = await admin.webhooks.deliveries();

for (const delivery of deliveries) {
  console.log(`${delivery.eventType} → ${delivery.status} (attempts: ${delivery.attempts})`);
  if (delivery.lastError) {
    console.log(`  last error: ${delivery.lastError} (HTTP ${delivery.responseStatusCode})`);
  }
}
```

```python
deliveries = admin.webhooks.deliveries()

for delivery in deliveries:
    print(f"{delivery.event_type} → {delivery.status} (attempts: {delivery.attempts})")
    if delivery.last_error:
        print(f"  last error: {delivery.last_error} (HTTP {delivery.response_status_code})")
```

### Data

Each delivery has the following shape:

```typescript
interface TWebhookDelivery {
  id: string; // delivery identifier, passed to replayDelivery
  eventType: TWebhookEventType; // the event type that was delivered
  eventId: string; // the envelope id of the delivered event
  status: 'pending' | 'delivering' | 'success' | 'failed';
  attempts: number; // delivery attempts made so far
  responseStatusCode: number | null; // HTTP status your endpoint returned on the last attempt
  lastError: string | null; // error text from the last failed attempt
  createdAt: string; // ISO 8601
  updatedAt: string; // ISO 8601
}
```

```python
class WebhookDelivery:
    id: str  # delivery identifier, passed to replay_delivery
    event_type: WebhookEventType  # the event type that was delivered
    event_id: str  # the envelope id of the delivered event
    status: str  # "pending" | "delivering" | "success" | "failed"
    attempts: int  # delivery attempts made so far
    response_status_code: int | None  # HTTP status your endpoint returned on the last attempt
    last_error: str | None  # error text from the last failed attempt
    created_at: str  # ISO 8601
    updated_at: str  # ISO 8601
```

## Replay a delivery

Re-arm an already-settled delivery. The same event id is reused, so your idempotency handling still applies.

```typescript
await admin.webhooks.replayDelivery({ deliveryId: 'whd-...' });
```

```python
from linkedapi import ReplayWebhookDeliveryParams

admin.webhooks.replay_delivery(ReplayWebhookDeliveryParams(delivery_id="whd-..."))
```

## Send a test event

Emit a synthetic `webhook.test` event to verify a freshly registered endpoint end-to-end.

```typescript
await admin.webhooks.sendTest();
```

```python
admin.webhooks.send_test()
```

## Receiving and parsing events

Use `parseWebhookEvent` / `parse_webhook_event` to turn a raw request body into a typed, discriminated event. Pass the **raw** body exactly as received, then branch on `type` to narrow `data`.

```typescript
import express from 'express';
import { parseWebhookEvent } from '@linkedapi/node';

const app = express();
app.use(express.raw({ type: 'application/json' }));

app.post('/hooks/linkedapi', (req, res) => {
  const event = parseWebhookEvent(req.body as Buffer);

  switch (event.type) {
    case 'workflow.completed':
      console.log(`Workflow ${event.data.workflowId} finished: ${event.data.status}`);
      console.log('Result:', event.data.result); // present in 'fat' mode only
      break;
    case 'account.reconnectionRequired':
      console.log(`Account ${event.data.accountId} needs reconnection.`);
      break;
    case 'inbox.messageReceived':
      console.log(`New message from ${event.data.personUrl}: ${event.data.text}`);
      break;
    case 'inbox.messageSent':
      console.log(`Outgoing message in thread ${event.data.threadId}`);
      break;
    case 'network.connectionAccepted':
      console.log(`${event.data.personUrl} accepted your connection request`);
      break;
    case 'network.connectionRequestReceived':
      console.log(`New connection request from ${event.data.personUrl}`);
      break;
    case 'webhook.test':
      console.log('Test event:', event.data.message);
      break;
  }

  // Acknowledge fast. A non-2xx response makes Linked API retry with backoff.
  res.sendStatus(200);
});
```

```python
from fastapi import FastAPI, Request, Response
from linkedapi import parse_webhook_event

app = FastAPI()


@app.post("/hooks/linkedapi")
async def receive(request: Request) -> Response:
    event = parse_webhook_event(await request.body())

    if event.type == "workflow.completed":
        print(f"Workflow {event.data.workflow_id} finished: {event.data.status}")
        print("Result:", event.data.result)  # present in 'fat' mode only
    elif event.type == "account.reconnectionRequired":
        print(f"Account {event.data.account_id} needs reconnection.")
    elif event.type == "inbox.messageReceived":
        print(f"New message from {event.data.person_url}: {event.data.text}")
    elif event.type == "inbox.messageSent":
        print(f"Outgoing message in thread {event.data.thread_id}")
    elif event.type == "network.connectionAccepted":
        print(f"{event.data.person_url} accepted your connection request")
    elif event.type == "network.connectionRequestReceived":
        print(f"New connection request from {event.data.person_url}")
    elif event.type == "webhook.test":
        print("Test event:", event.data.message)

    # Acknowledge fast. A non-2xx response makes Linked API retry with backoff.
    return Response(status_code=200)
```

`parseWebhookEvent` / `parse_webhook_event` throws when the body is not valid JSON or is missing the `id` / `type` envelope fields. Deduplicate on `event.id` – deliveries are at-least-once, so the same event can arrive more than once.

The returned union (`TWebhookEvent` in Node, `WebhookEvent` in Python) covers workflow events, account events, [inbox message events](/docs/webhooks#inbox-events), [network events](/docs/webhooks#network-events), and the test event. See the [Webhooks API](/docs/webhooks) for the full event catalog.

## Errors

All webhook methods throw `LinkedApiError` on failure – see [Admin overview](/sdks/admin-overview#error-handling). For the complete HTTP reference, see the [Webhooks API docs](/docs/webhooks).
