Recept

Koppla valfri kreditupplysningsbyrå

Programmet äger formatet en upplysning lagras i. Du äger byrån, avtalet och nyckeln. Har du redan ett avtal någonstans behöver du inte byta för vår skull — kontraktet nedan är allt en koppling behöver träffa.

Vad vi driftar, och vad vi inte gör

Vi driftar en koppling: Syna, produkten Kreditupplysning AB. Skälet är inte att de är störst — det är att de publicerar sina XML-scheman och sin produktdokumentation utan inloggning, så att hela mappningen går att pröva mot leverantörens eget facit i stället för mot vad vi mindes. Testsviten validerar vår fråga mot deras frågeschema och våra svar mot deras svarsschema, och validerar först deras egen exempelfil för att bevisa att validatorn duger.

De andra får ett recept i stället, och det är ett val vi står för:

  • Creditsafe Connect. Öppet kontrakt och recept, ingen driftad koppling. Två skäl, båda ur Creditsafes egen dokumentation: sandlådan går inte att skaffa själv utan kräver ett välkomstmejl från en account manager, och autentiseringen är användarnamn och LÖSENORD (POST /v1/authenticate ger en token som lever en timme). Ett kontolösenord är inte en API-nyckel, och det ska inte ligga i vår databas. Har du ett konto vi får låna för att köra provet skarpt blir den här raden en driftad koppling i stället.
  • Roaring. Öppet kontrakt och recept. Roaring har en självbetjänad gratis sandlåda, men autentiseringsformen är inte bekräftad ur något dokument vi läst — och en adapter som gissar hur inloggningen ser ut är precis det den här ramen finns för att inte bygga.

En adapter mot ett API vi aldrig kört skarpt är en gissning med kod runt omkring, och en gissning om kreditvärdighet blir ditt beslut på fel underlag. Ett öppet kontrakt kostar dig tjugo rader kod en gång; en adapter kostar oss underhåll mot ett gränssnitt någon annan ändrar när de vill — och det är den kostnaden som annars hamnar på ditt pris.

Kontraktet

Fyra beslut i formen nedan är avsiktliga och värda att läsa innan du skriver din adapter.

type CreditRating =
  | { known: true
      /** 1–5 där 5 är bäst. VÅR skala — aldrig byråns. */
      grade: 1 | 2 | 3 | 4 | 5
      /** Byråns egen beteckning, ordagrant: "AAA", "Kreditklass 4", "B". */
      providerLabel: string }
  | { known: false; reason: string }   // "Företaget klassas ej!"

type CreditCheck = {
  orgNumber: string          // tio siffror, alltid juridiskt format
  legalName: string
  legalForm: string | null

  rating: CreditRating
  creditLimit: { amount: number; currency: string
                 /** "under" | "over" när byrån bara lämnar en gräns */
                 bound: "under" | "over" | null } | null
  defaultRiskPercent: number | null

  /** count 0 är ett SVAR. null är "vet ej". Aldrig samma sak. */
  remarks: { count: number; totalAmount: number | null } | null
  debtBalance: { amount: number; currency: string } | null
  status: string | null      // konkurs, rekonstruktion, varningslista

  fetchedAt: string          // NÄR VI FRÅGADE (ISO 8601)
  dataAsOf: string | null    // hur färskt BYRÅNS underlag var
  source: string             // "syna", "manuell", "din-byra"
  sourceProduct: string | null
}

1. grade är vår skala, inte byråns

Creditsafe säger A–E, Syna säger en siffra 1–5, UC säger något tredje. Att exponera den ena hade gjort kontraktet till den leverantörens, och nästa byrå hade inte gått att koppla in utan att ljuga i datan. Vi normaliserar till 1–5 där 5 är bäst, och behåller providerLabel ordagrant bredvid — ingen information går förlorad, ingen leverantör äger vokabulären.

2. { known: false } är ett riktigt svar

"Går inte att klassa" är inte samma sak som ett dåligt betyg, och kontraktet får inte kollapsa det till noll. Syna svarar bokstavligen "Företaget klassas ej!" på inaktiva bolag.

3. remarks: null är inte count: 0

"Byrån svarade att det inte finns några anmärkningar" och "vi frågade inte efter anmärkningar" är olika besked, och de får aldrig se likadana ut på ett kundkort. Samma sak gäller boundpå limiten: "mindre än 5 000 kr" är inte 5 000 kr.

