Docs › Security & account › REST API

REST API reference

The-Y CRM's public REST API is meant for server-to-server integrations: create/read/update contacts and receive events via webhook. Requests are authenticated with an API key.

Base URL (hosted): https://crm.the-y.at/api/ext/v1 · self-host: https://<your-domain>/api/ext/v1 · Paid feature

Authentication

Every request needs your API key in the header — either as a Bearer token or as X-API-Key:

Authorization: Bearer <YOUR_API_KEY>
# or
X-API-Key: <YOUR_API_KEY>

Create a key: in the app under Settings → Integrations (under “Advanced” → Connections) → "API keys" → new key. The key is shown only once — store it safely (The-Y only stores a hash). The key is bound to your tenant.

Permissions, expiry and renewal (since 1.1.60): each key has its own permissions: contacts:read (read contacts, GET) and/or contacts:write (create and update contacts, POST/PATCH). Optionally it gets an expiry date (at most 5 years). “Renew” creates a new key; the old one keeps working for the time you choose (immediately, 24 hours, 7 or 30 days). Keys from before 1.1.60 keep full permissions without expiry until you choose “Set permissions”.

Treat the key like a password: use it server-side only, never in the browser/frontend. Revoke compromised keys in Integrations.

Conventions

StatusMeaning
200 / 201OK / created
400validation error (e.g. required field missing)
401key missing, invalid or expired
403feature not enabled (Paid), the key lacks the permission (insufficient_scope) or the account is suspended (tenant_suspended)
404resource not found
429rate limit exceeded

Contacts

GET/contacts

List contacts (paginated). Optional search via ?q=.

# Request
curl "https://crm.the-y.at/api/ext/v1/contacts?limit=20&offset=0" \
  -H "Authorization: Bearer $API_KEY"

# Response
{
  "data": [
    { "id": "c_123", "name": "Maria Muster", "email": "maria@example.com", "phone": "+4366012345678", "company": "ACME" }
  ],
  "hasMore": false
}

POST/contacts

Create a contact. At least a name or an email is required.

curl -X POST "https://crm.the-y.at/api/ext/v1/contacts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Maria Muster", "email": "maria@example.com", "phone": "+4366012345678", "company": "ACME" }'

# 201 Created
{ "data": { "id": "c_123", "name": "Maria Muster", "email": "maria@example.com" } }

GET/contacts/{id}

Read a single contact.

curl "https://crm.the-y.at/api/ext/v1/contacts/c_123" \
  -H "Authorization: Bearer $API_KEY"

PATCH/contacts/{id}

Partially update a contact — only the sent fields change (existing ones are kept).

curl -X PATCH "https://crm.the-y.at/api/ext/v1/contacts/c_123" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+4366099999999" }'
The API is contact-centric and growing. Need more resources (companies, deals, tickets)? Write to us at office@the-y.at — or use the webhooks below for live events.

Outbound webhooks (events)

Instead of polling, have events pushed to your URL. Under Settings → Integrations → "Send data (webhook)" you set a target URL and the events (e.g. contact.created, contact.updated). The CRM creates the signing secret.

Verify the webhook signature (since 1.1.60)

Every delivery is signed, so your system can check that the data really comes from The-Y CRM and wasn't changed on the way.

Secret: it is shown once when you create the webhook (whsec_…). Afterwards it is only stored encrypted and not visible anywhere. Lost it? “Renew secret” – the old one stops working immediately.

HeaderContent
X-TheY-Timestamptime of the delivery in seconds since 1970 (Unix time)
X-TheY-Signaturev1=<hex> – HMAC-SHA256 with the secret over <X-TheY-Timestamp>.<body> (timestamp, a dot, the unchanged body)
X-TheY-Evente.g. contact.created
X-TheY-Deliveryunique ID of the delivery (stays the same on retries)
X-KSCRM-Signature, X-KSCRM-Eventold: HMAC-SHA256 over the body only, without a timestamp. Still sent so existing receivers don't break
Please switch: the old X-KSCRM-Signature header will be removed with version 1.2.0. Until then, verify X-TheY-Signature as shown below.

How to verify: compute the signature yourself and compare in constant time, and reject deliveries whose timestamp is more than 5 minutes off your clock (protects against replaying intercepted deliveries). Important: sign the raw body as received – don't parse it as JSON and serialise it again first.

# Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.THEY_WEBHOOK_SECRET; // whsec_…
const app = express();

app.post("/the-y/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ts = Number(req.get("X-TheY-Timestamp"));
  const sig = (req.get("X-TheY-Signature") || "").split(",").map((s) => s.trim()).find((s) => s.startsWith("v1="))?.slice(3) || "";
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return res.status(400).send("too old");
  const want = crypto.createHmac("sha256", SECRET).update(`${ts}.${req.body.toString("utf8")}`).digest("hex");
  const a = Buffer.from(sig, "utf8"), b = Buffer.from(want, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send("bad signature");
  const event = JSON.parse(req.body.toString("utf8"));
  // … process event.event, event.data
  res.sendStatus(204);
});
# PHP
<?php
$secret = getenv('THEY_WEBHOOK_SECRET'); // whsec_…
$body   = file_get_contents('php://input');
$ts     = (int)($_SERVER['HTTP_X_THEY_TIMESTAMP'] ?? 0);
$sigHdr = $_SERVER['HTTP_X_THEY_SIGNATURE'] ?? '';
$sig    = '';
foreach (explode(',', $sigHdr) as $part) { $part = trim($part); if (str_starts_with($part, 'v1=')) $sig = substr($part, 3); }
if (abs(time() - $ts) > 300) { http_response_code(400); exit('too old'); }
$want = hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($want, $sig)) { http_response_code(401); exit('bad signature'); }
$event = json_decode($body, true);
// … process $event['event'], $event['data']
http_response_code(204);

Inbound webhooks

External systems can create/update data in The-Y via a token URL: POST /api/hooks/<token> (token created in Integrations). Per webhook you choose the access level: upsert (create/update only, deletions ignored — anti-lock-in) or full (incl. delete).

What's next