Developers › Quickstart

Quickstart

Three calls: check that your key works, count for free, then fetch rows. Nothing here costs more than a few cents.

Doing this with a sandbox key(syyni_test_…)? Everything below works identically — the register is smaller and nothing is charged. See Sandbox.

1. Check the key

/usage is free and does not consume any quota. It is also the endpoint to call when something stops working — it tells you your limits and what you have spent this month.

curl
export SYYNI_API_KEY="syyni_live_..."

curl -H "Authorization: Bearer $SYYNI_API_KEY" \
  https://syynilabs.fi/api/v1/usage
json
{
  "key": { "prefix": "1fq2Jl8H", "customer": "Your Company Oy" },
  "current_month": {
    "spend": "12.40", "currency": "EUR", "vat": "excluded",
    "requests": 143, "rows": 2180
  },
  "today": { "requests": 12 },
  "limits": {
    "requests_per_minute": 60, "requests_per_day": 5000,
    "rows_per_month": 50000, "spend_cap": "500.00",
    "customer_requests_per_month": null
  },
  "customer_current_month": { "requests": 143 }
}

customer_requests_per_month is an account-level cap shared by all your keys (null = none), and customer_current_month.requests counts against it.

2. Count before you buy

Counting is free, so shape the filter here rather than by fetching rows and discarding them. The response gives both levels, because the same filter means a different number of each: a company row is a company, a decision-maker row is a person.

curl
curl -H "Authorization: Bearer $SYYNI_API_KEY" \
  "https://syynilabs.fi/api/v1/companies/count\
?region=01&industry_division=41&min_age_years=3&has_decision_maker=true"

Unknown parameters are rejected rather than ignored. A typo like municipalty=091 would otherwise silently widen your query to the whole register — and bill you for it.

3. Fetch rows

Ask for the field groups you actually need. The base row is always included and always billed; phone, email and decision_makers cost extra, and only for rows where they contain something.

curl
curl -H "Authorization: Bearer $SYYNI_API_KEY" \
  "https://syynilabs.fi/api/v1/companies\
?region=01&industry_division=41&fields=phone&page_size=100"
json
{
  "data": [
    {
      "business_id": "0100130-4",
      "name": "Example Oy",
      "industry": { "code": "41200", "description": "Construction of buildings" },
      "address": { "street": "Esimerkkitie 1", "postal_code": "00100",
                   "city": "Helsinki", "municipality_code": "091" },
      "availability": { "has_phone": true, "phone_count": 3,
                        "has_email": true, "has_decision_maker": true },
      "phone_numbers": ["+358401234567", "+358401234568"],
      "phone_updated": "2026-08-16"
    }
  ],
  "pagination": { "page_size": 100, "has_more": true, "cursor": "eyJrIjpb..." },
  "billing": {
    "rows_returned": 100, "rows_billed": 100,
    "cost": "5.92", "currency": "EUR", "vat": "excluded",
    "field_groups": {
      "core":  { "billed": 100, "empty": 0,  "free_repeat": 0 },
      "phone": { "billed": 61,  "empty": 26, "free_repeat": 13 }
    }
  }
}

Read the billing block: 26 rows had no phone number and were not charged for it, and 13 had been fetched before within the last 30 days and were free.

4. Page through the results

Pagination is cursor-based. Pass the cursor back verbatim and stop when has_more is false. Do not construct cursors yourself — they are tied to the filters and fields you used, and a mismatched cursor returns invalid_cursor rather than silently wrong rows.

python
import os, requests

KEY = os.environ["SYYNI_API_KEY"]
BASE = "https://syynilabs.fi/api/v1"
params = {"region": "01", "industry_division": "41",
          "fields": "phone", "page_size": 500}

rows, spent = [], 0.0
while True:
    r = requests.get(f"{BASE}/companies", params=params,
                     headers={"Authorization": f"Bearer {KEY}"},
                     timeout=60)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    spent += float(body["billing"]["cost"])
    if not body["pagination"]["has_more"]:
        break
    params["cursor"] = body["pagination"]["cursor"]

print(f"{len(rows)} rows, {spent:.2f} EUR")
javascript
const KEY = process.env.SYYNI_API_KEY;
const BASE = "https://syynilabs.fi/api/v1";

let cursor = null, rows = [], spent = 0;
do {
  const url = new URL(`${BASE}/companies`);
  url.searchParams.set("region", "01");
  url.searchParams.set("fields", "phone");
  url.searchParams.set("page_size", "500");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  if (!res.ok) throw new Error((await res.json()).error.code);

  const body = await res.json();
  rows.push(...body.data);
  spent += Number(body.billing.cost);
  cursor = body.pagination.has_more ? body.pagination.cursor : null;
} while (cursor);

console.log(rows.length, "rows,", spent.toFixed(2), "EUR");

Handle 429 properly

Two different limits return 429: rate_limited is the per-minute burst limit and clears in seconds; quota_exceeded is your daily or monthly quota and clears at midnight or month end. Both send Retry-After in seconds, and the same number appears in the body as retry_after_seconds — sleep that long rather than guessing. Nothing is billed for a rejected request.

Full endpoint reference →

Aloita kohderyhmän rajaus