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

# Build Custom Integrations Using the Stairoids REST API

> Use the Stairoids REST API to push signals from any tool and receive outgoing action triggers via webhooks — no native integration required.

Not every tool in your stack will have a native Stairoids integration — and that's fine. The Stairoids REST API gives you a direct path to push signals in from any source and receive action triggers out via webhooks. Whether you're connecting an internal data warehouse, a niche sales tool, or wiring things together with a no-code platform like n8n, Make, or Zapier, the Custom API integration is how you do it.

## Two integration patterns

<Tabs>
  <Tab title="Push signals in">
    Use the `POST /v1/signals` endpoint to send a signal into Stairoids from any tool, script, or automation platform. This is the right pattern when your tool generates an event you want Stairoids to act on — for example, a product usage event, a form submission from a tool not natively integrated, or a custom intent signal from your data warehouse.

    **When to use this pattern:**

    * Your tool supports outgoing webhooks or HTTP requests
    * You're using n8n, Make, or Zapier to relay events from a third-party tool
    * You're writing a custom script or backend service that generates events

    Authentication uses a Bearer token. Generate your API key under **Settings → API Keys → Create API Key**.
  </Tab>

  <Tab title="Receive action triggers out">
    Configure an outgoing automation in Stairoids with action type **Webhook**. When the automation fires, Stairoids sends a `POST` request to your endpoint with the full signal and contact context. This is the right pattern when you want a non-native tool to respond to something that happened inside Stairoids.

    **When to use this pattern:**

    * You want to trigger an action in a tool Stairoids doesn't natively support
    * You're building a custom integration that needs to receive structured event data
    * You want to log Stairoids automation events into your own data store

    Stairoids signs every outgoing webhook with an HMAC-SHA256 signature so you can verify the request is genuine.
  </Tab>
</Tabs>

***

## Example 1 — Push a signal in

### Request

Send a `POST` request to `https://api.stairoids.com/v1/signals` with a JSON body describing the signal event.

```bash theme={null}
curl -X POST https://api.stairoids.com/v1/signals \
  -H "Authorization: Bearer sk_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "product_trial_started",
    "source": "internal_app",
    "account": {
      "domain": "acmecorp.com",
      "name": "Acme Corp"
    },
    "contact": {
      "email": "jane.doe@acmecorp.com",
      "first_name": "Jane",
      "last_name": "Doe"
    },
    "properties": {
      "plan": "pro_trial",
      "trial_ends_at": "2024-09-15T00:00:00Z",
      "invited_by": "sales_outreach"
    }
  }'
```

### Request body fields

| Field                | Type   | Required    | Description                                                                                                      |
| -------------------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------- |
| `type`               | string | ✅           | The signal event type. Use any descriptive string for custom events.                                             |
| `source`             | string | ✅           | An identifier for the system sending the signal (e.g., `"internal_app"`, `"zapier"`, `"n8n"`).                   |
| `account.domain`     | string | Recommended | The company domain. Used for account matching in Stairoids.                                                      |
| `account.name`       | string | —           | The company name. Used if no existing account matches the domain.                                                |
| `contact.email`      | string | Recommended | The contact's email address. Used for contact matching.                                                          |
| `contact.first_name` | string | —           | Contact's first name.                                                                                            |
| `contact.last_name`  | string | —           | Contact's last name.                                                                                             |
| `properties`         | object | —           | Any additional key-value pairs relevant to the signal. Accessible in automation conditions and action templates. |

### Response

A successful signal submission returns `201 Created`:

```json theme={null}
{
  "id": "sig_01j4k8m2n3p5q6r7s8t9u0v1",
  "type": "product_trial_started",
  "source": "internal_app",
  "status": "received",
  "account_id": "acc_01j4k8m2n3p5q6r7",
  "contact_id": "con_01j4k8m2n3p5q6r8",
  "created_at": "2024-09-01T14:32:00Z"
}
```

<Note>
  You can use any string value for the `type` field when sending custom signals — there is no required list. Use descriptive names that reflect the event in your system, such as `"product_trial_started"`, `"demo_requested"`, or `"invoice_overdue"`. These type strings become available as trigger conditions in Stairoids automations immediately after the first signal of that type is received.
</Note>

***

## Example 2 — Receive a webhook action trigger

When a Stairoids automation fires an outgoing action of type **Webhook**, Stairoids sends a `POST` request to the URL you configure. Here's the payload structure:

```json theme={null}
{
  "event": "automation.action_triggered",
  "automation_id": "auto_01j4k8m2n3p5q6r7s8",
  "automation_name": "High-intent trial → notify sales",
  "triggered_at": "2024-09-01T14:32:05Z",
  "account": {
    "id": "acc_01j4k8m2n3p5q6r7",
    "name": "Acme Corp",
    "domain": "acmecorp.com"
  },
  "contact": {
    "id": "con_01j4k8m2n3p5q6r8",
    "email": "jane.doe@acmecorp.com",
    "first_name": "Jane",
    "last_name": "Doe"
  },
  "signal": {
    "id": "sig_01j4k8m2n3p5q6r7s8t9u0v1",
    "type": "product_trial_started",
    "source": "internal_app",
    "properties": {
      "plan": "pro_trial",
      "trial_ends_at": "2024-09-15T00:00:00Z"
    },
    "created_at": "2024-09-01T14:32:00Z"
  }
}
```