4. Två datum, inte ett

fetchedAt är när du frågade. dataAsOf är hur färskt byråns underlag var — en klassning kan vara flera år gammal när du hämtar den. Kreditupplysningslagen 12 § ger byrån skyldighet att rätta en oriktig uppgift och underrätta var och en som fått den under de senaste tolv månaderna; ett lagrat svar är alltså färskvara, och det är därför båda datumen måste bäras.

Adaptern

// En adapter är en funktion. Ingenting mer.
export async function hamtaUpplysning(orgNumber: string): Promise<CreditCheck> {
  const svar = await fetch(`https://api.dinbyra.se/v2/company/${orgNumber}`, {
    headers: { Authorization: `Bearer ${process.env.BYRA_KEY}` },
  }).then((r) => r.json());

  return {
    orgNumber,
    legalName: svar.name,
    legalForm: svar.legal_form ?? null,

    // Byråns skala översätts till vår. Behåll deras beteckning ordagrant.
    rating: svar.rating
      ? { known: true, grade: BETYG[svar.rating], providerLabel: svar.rating }
      : { known: false, reason: svar.rating_comment ?? "Byrån lämnar ingen klassning." },

    creditLimit: svar.credit_limit
      ? { amount: svar.credit_limit, currency: svar.currency ?? "SEK", bound: null }
      : null,
    defaultRiskPercent: svar.pod ?? null,

    // Svarade byrån "inga anmärkningar"? Skriv 0. Frågade du inte? Skriv null.
    remarks: svar.remarks ? { count: svar.remarks.count, totalAmount: svar.remarks.sum } : null,
    debtBalance: null,
    status: svar.status_text ?? null,

    fetchedAt: new Date().toISOString(),
    dataAsOf: svar.data_date ?? null,
    source: "din-byra",
    sourceProduct: "Företagsupplysning v2",
  };
}

// A–E blir 1–5. Riktningen är byråns, siffran är vår.
const BETYG: Record<string, 1 | 2 | 3 | 4 | 5> = { A: 5, B: 4, C: 3, D: 2, E: 1 };

Lägg funktionen i din egen kopia av programmet och anropa den där runCreditCheck() i src/lib/credit/run.ts väljer leverantör. Vill du inte röra koden alls: välj Egen leverantör i inställningarna och skriv in svaret för hand — samma kundkort, samma upplysningsdatum, samma gallring.

Tre gränser du inte får flytta

Bara juridiska personer

Organisationsnumret prövas mot tre villkor innan något anrop görs: tio siffror (tolvsiffrig form kräver prefixet 16), siffrorna 3–4 minst 20, och giltig Luhn-kontrollsiffra. Villkor två utesluter varje personnummer — och därmed varje enskild firma, som har innehavarens personnummer som organisationsnummer. När en kreditupplysning om en fysisk person lämnas ut ska en kopia samtidigt och kostnadsfritt skickas till den omfrågade (11 §) — utan undantag för näringsidkare, och enligt tredje stycket gäller det också handelsbolag och kommanditbolag. Är personen dessutom inte näringsidkare krävs legitimt behov (9 §). Den kopieplikten byggs inte halvvägs. Regeln står som CHECK i databasen, så ingen kodväg kan gå runt den.

Ett lagrat svar får aldrig lämnas vidare

Att hämta en upplysning för det egna bolagets kreditbeslut är att vara användare. Att lämna den vidare — genom ett API, genom byråportalen till en annan klient, genom en delad databas — är att lämna en kreditupplysning (2 §) som ett led i näringsverksamhet, alltså kreditupplysningsverksamhet enligt kreditupplysningslagen 1 §, och den kräver tillstånd av Integritetsskyddsmyndigheten enligt 3 §. Tabellen är därför stängd för både byrårollen och API-rollen med restriktiva policyer, och det finns ingen endpoint som läser den. Bygger du en egen adapter: håll svaret i din egen installation.

Spara aldrig byråns råsvar

En fullständig företagsrapport innehåller styrelseledamöternas personnummer. Kontraktet har därför inget raw-fält och inga namngivna fysiska personer alls — det är den tekniska garantin för att en företagsupplysning förblir en företagsupplysning. Svaret gallras dessutom 18 månader efter upplysningsdatum.

Vidare