API · v1
API-dokumentation
Samma verktyg som i appen, med samma krediter: domänsök, mejlfinnaren, verifiering, bolagssök, telefonnummer och CRM. Du skickar en nyckel och får JSON tillbaka, och varje svar visar vad anropet kostade.
- Bas-URL
https://spanly.se/v1- Format
- JSON in och ut, UTF-8. Felmeddelanden på svenska.
- Nyckel
- Skapas under Konto i appen. En aktiv nyckel per konto.
Autentisering
Skapa en nyckel under API i appens sidomeny. Nyckeln börjar med spanly_ och visas bara en gång, så spara den direkt. Skapar du en ny nyckel slutar den gamla att fungera på en gång.
Skicka nyckeln i huvudet Authorization:
Authorization: Bearer spanly_…rekommenderas.- Det går också att skicka
api_keysom query-parameter eller som fält i JSON-kroppen, men då hamnar nyckeln lättare i loggar.
Anropa API:t från din server. Svaren har inga CORS-huvuden, så anrop direkt från en webbläsare på en annan domän blockeras. Nyckeln ska heller aldrig ligga i kod som skickas till en webbläsare.
Domänsök, mejlfinnaren och verifiering tar emot både GET med query-parametrar och POST med JSON-kropp. Formulärkodade kroppar fungerar också. Exemplen använder POST och JSON.
Exemplen på sidan använder påhittade bolag på domänen .exempel.
curl https://spanly.se/v1/account \ -H "Authorization: Bearer $SPANLY_API_KEY"
// Node 18 eller senare
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/account', {
headers: { Authorization: `Bearer ${key}` },
});
const account = await res.json();
console.log(account.credits.remaining, 'krediter kvar');
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.get(
"https://spanly.se/v1/account",
headers={"Authorization": f"Bearer {key}"},
timeout=30,
)
print(res.json()["credits"]["remaining"], "krediter kvar")
{
"error": "API-nyckel saknas. Skicka den som \"Authorization: Bearer <nyckel>\" eller api_key=<nyckel>."
}
Debitering
En kredit motsvarar en personadress som levereras och som det finns skäl att lita på. Regeln är densamma i appen och i API:t.
| Adress i svaret | Kostar |
|---|---|
| GiltigPersonadress som mejlservern bekräftar, oavsett källa | 1 kredit |
| RiskabelPersonadress från bolagets webbplats eller offentliga bolagsuppgifter, på en server som tar emot all post | 1 kredit |
| OkändPersonadress som inte gick att kontrollera, oavsett källa | 0 |
| OgiltigBrevlådan eller domänen finns inte | 0 |
| Allmän adress, till exempel info@ eller kontakt@ | 0 |
Verifiering med /v1/email-verifier | 0 |
| Sökning utan resultat | 0 |
| Adress som kontot redan har betalat för | 0 |
Bara bekräftade adresser kostar
Du betalar bara för personadresser som mejlservern bekräftar. Adresser som inte gått att kontrollera får du utan kostnad.
Bra att veta
- Varje adress debiteras en gång per konto. Hittar du den igen, i en annan sökning eller en senare månad, kostar den inget.
- Svaret visar alltid
charged(krediter som anropet drog) ochcreditsLeft(krediter kvar i perioden). I listor har varje adress dessutomcharged: trueom just den debiterades i anropet. - Med noll krediter kvar svarar domänsök, mejlfinnaren och bolagssök med
402innan något söks. - Räcker krediterna inte till alla adresser levereras så många som de räcker till. I domänsök står antalet som inte visades i
hidden. Ett bolagssök stoppas med statusno_credits, och rader som redan har levererats finns kvar. - Krediterna fylls på varje månad, räknat från dagen du skapade kontot, betalade eller bytte plan. På betalda planer följer oanvända krediter med till nästa period, högst en månads krediter.
{
"domain": "ekdahls.exempel",
"emails": [
{
"email": "karin.dahl@ekdahls.exempel",
"status": "giltig",
"charged": true
},
{
"email": "erik.sund@ekdahls.exempel",
"status": "giltig",
"charged": true
},
{
"email": "info@ekdahls.exempel",
"type": "allman",
"charged": false
}
],
"hidden": 0,
"charged": 2,
"creditsLeft": 48
}
Samma sökning i appen
2 verifierade adresser, 2 krediter. 48 kvar.
Fel
Fel har en HTTP-status och en JSON-kropp med fältet error. Texten är skriven för att kunna visas för en användare som den är. Vissa fel har också code och fler fält.
| Status | Betydelse |
|---|---|
| 400 | Parametrar saknas eller är fel, eller kroppen är inte giltig JSON. error säger vad som behöver ändras. |
| 401 | Nyckeln saknas, är fel eller har ersatts av en ny. |
| 402 | Krediterna eller verifieringarna för månaden är slut. code är no_credits eller verify_quota, och creditsLeft visar saldot. |
| 403 | Kontot är spärrat, eller kontots e-postadress behöver bekräftas först (code: "email_unverified"). |
| 404 | Jobbet finns inte eller tillhör ett annat konto, eller sökvägen finns inte. |
| 405 | Fel metod, till exempel GET mot /v1/company-search. |
| 413 | Kroppen är större än 1 MB. |
| 415 | Kroppen är inte JSON. Skicka Content-Type: application/json. |
| 429 | För många anrop, eller för många bolagssökningar i kö. Se Gränser. |
| 500 | Något gick fel hos oss. Försök igen om en stund. |
{
"error": "Krediterna för månaden är slut. Uppgradera eller vänta till nästa period.",
"code": "no_credits",
"creditsLeft": 0
}
{
"error": "Din plan tillåter högst 100 bolag per sökning.",
"code": "max_companies",
"limit": 100
}
Gränser och huvuden
Varje nyckel får göra 60 anrop per minut. Svaren har två huvuden som visar hur många anrop som återstår i den aktuella minuten. Går du över gränsen får du 429 med Retry-After i sekunder, och samma värde i fältet retryAfter.
| Huvud | Innehåll |
|---|---|
| X-RateLimit-Limit | Antal anrop per minut för nyckeln. |
| X-RateLimit-Remaining | Anrop kvar i den aktuella minuten. |
| Retry-After | Bara vid 429. Sekunder tills du kan försöka igen. |
Krediterna syns i svarets kropp, inte i huvuden.
Gränser per plan
| Plan | Per månad och sökning |
|---|---|
| Gratis | 50 krediter, 100 verifieringar, högst 100 bolag per bolagssök. |
| Start | 1 000 krediter, 10 000 verifieringar, högst 1 000 bolag. |
| Pro | 5 000 krediter, 50 000 verifieringar, högst 5 000 bolag. |
| Byrå | 50 000 krediter, 500 000 verifieringar, högst 10 000 bolag. |
Ett konto kan ha fem bolagssökningar i kö eller igång samtidigt. De körs en i taget per konto.
HTTP/1.1 429 Too Many Requests Content-Type: application/json; charset=utf-8 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 Retry-After: 23
{
"error": "För många anrop. Gränsen är 60 per minut.",
"retryAfter": 23
}
POST/v1/domain-searcheller GET
Domänsök
Läser bolagets webbplats och returnerar alla adresser på domänen, med namn, roll, källa och verifieringsstatus. Svaret visar också adressformatet på webbplatsen, till exempel fornamn.efternamn, utläst ur de adresser som faktiskt är publicerade. Formatet är bara information och används aldrig för att skapa adresser.
Vi läser upp till 15 sidor och kontrollerar upp till 40 adresser mot mejlservern medan du väntar. Ett anrop kan ta upp till en minut, så sätt en timeout på minst 90 sekunder.
Parametrar
| Parameter | Beskrivning |
|---|---|
| domain | Bolagets domän, till exempel ekdahls.exempel. Du kan också skicka en hel webbadress eller en mejladress, så plockar vi ut domänen. |
Svar
| Fält | Beskrivning |
|---|---|
| domain | Domänen som söktes, utan www. |
| company | Bolagets namn, hämtat från webbplatsen. |
| website | Adressen vi läste. |
| pattern | Adressformatet på webbplatsen, utläst ur publicerade adresser, till exempel fornamn.efternamn eller f.efternamn. Bara information: det används aldrig för att skapa en adress. |
| patternExample | Formatet som mall: {fornamn}.{efternamn}@domän. Bara information. |
| emails | Adresserna, med personadresser först och högst säkerhet överst. Se Kontakten. |
| pagesFetched | Antal sidor vi läste. |
| hidden | Adresser som inte visas eftersom krediterna tog slut. Förklaringen står i reason. |
| reason | Finns bara när något behöver förklaras, till exempel att webbplatsen inte gick att nå. |
| charged | Krediter som anropet drog. |
| creditsLeft | Krediter kvar i perioden. |
| historyId | Sökningens id i historiken i appen. |
Debitering
1 kredit per debiterbar personadress i svaret, enligt regeln ovan. Allmänna adresser är gratis, och går webbplatsen inte att nå kostar sökningen inget.
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Domänen saknas, ser inte giltig ut eller tillhör en e-posttjänst för privatpersoner, som gmail.com. |
| 402 | no_credits: krediterna är slut. Ingen sökning görs. |
curl https://spanly.se/v1/domain-search \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "ekdahls.exempel"}'
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/domain-search', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain: 'ekdahls.exempel' }),
signal: AbortSignal.timeout(90_000),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const e of data.emails) console.log(e.email, e.status);
console.log(data.charged, data.creditsLeft);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.post(
"https://spanly.se/v1/domain-search",
headers={"Authorization": f"Bearer {key}"},
json={"domain": "ekdahls.exempel"},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(data["error"])
for e in data["emails"]:
print(e["email"], e["status"])
print(data["charged"], data["creditsLeft"])
{
"domain": "ekdahls.exempel",
"company": "Ekdahls Bygg AB",
"website": "https://ekdahls.exempel/",
"pattern": "fornamn.efternamn",
"patternExample": "{fornamn}.{efternamn}@ekdahls.exempel",
"emails": [
{
"email": "karin.dahl@ekdahls.exempel",
"name": "Karin Dahl",
"role": "VD",
"type": "person",
"status": "giltig",
"confidence": 95,
"source": "webbplats",
"sourceUrl": "https://ekdahls.exempel/om-oss",
"private": false,
"guessed": false,
"foundAt": "2026-11-04T09:12:40.118Z",
"reason": "Brevlådan finns.",
"charged": true
},
{
"email": "erik.sund@ekdahls.exempel",
"name": "Erik Sund",
"role": "Ekonomichef",
"type": "person",
"status": "giltig",
"confidence": 95,
"source": "webbplats",
"sourceUrl": "https://ekdahls.exempel/kontakt",
"private": false,
"guessed": false,
"foundAt": "2026-11-04T09:12:40.118Z",
"reason": "Brevlådan finns.",
"charged": true
},
{
"email": "info@ekdahls.exempel",
"name": "",
"role": "",
"type": "allman",
"status": "giltig",
"confidence": 95,
"source": "webbplats",
"sourceUrl": "https://ekdahls.exempel/kontakt",
"private": false,
"guessed": false,
"foundAt": "2026-11-04T09:12:40.118Z",
"reason": "Brevlådan finns.",
"charged": false
}
],
"pagesFetched": 7,
"hidden": 0,
"charged": 2,
"creditsLeft": 48,
"historyId": "hK2m9xQ4rT7b"
}
POST/v1/email-findereller GET
Mejlfinnaren
Hittar arbetsadressen till en namngiven person, där den står publicerad: på bolagets egen webbplats (kontakt-, team-, om oss- och pressidor, mejllänkar och strukturerad data) och i bolagets uppslag i en katalog eller ett register när adressen hör till personen. Du får adressen med källa och länk till sidan, och vi kontrollerar den mot mejlservern.
Vi bygger eller gissar aldrig en adress ur ett namn. Står ingen adress publicerad är email null, found är false och reason förklarar varför. Det kostar inget. En publicerad adress som mejlservern säger inte finns returneras inte.
Parametrar
| Parameter | Beskrivning |
|---|---|
| firstName | Förnamn, högst 60 tecken. Går också att skicka som first_name. |
| lastName | Efternamn, högst 60 tecken. Går också att skicka som last_name. |
| domain | Bolagets domän. Ange domain eller company. |
| company | Bolagets namn. Utan domain letar vi upp webbplatsen från namnet. |
| land | Landet vi letar i när webbplatsen ska hittas från bolagsnamnet. |
Svar
| Fält | Beskrivning |
|---|---|
Den publicerade adressen, eller null om ingen hittades. | |
| found | true när en publicerad adress hittades och returneras. |
| confidence | Hur säkra vi är på att adressen är rätt och når fram, 0–100. Är found false är den 0. |
| status | Resultatet av kontrollen. Se Statusar. |
| reason | Förklaring till statusen, eller till varför ingen adress hittades. |
| source | webbplats (bolagets egen sajt), hitta (företagskatalog) eller register (officiellt register). null om ingen adress hittades. |
| sourceUrl | Sidan där adressen är publicerad, eller null. |
| pages, sources | Antal sidor vi läste och vilka källor vi sökte i. |
| mx, catchAll | Mejlservern, och om den tar emot alla adresser. |
| name, domain, company | Personens namn och bolaget som söktes. |
| charged, creditsLeft | Krediter som drogs (0 eller 1) och krediter kvar. |
Debitering
1 kredit när en publicerad personadress hittas och är verifierad (giltig eller riskabel). Hittas ingen adress, eller går den inte att kontrollera (okand), kostar det inget. En adress du redan har betalat för kostar inget igen.
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Förnamn eller efternamn saknas eller är för långt, domänen ser inte giltig ut, eller både domain och company saknas. |
| 402 | no_credits: krediterna är slut. Ingen sökning görs. |
curl https://spanly.se/v1/email-finder \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Eva",
"lastName": "Ström",
"domain": "strombyra.exempel"
}'
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/email-finder', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: 'Eva',
lastName: 'Ström',
domain: 'strombyra.exempel',
}),
signal: AbortSignal.timeout(90_000),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.email ?? data.reason, data.confidence);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.post(
"https://spanly.se/v1/email-finder",
headers={"Authorization": f"Bearer {key}"},
json={
"firstName": "Eva",
"lastName": "Ström",
"domain": "strombyra.exempel",
},
timeout=90,
)
data = res.json()
if not res.ok:
raise RuntimeError(data["error"])
print(data["email"] or data["reason"], data["confidence"])
{
"name": "Eva Ström",
"domain": "strombyra.exempel",
"company": "Strömbyrå AB",
"email": "eva.strom@strombyra.exempel",
"found": true,
"confidence": 92,
"status": "giltig",
"reason": "Brevlådan finns.",
"source": "webbplats",
"sourceUrl": "https://strombyra.exempel/kontakt",
"pages": 6,
"sources": ["webbplats"],
"mx": "mx1.strombyra.exempel",
"catchAll": false,
"type": "person",
"private": false,
"charged": 1,
"creditsLeft": 47,
"historyId": "hP4w7kZr2nXc"
}
{
"name": "Eva Ström",
"domain": "strombyra.exempel",
"email": null,
"found": false,
"status": null,
"confidence": 0,
"reason": "Ingen publicerad adress hittades för Eva Ström på strombyra.exempel.",
"source": null,
"sourceUrl": null,
"charged": 0,
"creditsLeft": 48
}
POST/v1/email-verifiereller GET
Verifiera
Kontrollerar en adress: att den har rätt format, att domänen har en e-postserver (MX) och att servern tar emot just den brevlådan. Vi testar också om servern tar emot alla adresser (catch-all). Inget mejl skickas.
Resultatet sparas i 30 dagar, och ett okänt resultat i 6 timmar. Frågar du om samma adress inom den tiden får du det sparade resultatet, och cached är true. En adress som ägaren har spärrat kontrolleras inte: svaret har då status okand och suppressed: true.
Parametrar
| Parameter | Beskrivning |
|---|---|
| Adressen som ska kontrolleras, högst 254 tecken. |
Svar
| Fält | Beskrivning |
|---|---|
| status, reason | Resultatet och en mening om varför. Se Statusar. |
| mx | Mejlservern för domänen. |
| catchAll | Om servern tar emot alla adresser. null om det inte gick att testa. |
| type | Om adressen ser ut att tillhöra en person eller en funktion som info@. |
| private | Adressen ligger hos en e-posttjänst för privatpersoner. |
| cached | Resultatet kommer från en tidigare kontroll. |
| verify | used och limit för månadens verifieringar. |
| charged, creditsLeft | charged är alltid 0. |
Debitering
Kostar inga krediter. Varje anrop räknas mot månadens verifieringar: 100 på Gratis, 10 000 på Start, 50 000 på Pro och 500 000 på Byrå. Kontrollerna som domänsök och bolagssök gör räknas inte.
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Adressen saknas eller är längre än 254 tecken. |
| 402 | verify_quota: månadens verifieringar är slut. |
curl https://spanly.se/v1/email-verifier \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jonas.ek@ekmontage.exempel"}'
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/email-verifier', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'jonas.ek@ekmontage.exempel' }),
signal: AbortSignal.timeout(60_000),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.status, data.reason);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.post(
"https://spanly.se/v1/email-verifier",
headers={"Authorization": f"Bearer {key}"},
json={"email": "jonas.ek@ekmontage.exempel"},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(data["error"])
print(data["status"], data["reason"])
{
"email": "jonas.ek@ekmontage.exempel",
"status": "giltig",
"reason": "Brevlådan finns.",
"mx": "mx1.ekmontage.exempel",
"catchAll": false,
"type": "person",
"private": false,
"cached": false,
"charged": 0,
"creditsLeft": 47,
"verify": { "used": 13, "limit": 100 },
"historyId": "hW8c3nLq5vRd"
}
POST/v1/company-search
Bolagssök
Startar en sökning på bransch och område. Sökningen körs i bakgrunden och svaret kommer direkt med ett jobb. Hämta sedan jobbet med GET /v1/jobs/:id tills det är klart.
Reklamspärrade bolag tas aldrig med, och spärrade adresser filtreras bort.
Parametrar
| Parameter | Beskrivning |
|---|---|
| bransch | Branschen, till exempel Elinstallationer eller ett vardagligt ord som elfirmor. Vi tolkar ordet mot branschregistret och skriver resultatet i resolvedBransch. |
| land | Sverige, Norge eller Finland. Danmark går inte att söka, eftersom kall e-post till företag är förbjuden där. |
| lan | Län i Sverige, fylke i Norge eller ort i Finland, till exempel Uppsala. |
| kommun | Kommun, till exempel Borås. |
| minAnstallda | Minsta antal anställda. |
| max | Högsta antal bolag. Taket beror på planen, se Gränser. |
| filters | Fler filter, till exempel ekonomi, roller och uteslutna branscher. Se Sökfilter. |
| name | Namn på sökningen, högst 120 tecken. Utan namn sätter vi ett utifrån bransch och område. |
Svar
| Fält | Beskrivning |
|---|---|
| jobId | Jobbets id. |
| job | Jobbet med status queued. Se Jobbet. |
Debitering
Själva anropet kostar inget. Medan jobbet körs dras 1 kredit för varje debiterbar personadress som levereras, och summan står i jobbets charged.
Tar krediterna slut levereras först adresserna till VD, sedan stoppas jobbet med status no_credits. Raderna som redan har levererats finns kvar.
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Bransch saknas, landet är inte se, no eller fi, eller max är högre än planen tillåter (code: "max_companies" med limit). |
| 402 | no_credits: krediterna är slut. Inget jobb skapas. |
| 429 | Kontot har redan fem sökningar i kö eller igång. |
curl https://spanly.se/v1/company-search \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bransch": "Elinstallationer",
"land": "se",
"lan": "Uppsala",
"minAnstallda": 10,
"max": 50
}'
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/company-search', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
bransch: 'Elinstallationer',
land: 'se',
lan: 'Uppsala',
minAnstallda: 10,
max: 50,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.jobId, data.job.status);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.post(
"https://spanly.se/v1/company-search",
headers={"Authorization": f"Bearer {key}"},
json={
"bransch": "Elinstallationer",
"land": "se",
"lan": "Uppsala",
"minAnstallda": 10,
"max": 50,
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(data["error"])
print(data["jobId"], data["job"]["status"])
{
"jobId": "jT4n8c2vQp7a",
"job": {
"id": "jT4n8c2vQp7a",
"name": "Elinstallationer – Uppsala",
"type": "companies",
"status": "queued",
"params": {
"bransch": "Elinstallationer",
"land": "se",
"lan": "Uppsala",
"kommun": "",
"minAnstallda": 10,
"max": 50
},
"progress": { "done": 0, "total": 0 },
"counts": { "companies": 0, "emails": 0, "verified": 0 },
"charged": 0,
"createdAt": "2026-11-04T10:12:03.552Z",
"startedAt": null,
"finishedAt": null,
"resolvedBransch": null,
"messages": [],
"via": "api",
"queuePosition": 1
}
}
GET/v1/jobs/:id
Hämta jobb
Returnerar jobbets status och alla rader som har levererats hittills. Jobbet är klart när status inte längre är queued eller running.
Fråga högst var femte sekund medan jobbet körs. Med rows=0 får du bara status, vilket går snabbare för stora listor. Hämta raderna när jobbet är klart.
Parametrar
| Parameter | Beskrivning |
|---|---|
| id | Jobbets id, till exempel jT4n8c2vQp7a. |
| rows | Ange 0 för att bara få jobbet, utan rader. |
Svar
| Fält | Beskrivning |
|---|---|
| job | Se Jobbet. |
| rows | Ett bolag per rad, med kontakter. Se Raden. Saknas när rows=0. |
Debitering
Kostar inget. Krediterna drogs när raderna levererades.
Felkoder
| Status | Betydelse |
|---|---|
| 404 | Jobbet finns inte eller tillhör ett annat konto. |
curl -G https://spanly.se/v1/jobs/jT4n8c2vQp7a \ -d rows=0 \ -H "Authorization: Bearer $SPANLY_API_KEY"
const key = process.env.SPANLY_API_KEY;
const headers = { Authorization: `Bearer ${key}` };
const url = 'https://spanly.se/v1/jobs/jT4n8c2vQp7a';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let job;
do {
await sleep(5000);
const res = await fetch(`${url}?rows=0`, { headers });
({ job } = await res.json());
console.log(job.status, job.progress);
} while (job.status === 'queued' || job.status === 'running');
const { rows } = await (await fetch(url, { headers })).json();
console.log(rows.length, 'bolag,', job.charged, 'krediter');
import os
import time
import requests
key = os.environ["SPANLY_API_KEY"]
headers = {"Authorization": f"Bearer {key}"}
url = "https://spanly.se/v1/jobs/jT4n8c2vQp7a"
while True:
time.sleep(5)
res = requests.get(url, params={"rows": 0},
headers=headers, timeout=30)
job = res.json()["job"]
print(job["status"], job["progress"])
if job["status"] not in ("queued", "running"):
break
res = requests.get(url, headers=headers, timeout=60)
rows = res.json()["rows"]
print(len(rows), "bolag,", job["charged"], "krediter")
{
"job": {
"id": "jT4n8c2vQp7a",
"name": "Elinstallationer – Uppsala",
"type": "companies",
"status": "done",
"progress": { "done": 50, "total": 50 },
"counts": { "companies": 50, "emails": 44, "verified": 39 },
"charged": 36,
"startedAt": "2026-11-04T10:12:04.010Z",
"finishedAt": "2026-11-04T10:19:47.731Z",
"resolvedBransch": "Elinstallationer",
"via": "api"
},
"rows": [
{
"bolag": "Fyrisvalla El AB",
"orgnr": "000000-0000",
"land": "se",
"ort": "Uppsala",
"region": "Uppsala",
"bransch": "Elinstallationer",
"anstallda": "14",
"webb": "https://fyrisvallael.exempel",
"telefon": "018-000 00 00",
"vd": "Lena Holm",
"vdRoll": "VD",
"contacts": [
{
"email": "l.holm@fyrisvallael.exempel",
"name": "Lena Holm",
"role": "VD",
"type": "person",
"status": "giltig",
"confidence": 95,
"source": "webbplats",
"sourceUrl": "https://fyrisvallael.exempel/",
"private": false,
"guessed": false,
"foundAt": "2026-11-04T10:12:31.204Z",
"reason": "Brevlådan finns.",
"charged": true
}
]
}
]
}
GET/v1/jobs
Lista jobb
Kontots senaste 500 bolagssökningar, nyaste först. Listan innehåller sökningar från både appen och API:t, och via visar vilket. Raderna ingår inte.
Svar
En lista med jobb. Se Jobbet.
Debitering
Kostar inget.
curl https://spanly.se/v1/jobs \ -H "Authorization: Bearer $SPANLY_API_KEY"
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/jobs', {
headers: { Authorization: `Bearer ${key}` },
});
const jobs = await res.json();
for (const j of jobs) console.log(j.id, j.name, j.status);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.get(
"https://spanly.se/v1/jobs",
headers={"Authorization": f"Bearer {key}"},
timeout=30,
)
for j in res.json():
print(j["id"], j["name"], j["status"])
[
{
"id": "jT4n8c2vQp7a",
"name": "Elinstallationer – Uppsala",
"type": "companies",
"status": "done",
"progress": { "done": 50, "total": 50 },
"charged": 36,
"via": "api"
}
]
POST/v1/jobs/:id/stop
Stoppa jobb
Stoppar ett jobb som körs eller står i kö. Rader som redan har levererats finns kvar, och krediterna för dem är redan dragna. Ett jobb som körs kan stå kvar som running någon sekund innan det blir stopped.
Parametrar
| Parameter | Beskrivning |
|---|---|
| id | Jobbets id. |
Svar
Jobbet, se Jobbet.
Debitering
Kostar inget.
Felkoder
| Status | Betydelse |
|---|---|
| 404 | Jobbet finns inte eller tillhör ett annat konto. |
curl -X POST https://spanly.se/v1/jobs/jT4n8c2vQp7a/stop \ -H "Authorization: Bearer $SPANLY_API_KEY"
const key = process.env.SPANLY_API_KEY;
const url = 'https://spanly.se/v1/jobs/jT4n8c2vQp7a/stop';
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${key}` },
});
const job = await res.json();
console.log(job.status);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.post(
"https://spanly.se/v1/jobs/jT4n8c2vQp7a/stop",
headers={"Authorization": f"Bearer {key}"},
timeout=30,
)
print(res.json()["status"])
GET/v1/jobs/:id/export
Exportera
Hämtar jobbets rader som fil, i samma format som exporten i appen. Svaret är filen, inte JSON. Spärrade adresser kommer aldrig med.
Parametrar
| Parameter | Beskrivning |
|---|---|
| id | Jobbets id. |
| format |
csv: alla fält, en rad per adress, semikolon och UTF-8 med BOM så att svensk Excel öppnar filen rätt.enkel: bara bolag och mejl, utan ogiltiga adresser.instantly: kolumner för Instantly och Lemlist.tsv: tabbseparerad, för Google Kalkylark. |
Debitering
Kostar inget.
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Okänt format. |
| 404 | Jobbet finns inte eller tillhör ett annat konto. |
curl -G https://spanly.se/v1/jobs/jT4n8c2vQp7a/export \ -d format=csv \ -H "Authorization: Bearer $SPANLY_API_KEY" \ -o elinstallationer-uppsala.csv
import { writeFile } from 'node:fs/promises';
const key = process.env.SPANLY_API_KEY;
const url = 'https://spanly.se/v1/jobs/jT4n8c2vQp7a/export';
const res = await fetch(`${url}?format=csv`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const file = Buffer.from(await res.arrayBuffer());
await writeFile('elinstallationer-uppsala.csv', file);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.get(
"https://spanly.se/v1/jobs/jT4n8c2vQp7a/export",
params={"format": "csv"},
headers={"Authorization": f"Bearer {key}"},
timeout=60,
)
res.raise_for_status()
with open("elinstallationer-uppsala.csv", "wb") as f:
f.write(res.content)
bolag;orgnr;land;ort;region;bransch;anstallda;webb;telefon;vd;vd_roll;mejl;namn;roll;typ;status;sakerhet;kalla;kalla_url;privat;hittad;styrelse;register_url Fyrisvalla El AB;000000-0000;se;Uppsala;Uppsala;Elinstallationer;14;https://fyrisvallael.exempel;018-000 00 00;Lena Holm;VD;l.holm@fyrisvallael.exempel;Lena Holm;VD;person;giltig;95;webbplats;https://fyrisvallael.exempel/;nej;2026-11-04T10:12:31.204Z;;
GET/v1/account
Konto
Plan, krediter och verifieringar för den aktuella perioden. Bra för att kontrollera nyckeln och saldot före en stor körning.
Svar
| Fält | Beskrivning |
|---|---|
| email, name, company | Kontots uppgifter. |
| plan | Planens id. Namnet står i planName. |
| period | Månads- eller årsbetalning. Krediterna fylls på varje månad i båda fallen. |
| credits | used, limit, remaining, bonus, periodStart och resetsAt, som är nästa påfyllning. |
| verify | used, limit och remaining för månadens verifieringar. |
| limits | maxCompanies: högsta antal bolag per bolagssök. |
Debitering
Kostar inget.
curl https://spanly.se/v1/account \ -H "Authorization: Bearer $SPANLY_API_KEY"
const key = process.env.SPANLY_API_KEY;
const res = await fetch('https://spanly.se/v1/account', {
headers: { Authorization: `Bearer ${key}` },
});
const account = await res.json();
console.log(account.planName, account.credits.remaining);
import os
import requests
key = os.environ["SPANLY_API_KEY"]
res = requests.get(
"https://spanly.se/v1/account",
headers={"Authorization": f"Bearer {key}"},
timeout=30,
)
account = res.json()
print(account["planName"], account["credits"]["remaining"])
{
"email": "du@dittbolag.exempel",
"name": "Anna Berg",
"company": "Dittbolag AB",
"plan": "start",
"planName": "Start",
"period": "month",
"credits": {
"used": 212,
"limit": 1000,
"remaining": 788,
"bonus": 0,
"periodStart": "2026-10-14T08:02:11.000Z",
"resetsAt": "2026-11-14T08:02:11.000Z"
},
"verify": {
"used": 1406,
"limit": 10000,
"remaining": 8594
},
"limits": { "maxCompanies": 1000 }
}
POST/v1/phone-finder
Telefonnummer
Vi visar bara nummer som bolaget självt har publicerat, på sin webbplats eller i registret. Växelnumret är alltid synligt och gratis. Ett direkt- eller mobilnummer som står vid en namngiven person är dolt tills du visar det, och då kostar det 1 kredit. Samma nummer debiteras bara en gång per konto.
Anrop
| Anrop | Beskrivning |
|---|---|
| /v1/phone-finder | En persons nummer. Skicka domain eller company och name (eller first_name och last_name), och gärna land. Personens nummer visas och debiteras direkt. Svaret har också bolagets växel i companyPhone. |
| /v1/phone-reveal | Visar dolda nummer. Skicka ids, orgnr och land, domain eller job_id. Med max (eller bekraftat_antal) debiteras högst så många nya nummer, resten förblir dolda. Svarar 402 om inget kunde visas för att krediterna är slut. |
| /v1/phone-lookup | Vilket bolag ett nummer hör till (nr, land). Bara bolag som själva har publicerat numret. Mobilnummer och nummer i enskilda firmor ger alltid tomt svar. Kostar inget. |
| reveal_phones=1 | Visar och debiterar personnumren i phones direkt i domänsökets svar. |
Numret
| Fält | Beskrivning |
|---|---|
| type | Växel eller personligt nummer. |
| number, display | Numret i E.164 och i läsbar form. number är null när numret är dolt, och då är display maskerat. |
| name, role | Personen som numret står vid. |
| source, sourceUrl, foundAt | Var och när numret hittades. |
| private | Numret hör till en enskild firma. Tänk på NIX och GDPR. |
Debitering
1 kredit per personligt nummer som visas för första gången. Växelnummer och uppslag med /v1/phone-lookup kostar inget.
curl -X POST https://spanly.se/v1/phone-finder \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Lena Holm","domain":"fyrisvallael.exempel"}'
POST/v1/company-search
Sökfilter
Bolagssöket tar också emot filters, eller samma nycklar direkt i kroppen. Med andra filter behövs inte bransch. Alla nycklar är valfria. Urvalet blir de max största bolagen som matchar.
Vanliga nycklar
| Nyckel | Beskrivning |
|---|---|
| branscher, uteslutBranscher | Branscher att ta med eller utesluta, som namn eller kod. En kod gäller som prefix, så "43" är hela 43. |
| regioner, uteslutRegioner | Län, kommuner eller orter. |
| maxAnstallda | Bolag med okänt antal räknas som 0. |
| minOmsattningTkr, maxOmsattningTkr, minResultatTkr, maxResultatTkr | Ur senaste årsredovisningen. Finns bara för svenska bolag. |
| tillvaxt | true betyder att omsättningen ökade senaste året. Ett objekt med ar (1–3) och gränser som minOmsattning eller maxAnstallda anger förändringen i procent mot bokslutet så många år tidigare. |
| nyckelord, uteslutOrd | Ord i namnet eller branschen. |
| roller, nivaer | Vilka personer som ska med, till exempel vd, ekonomi eller ledning. |
| perBolag | Högst så många kontakter per bolag. Bara de debiteras. |
| signaler, signalDagar | Bolag med händelser inom så många dagar (standard 90). |
| doljListade | Hoppa över bolag som redan finns i någon av dina listor. |
| skipContacted | Hoppa över bolag som har en annan status än Ej kontaktad i CRM:et. De kostar inga krediter. |
Ett bolag utan nyckeltal tas inte med när ett ekonomi- eller tillväxtfilter är satt. Medan sökindexet laddas efter en omstart svarar anropet 503. Försök igen om en stund.
curl -X POST https://spanly.se/v1/company-search \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"land": "se",
"max": 200,
"filters": {
"branscher": ["43"],
"regioner": ["Västra Götalands län"],
"minOmsattningTkr": 10000,
"roller": ["vd"],
"perBolag": 1
}
}'
GET/v1/crm/companies
CRM
Läs och ändra säljstatus och anteckningar för kontots bolag. I ett team är det teamets CRM. Ett bolag anges med land och orgnr, med eller utan bindestreck. Ett bolag som inte finns i CRM:et har statusen ny.
Anrop
| Anrop | Beskrivning |
|---|---|
| GET /v1/crm/companies | Bolagen, senast ändrade först. limit är högst 1000 (standard 100). Svar: {statuses, companies}. |
| GET /v1/crm/companies/:land/:orgnr | Ett bolag med kontakter, anteckningar, uppgifter och aktivitet. |
| PUT /v1/crm/companies/:land/:orgnr | Sätter status: {status, name?}. Svar: {ok, changed, company}. |
| POST /v1/crm/notes | Ny anteckning: {land, orgnr, text, email?}. Svar: {note}. |
status
ny (Ej kontaktad), kontaktad, svarat, mote (Möte bokat), ej_intresserad, kund och ror_ej (Rör ej).
Felkoder
| Status | Betydelse |
|---|---|
| 400 | Okänd status. |
| 403 | Nyckeln hör till en läsare i ett team, som inte får ändra. |
Debitering
Kostar inget.
curl -X PUT https://spanly.se/v1/crm/companies/se/000000-0000 \
-H "Authorization: Bearer $SPANLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"mote"}'
Integrationer och webhooks
HubSpot, Pipedrive, Salesforce, Microsoft Dynamics 365, SuperOffice, Upsales, Lime CRM, Lime Go och Google Kalkylark kopplar du under Integrationer i appen. Säljstatusen kan synkas åt båda hållen med alla åtta CRM, och en lista kan fyllas på i samma kalkylark varje natt. Där lägger du också till webhooks, till exempel till Zapier eller Make. Att skicka kostar inga krediter, och ogiltiga eller spärrade adresser skickas aldrig.
Varje leverans är en POST med JSON: {id, type, createdAt, data}. Svara med 2xx inom 10 sekunder. Svar med 5xx, 408 eller 429 och nätverksfel görs om efter 1, 5, 30, 120 och 720 minuter, högst 6 försök. Övriga 4xx görs inte om, och 410 stänger av webhooken.
Händelser
list.completed- Ett bolagssök är klart, med bolagen och kontakterna (högst 1000 bolag).
contact.created- En ny personadress har hittats och betalats.
status.changed- Ett bolags status i CRM:et ändrades.
reply.received- Någon svarade på ett utskick.
bulk.done- En uppladdad fil är färdigbearbetad, med länk till resultatfilen.
ping- Testhändelse från knappen Skicka test.
Signatur
Huvudet X-Spanly-Signature är sha256= följt av HMAC-SHA256 med webhookens nyckel (whsec_…) av "<X-Spanly-Timestamp>.<rå kropp>". X-Spanly-Delivery är samma vid nya försök, så använd det för att undvika dubbletter.
import crypto from 'node:crypto';
function verify(req, rawBody, secret) {
const ts = req.headers['x-spanly-timestamp'];
const sig = String(req.headers['x-spanly-signature'] || '');
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && expected.length === sig.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Chrome-tillägget
Tillägget visar bolaget och adresserna för webbplatsen du besöker, hittar en persons adress och slår upp avsändaren i Gmail. Det loggar in med samma API-nyckel och kostar samma krediter som appen. Att slå upp en avsändare är gratis, och att kontrollera adressen räknas mot planens verifieringar.
Tillägget gör ingenting förrän du klickar. I Gmail läses bara avsändarens adress i mejlet som är öppet, aldrig ämne, text eller bilagor.
Anrop som tillägget använder
| Anrop | Beskrivning |
|---|---|
| GET /v1/ext/me | Konto, krediter och sparade listor. |
| POST /v1/ext/domain-search | Domänsök: {domain}. |
| POST /v1/ext/email-finder | Mejlfinnaren: {firstName, lastName, company?, domain?}. |
| POST /v1/ext/save | Sparar en träff i en lista. |
| POST /v1/ext/sender | Avsändarens bolag, VD, status i CRM:et och känd mejlstatus: {email}. Kostar inget. |
| POST /v1/ext/verify | Kontrollerar en adress: {email}. |
| POST /v1/ext/save-sender | Sparar avsändaren i en lista. |
| POST /v1/ext/task | Skapar en uppgift i CRM:et: {title, dueAt?, email?, land?, orgnr?}. |
Privata adresser som gmail.com och hotmail.com kopplas inte till något bolag och sparas inte.
Statusar och källor
Varje adress har en status från vår kontroll mot mejlservern, och en källa som visar var den kommer ifrån. Vi gissar aldrig fram en status, och vi skapar aldrig en adress: bara publicerade adresser returneras.
status
- giltig
- Mejlservern bekräftar att brevlådan finns, och domänen tar inte emot vilka adresser som helst.
- riskabel
- Servern tar emot alla adresser (catch-all), så just den här brevlådan går inte att bekräfta.
- ogiltig
- Brevlådan eller domänen finns inte, domänen saknar e-postserver, eller adressen har fel format.
- okand
- Kontrollen gick inte att göra. Servern svarade inte, använder grålistning eller stoppar kontroller, eller så var vår förbindelse till den blockerad.
reasonsäger vilket.
source
webbplats- Adressen står på bolagets egen webbplats.
sourceUrlpekar på sidan. registerhitta- Adressen kommer från offentliga bolagsuppgifter, i ett bolagsregister eller en företagskatalog.
sourceUrlpekar på källan när den finns.
Kontakten
En adress i emails från domänsök och i contacts på en rad från bolagssök.
| Fält | Beskrivning |
|---|---|
| Adressen, med gemener. | |
| name, role | Namn och roll när de går att se, annars tomma. |
| type | En persons adress, eller en funktion som info@ och kontakt@. |
| status, reason | Se Statusar. |
| confidence | Säkerhet, utifrån källan och kontrollen. |
| source, sourceUrl | Var adressen hittades. |
| foundAt | När adressen hittades. |
| private | Adressen ligger hos en e-posttjänst för privatpersoner. Tänk efter innan du kontaktar den. |
| charged | Om just den här adressen debiterades i det här anropet. |
Jobbet
En bolagssökning. Samma sökningar syns i appen.
| Fält | Beskrivning |
|---|---|
| id, name | Jobbets id och namn. |
| status |
queued i kö, running söker, done klart, stopped stoppat, failed misslyckades (se error), no_credits stoppat för att krediterna tog slut. |
| params | Parametrarna, med standardvärden ifyllda. |
| progress | done och total: genomsökta bolag av totalt. |
| counts | companies levererade bolag, emails adresser som inte är ogiltiga, verified giltiga och riskabla. |
| charged | Krediter som jobbet har dragit hittills. |
| createdAt, startedAt, finishedAt | Tidpunkter i ISO 8601 (UTC), eller null. |
| resolvedBransch | Branschen som sökordet tolkades som. |
| messages | Korta meddelanden om körningen, högst 50. |
| via | Var jobbet skapades. |
| queuePosition | Bara när status är queued. 1 betyder näst på tur. |
| error, suggestions | Bara när jobbet misslyckades: förklaringen, och förslag på branscher om sökordet inte gick att tolka. |
Raden
Ett bolag i ett bolagssök. Värden som saknas är tomma strängar.
| Fält | Beskrivning |
|---|---|
| bolag, orgnr, land | Bolagets namn, organisationsnummer och land. |
| ort, region | Ort, och län, fylke eller region. |
| bransch, anstallda, omsattningTkr | Bransch, antal anställda och omsättning i tusental kronor. Antal anställda kan vara ett intervall, därför är värdena strängar. |
| webb, webbKalla, telefon | Bolagets webbplats, varifrån webbadressen kom, och telefonnummer. |
| vd, vdRoll | Bolagets VD eller motsvarande, och rollen. |
| styrelse | Styrelsens ledamöter när de finns. |
| registerUrl | Länk till bolagets offentliga bolagsuppgifter när den finns. |
| reklamsparr | Alltid false. Reklamspärrade bolag levereras inte. |
| contacts | Bolagets adresser, med personer först. Se Kontakten. |
Nyckeln får du när du har skapat ett konto. Gratisplanen har 50 verifierade adresser i månaden. Frågor om API:t skickar du till kontakt@spanly.se.
Skapa konto