### Verifying the HMAC-SHA256 signature

Every outgoing webhook includes an `X-Stairoids-Signature` header. Verify it to confirm the request genuinely came from Stairoids and hasn't been tampered with.

The signature is computed as:

```text theme={null}
HMAC-SHA256(webhook_secret, raw_request_body)
```

Your webhook secret is shown once when you configure the Webhook action in Stairoids. Store it securely. Here's how to verify it:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const crypto = require('crypto');

    function verifyStairoidsSignature(rawBody, receivedSignature, secret) {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(rawBody, 'utf8')
        .digest('hex');

      const trusted = Buffer.from(`sha256=${expectedSignature}`, 'ascii');
      const received = Buffer.from(receivedSignature, 'ascii');

      // Use timingSafeEqual to prevent timing attacks
      return crypto.timingSafeEqual(trusted, received);
    }

    // In your Express route handler:
    app.post('/webhook/stairoids', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-stairoids-signature'];
      const isValid = verifyStairoidsSignature(req.body, signature, process.env.STAIROIDS_WEBHOOK_SECRET);

      if (!isValid) {
        return res.status(401).send('Invalid signature');
      }

      const payload = JSON.parse(req.body);
      // Handle payload...
      res.status(200).send('OK');
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib
    import os
    from flask import Flask, request, abort

    app = Flask(__name__)
    WEBHOOK_SECRET = os.environ.get("STAIROIDS_WEBHOOK_SECRET")

    def verify_signature(raw_body: bytes, received_signature: str) -> bool:
        expected = hmac.new(
            WEBHOOK_SECRET.encode("utf-8"),
            raw_body,
            hashlib.sha256
        ).hexdigest()
        expected_header = f"sha256={expected}"
        return hmac.compare_digest(expected_header, received_signature)

    @app.route("/webhook/stairoids", methods=["POST"])
    def stairoids_webhook():
        signature = request.headers.get("X-Stairoids-Signature", "")
        raw_body = request.get_data()

        if not verify_signature(raw_body, signature):
            abort(401)

        payload = request.get_json(force=True)
        # Handle payload...
        return "OK", 200
    ```
  </Tab>
</Tabs>

***

## Send a signal from code

<CodeGroup>
  ```javascript Node.js theme={null}
  const axios = require('axios');

  async function sendStairoidsSignal(signalData) {
    const response = await axios.post(
      'https://api.stairoids.com/v1/signals',
      signalData,
      {
        headers: {
          'Authorization': `Bearer ${process.env.STAIROIDS_API_KEY}`,
          'Content-Type': 'application/json',
        },
      }
    );
    return response.data;
  }

  // Usage
  sendStairoidsSignal({
    type: 'demo_requested',
    source: 'website',
    account: { domain: 'acmecorp.com', name: 'Acme Corp' },
    contact: { email: 'jane.doe@acmecorp.com', first_name: 'Jane', last_name: 'Doe' },
    properties: { demo_type: 'product_tour', requested_at: new Date().toISOString() },
  }).then(signal => console.log('Signal created:', signal.id));
  ```

  ```python Python theme={null}
  import os
  import requests

  def send_stairoids_signal(signal_data: dict) -> dict:
      response = requests.post(
          "https://api.stairoids.com/v1/signals",
          json=signal_data,
          headers={
              "Authorization": f"Bearer {os.environ['STAIROIDS_API_KEY']}",
              "Content-Type": "application/json",
          },
      )
      response.raise_for_status()
      return response.json()

  # Usage
  signal = send_stairoids_signal({
      "type": "demo_requested",
      "source": "website",
      "account": {"domain": "acmecorp.com", "name": "Acme Corp"},
      "contact": {"email": "jane.doe@acmecorp.com", "first_name": "Jane", "last_name": "Doe"},
      "properties": {"demo_type": "product_tour"},
  })
  print(f"Signal created: {signal['id']}")
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stairoids.com/v1/signals \
    -H "Authorization: Bearer $STAIROIDS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "type": "demo_requested",
      "source": "website",
      "account": {
        "domain": "acmecorp.com",
        "name": "Acme Corp"
      },
      "contact": {
        "email": "jane.doe@acmecorp.com",
        "first_name": "Jane",
        "last_name": "Doe"
      },
      "properties": {
        "demo_type": "product_tour"
      }
    }'
  ```
</CodeGroup>

<Tip>
  If you'd rather not write code, use **n8n** or **Make** as a bridge between Stairoids and unsupported tools. Both platforms can send HTTP requests to `POST /v1/signals` and receive Stairoids outgoing webhooks using their built-in Webhook nodes — no custom code required. This lets you connect tools like Salesforce, Slack, Notion, Google Sheets, or virtually anything else with a few clicks.
</Tip>
