# Webhooks

> Send signed JSON to a URL of your choice when something happens in the CRM (an opportunity is won, a new company is added, a form is submitted…). Zapier, Make, n8n, and Pipedream use the same mechanism.

A webhook (outbound hook) is a way to **push** CRM events to your system: instead of constantly polling the API, you receive an HTTP `POST` when an event happens. You set it up on the **Settings → Apps** (*Ayarlar → Uygulamalar*) page; no code required.

## Setup

:::steps
### Open the connection

In **Settings → Apps** (*Ayarlar → Uygulamalar*), select **Webhook** (*Web kancası*) (or the Zapier, Make, n8n, or Pipedream card) and click **Connect** (*Bağla*).

### Enter the URL

Paste the `https://` URL that events should be sent to. You get this URL from "Webhooks by Zapier → Catch Hook" in Zapier, "Webhooks → Custom webhook" in Make, the "Webhook" node in n8n, or the "HTTP / Webhook" trigger in Pipedream.

### Choose events

Select the events that should trigger a delivery. The connection card shows a **signing key**; store it so the receiving side can verify that requests really come from the CRM.

### Test it

**Test connection** (*Bağlantıyı dene*) sends a test delivery with `event: "test"` to your receiver. The result of the latest delivery (HTTP status code or error) appears on the card.
:::

## Events

| Event | When | `data` fields |
|---|---|---|
| `opp_created` | A new opportunity was created | `id`, `rid`, `name`, `customer`, `customer_id`, `stage`, `stage_label`, `value`, `currency`, `owner` |
| `opp_stage` | An opportunity's stage changed (the title shows the old → new stage) | Opportunity fields |
| `opp_won` | An opportunity was won (`Win`) | Opportunity fields |
| `opp_lost` | An opportunity was lost (`Lost`) | Opportunity fields |
| `customer_created` | New company | `id`, `rid`, `name`, `status`, `source`, `city` |
| `contact_created` | New person (except those created automatically from email) | `id`, `name`, `email`, `phone`, `title`, `customer`, `customer_id` |
| `lead_created` | New fair lead | `id`, `company`, `contact`, `email`, `interest` |
| `visit_created` | Activity / visit logged | `id`, `customer`, `type`, `date`, `next_action`, `note` |
| `ticket_created` | New support ticket | `id`, `subject`, `priority`, `who` |
| `form_submitted` | Website form / Typeform / Tally submission | `form`, `company`, `name`, `email`, `phone`, `customer_id`, `task_id`, `new_customer` |

Events are sent for every change, whether it comes from the web interface, the mobile app, the API, MCP, workflows, or inbound hooks. There are two exceptions:

- **Sample data** records don't send events.
- If a single operation produces more than 25 events (e.g. a bulk import from Excel), no events are sent; a note is added to the audit log instead.

## Payload

```http
POST /sizin/adresiniz HTTP/1.1
Content-Type: application/json
X-Solk-Event: opp_won
X-Solk-Signature: sha256=5d1c0a3e9b…
```

```json
{
  "event": "opp_won",
  "title": "Fırsat kazanıldı",
  "text": "Kuzey Plastik Sanayi · Streç film tedariki · 338.400 € · Sorumlu: Deniz Aksoy",
  "url": "https://ornek.solk.app/opportunities/53",
  "data": {
    "id": 53,
    "rid": "006Xq3LmT0aZb9K",
    "name": "Streç film tedariki",
    "customer": "Kuzey Plastik Sanayi",
    "customer_id": 31,
    "stage": "Win",
    "stage_label": "Win",
    "value": 338400.0,
    "currency": "EUR",
    "owner": "Deniz Aksoy"
  },
  "workspace": "Örnek Kimya",
  "app": "Solk CRM",
  "sent_at": "2026-10-02T14:05:11"
}
```

| Field | Description |
|---|---|
| `event` | Event name (see the table above; `test` for test deliveries). |
| `title`, `text` | Human-readable title and summary — you can post them directly to notification channels. |
| `url` | The record's URL in the CRM. |
| `data` | Event-specific structured fields. If you need the full record, fetch it from the [REST API](/rest-api/overview) using `data.id`. |
| `workspace`, `app` | The installation's company name and app name. |
| `sent_at` | Time the delivery was sent (in the installation's local time). |

## Verifying the signature

`X-Solk-Signature` is the HMAC-SHA256 digest of the request body (as raw bytes), computed with the signing key. Verify it **before** you parse the body as JSON:

:::code
```python Python
import hmac, hashlib

def verify(body_bytes: bytes, signature: str, key: str) -> bool:
    expected = "sha256=" + hmac.new(key.encode(), body_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

# Flask
@app.post("/solk")
def solk():
    if not verify(request.get_data(), request.headers.get("X-Solk-Signature"), SIGNING_KEY):
        abort(401)
    event = request.get_json()
    ...
```
```javascript Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();
app.post("/solk", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", process.env.SOLK_SIGNING_KEY)
    .update(req.body).digest("hex");
  const signature = req.get("X-Solk-Signature") || "";
  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(204);
});
```
```php PHP
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('SOLK_SIGNING_KEY'));
if (!hash_equals($expected, $_SERVER['HTTP_X_SOLK_SIGNATURE'] ?? '')) {
    http_response_code(401); exit;
}
$event = json_decode($body, true);
```
:::

## Delivery

- Deliveries are sent in the background, independently of the request; CRM users don't wait.
- The timeout is **8 seconds**. Your receiver must return a `2xx`; queue long-running work and respond immediately.
- **There are no retries.** A failed delivery shows up as the latest status on the connection card. If you can't afford to miss events, sync periodically from the API (e.g. `GET /api/v1/opportunities?sort=update`).
- Ordering isn't guaranteed; sort events for the same record by `sent_at`.

## Channels

Slack, Microsoft Teams, Google Chat, Discord, and Telegram connections send the same events in the channel's own message format (Slack text, a Teams Adaptive Card…); the signature header is present only for webhook, Zapier, Make, n8n, and Pipedream connections. Notion, Airtable, Google Sheets, Asana, ClickUp, Linear, and Mailchimp connections instead create a record in that app from the event. Details: [Apps](/guides/apps).

## Writing back to the CRM

If your workflow needs to create or update records in the CRM (e.g. "invoice paid → complete task" in Zapier), create an API key in **Settings → Developers** (*Ayarlar → Geliştiriciler*) and call the [REST API](/rest-api/overview) from the HTTP step in Zapier / Make / n8n with the `Authorization: Bearer sk_…` header. The no-code way to push data in from outside is [inbound hooks](/guides/inbound).