Language: **English** · [Español](./kydhub-docs.es.md)

<!--
KydHub documentation Markdown source generated from the dev2 HTML preview.
Canonical review preview: http://192.168.0.232:4000/
Do not paste secrets, API keys, client secrets, webhook secrets, tokens, or PII here.
-->

# What KydHub solves

KydHub (Know Your Data Hub) is a verified identity and data hub. It allows a company like **Acme** to query authorized data from people and companies without having to collect, validate and maintain that information themselves.

The idea is simple: the person or company keeps its data in KydHub, KydHub acts as the trusted source, and each integration receives only the fields it needs and is authorized to read.

### Available capabilities now

### Query a person

Acme can query Bob Carter's authorized data using his KYD, for example `BOB.CARTER`, or a public ID `per_...`.

### Person addresses

A company can request only the registered addresses it needs, such as shipping, billing or residence addresses, depending on authorization.

### Query a company

Acme can query authorized data from another company by enterprise KYD or public ID `comp_...`, without using KydHub as an open directory.

### Company addresses

You can also query addresses or registered locations of a company, when the use case allows.

### Login with optional KWID

If you want to delegate login or SSO, KydHub offers OAuth/OIDC. If not, you can skip it and just use Data API.

### Advanced security

Server-only API keys, signed webhooks and optional post-quantum encryption for sensitive payloads.

### Integration by stack

Recipes for Python/FastAPI, Node.js/Express, Go and desktop apps like Tauri, Electron and Rust.

View integration recipes

### What the public API does not do

## For companies and people

KydHub connects two needs: companies that need reliable data and people who want to control what information they share.

#### For companies

- You query verified data without manually requesting it again.
- You request only the fields necessary for your use case.
- You use API keys server-to-server from the Acme backend.
- You can keep your current login and use KydHub only as a Data API.
- If needed, you can also offer Login with KWID/OIDC.
- You receive signed webhook events when the flow requires it.
#### For people

- You keep your identity and data in a controlled place.
- You authorize which company can read which information.
- You avoid repeating name, profile or addresses in each integration.
- Companies read authorized data, but do not modify your data via public API.
- Your KYD, for example `BOB.CARTER`, serves as a verifiable identifier.
### Services available at this stage

| Service | What it allows | Who uses it |
| --- | --- | --- |
| Person | Consult authorized data of a person, such as identity or basic profile. | Acme backend with API key. |
| Person addresses | Query a person's registered addresses when flow and permissions allow. | Acme backend with API key. |
| Company | Query authorized data from another company using enterprise KYD or public ID `comp_...`. | Acme backend with API key. |
| Company addresses | Check addresses or registered locations of a company. | Acme backend with API key. |

## Key concepts

These names appear in almost all examples. The idea is that you know what identifies who before copying a request.

## Before you start

Before making a call, separate three things: the environment where you are going to test, the credential that authorizes the Acme backend, and the identifier of the resource you want to query.

### Environments and URLs

| That | Valor | When to use |
| --- | --- | --- |
| Dev Dashboard | `https://dev.dashboard.kydhub.com` | Browser surface for KydHub dashboard in dev. |
| Dev Auth / OAuth endpoints | `https://dev.auth.kydhub.com` | Target canonical dev host for OAuth/OIDC/Login endpoints. Until rollout finishes, read exact endpoints from discovery. |
| Dev Docs | `https://dev.docs.kydhub.com` | Developer documentation in dev. |
| Data API base | `https://dev.api.kydhub.com/api/v1` | Server-to-server queries with API key. It does not require you to use KydHub OAuth. |
| OIDC Issuer | `https://dev.kydhub.com` | Stable issuer. Validate that the `iss` of the token matches this value. |
| OIDC Discovery | `https://dev.kydhub.com/.well-known/openid-configuration` | Source of truth for authorization, token, UserInfo and JWKS endpoints. |
> Auth host migration: `https://dev.auth.kydhub.com` is the target host for OAuth/OIDC/Login. `https://dev.kydhub.com` remains the stable issuer/discovery host; `https://dev.api.kydhub.com` remains the Data API host. Clients must use discovery and must not hardcode OAuth endpoint hosts.


### Minimum authentication for Data API

To query data, Acme sends an API key from its backend. This key identifies the company and allows applying limits, scopes, auditing and permissions.

```
QUERY https://dev.api.kydhub.com/api/v1/persons/query
X-API-KEY: kyd_sandbox_...
Content-Type: application/json
```

| Atributo | What is | Regla |
| --- | --- | --- |
| X-API-KEY | Company server-to-server secret. | Never put it in frontend, mobile apps, repositories or public documentation. |
| Content-Type | Request body format. | Use `application/json` in examples with body JSON. |

### Identifiers you are going to use

| Atributo | Example | When to use it |
| --- | --- | --- |
| person_kyd | `BOB.CARTER` | When you know the person's public KYD/KWID. |
| person_id | `per_...` | When you already have that person's opaque public ID. |
| company_kyd | `ACME.SUPPLIER` | When you consult data about a company for its business KYD. |
| company_id | `comp_...` | When you already have the opaque public ID of that company. |
| fields | `["full_name", "addresses.city"]` | List of requested fields. I ordered only what was necessary for your flow. |
| service_name | `"acme-checkout"` | Technical slug of the internal service making the query. It must be lowercase ASCII with hyphens. |
| response_profile | `"standard"` | Full JSON structure: `standard`, `summary`, `compliance` or `audit`. |
| format | `{ "names": "surname_first_comma", "addresses": "postal" }` | Presentation rules for authorized values: names, addresses, text, dates and locale. |

## Response formatting: profile vs presentation

KydHub separates three decisions so integrations stay unambiguous:

| Attribute | What it decides | Example |
| --- | --- | --- |
| `fields` | Which data Acme requests. | `name`, `addresses.shipping`, `profile.occupation` |
| `response_profile` | How the full JSON payload is organized. | `standard`, `summary`, `compliance`, `audit` |
| `format` | How authorized values are presented. | surname first, uppercase, no accents, postal address, localized date |

`format` is data presentation. It does not grant permissions, add fields, or replace scopes/Data Grants. If a field is not authorized, KydHub will not return it even if a formatting rule exists for that field.

### Response profiles (`response_profile`)

These profiles do not change name/address casing or text values; they only change the overall payload structure.

| `response_profile` | Use | Real difference |
| --- | --- | --- |
| `standard` | Normal integrations, SDKs, storage and webhooks. | Returns requested fields in the canonical `fields` structure. |
| `summary` | UI, cards, checkout, support and listings. | Returns a shallower human-readable summary for quick display. |
| `compliance` | Onboarding, formal review, verification and evidence. | Groups formal/verified data and compliance signals; it is not text formatting. |
| `audit` | Logs, debugging and access evidence. | Returns request/authorization/result traceability without expanding unnecessary business data. |

### Presentation format (`format`)

The client can ask KydHub to preformat values so each application does not have to implement the same rules.

```json
{
  "fields": ["name", "addresses.shipping", "profile.occupation", "created_at"],
  "response_profile": "standard",
  "format": {
    "names": "surname_first_comma",
    "addresses": "multiline",
    "text": "locale_title_case",
    "dates": "localized_datetime",
    "locale": "es-US",
    "include_raw": true
  }
}
```

### Response when authorization is missing

If the service does not have an approved data grant yet, the Data API returns an actionable error. It does not expose internal UUIDs and it does not use the portal response wrapper.

```json
{
  "status": "error",
  "error": {
    "code": "data_grant_required",
    "person_kyd": "BOB.CARTER",
    "service_name": "acme-checkout",
    "requested_fields": ["name"],
    "required_permissions": [
      {"permission": "person.identity:read", "reason": "Required to identify the requested person in the Data API flow."},
      {"permission": "person.name:read", "reason": "Required to read the requested field: name."}
    ],
    "resolution": {
      "action": "request_data_grant",
      "endpoint": "POST /api/v1/data-grants/requests",
      "request_body": {
        "subject_type": "person",
        "subject_kyd": "BOB.CARTER",
        "requested_scopes": ["person.identity:read", "person.name:read"],
        "purpose": "Allow acme-checkout to read the person's name.",
        "external_reference": "acme-checkout"
      }
    }
  }
}
```

When the response is successful, `person_id` always uses the `per_` prefix; the internal person-table UUID is never exposed.


Expected response:

```json
{
  "person_id": "per_7Q9M2K4R",
  "person_kyd": "BOB.CARTER",
  "response_profile_used": "standard",
  "format_used": {
    "names": "surname_first_comma",
    "addresses": "multiline",
    "text": "locale_title_case",
    "dates": "localized_datetime",
    "locale": "es-US",
    "include_raw": true
  },
  "fields": {
    "name": {
      "raw": "Bob José de la Cruz O'Neill García",
      "formatted": "O'Neill García, Bob José de la Cruz"
    },
    "addresses": {
      "shipping": {
        "raw": {
          "line1": "1200 Brickell Ave",
          "line2": "Suite 800",
          "city": "Miami",
          "region": "FL",
          "postal_code": "33131",
          "country": "US"
        },
        "formatted": "1200 Brickell Ave\nSuite 800\nMiami, FL 33131\nUS"
      }
    },
    "profile": {
      "occupation": {
        "raw": "software engineer",
        "formatted": "Software engineer"
      }
    },
    "created_at": {
      "raw": "2026-06-30T18:40:00Z",
      "formatted": "30/06/2026 18:40 UTC"
    }
  }
}
```

### Proposed name formats

> **Name field granularity:** `name` returns the complete authorized name. `name.primary` returns first given name + first family name for compact UI. `name.parts` returns structured arrays: `given_names`, `middle_names`, `family_names`. `format.names` controls presentation order/case; it must not silently drop second names or second surnames.


Base value used in examples:

```json
{
  "given_names": ["Bob", "José"],
  "middle_names": ["de la Cruz"],
  "family_names": ["O'Neill", "García"],
  "full_name": "Bob José de la Cruz O'Neill García"
}
```

| `format.names` | `formatted` output | Recommended use |
| --- | --- | --- |
| `as_recorded` | `Bob José de la Cruz O'Neill García` | Maximum fidelity to the stored value. |
| `given_first` | `Bob José de la Cruz O'Neill García` | Normal end-user UI. |
| `surname_first_comma` | `O'Neill García, Bob José de la Cruz` | CRM, reports and administrative lists. |
| `surname_first_space` | `O'Neill García Bob José de la Cruz` | Older systems that do not accept commas. |
| `locale_title_case` | `Bob José de la Cruz O'Neill García` | Locale-aware capitalization. |
| `upper` | `BOB JOSÉ DE LA CRUZ O'NEILL GARCÍA` | Documents/reports preserving accents. |
| `lower` | `bob josé de la cruz o'neill garcía` | Visual normalization or soft matching. |
| `ascii` | `Bob Jose de la Cruz ONeill Garcia` | Non-Unicode systems or simple search. |
| `ascii_upper` | `BOB JOSE DE LA CRUZ ONEILL GARCIA` | Legacy/regulatory systems that require uppercase ASCII. |
| `initials` | `BJDCONG` | Avatars or ultra-compact views. |
| `initials_dotted` | `B. J. O. G.` | Avatars, compact signatures or UI. |

Example request:

```json
{
  "fields": ["name"],
  "format": {
    "names": "ascii_upper",
    "include_raw": true
  }
}
```

Response:

```json
{
  "name": {
    "raw": "Bob José de la Cruz O'Neill García",
    "formatted": "BOB JOSE DE LA CRUZ ONEILL GARCIA",
    "format_used": "ascii_upper"
  }
}
```

### Proposed address formats

Base value used in examples:

```json
{
  "line1": "1200 Brickell Ave",
  "line2": "Suite 800",
  "city": "Miami",
  "region": "FL",
  "postal_code": "33131",
  "country": "US"
}
```

| `format.addresses` | `formatted` output | Recommended use |
| --- | --- | --- |
| `as_recorded` | `1200 Brickell Ave, Suite 800, Miami, FL, 33131, US` | Fidelity to the original record. |
| `single_line` | `1200 Brickell Ave, Suite 800, Miami, FL 33131, US` | Cards, checkout, dashboards and tables. |
| `multiline` | `1200 Brickell Ave\nSuite 800\nMiami, FL 33131\nUS` | PDFs, documents and postal blocks. |
| `postal` | `1200 Brickell Ave\nSuite 800\nMiami FL 33131\nUNITED STATES` | Physical shipping and country-specific logistics. |
| `compact` | `Miami, FL, US` | Profile, search or compact lists. |
| `uppercase` | `1200 BRICKELL AVE, SUITE 800, MIAMI, FL 33131, US` | Labels and systems that require uppercase. |
| `ascii_upper` | `AV. JOSE MARIA MORELOS #123, MERIDA, YUCATAN, MX` | Accented addresses for legacy systems. |

Example with `postal`:

```json
{
  "address": {
    "formatted": "1200 Brickell Ave\nSuite 800\nMiami FL 33131\nUNITED STATES",
    "format_used": "postal",
    "country_format_used": "US"
  }
}
```

### Proposed text and metadata formats

Applies to fields such as `profile.occupation`, `company.display_name`, `industry`, `city`, `region` or visible labels.

Base value:

```text
Inteligencia Artificial & Automatización
```

| `format.text` | `formatted` output | Recommended use |
| --- | --- | --- |
| `preserve` | `Inteligencia Artificial & Automatización` | Preserve the original value. |
| `upper` | `INTELIGENCIA ARTIFICIAL & AUTOMATIZACIÓN` | Uppercase reports or UI. |
| `lower` | `inteligencia artificial & automatización` | Visual normalization. |
| `locale_title_case` | `Inteligencia artificial & automatización` | Readable titles with locale-aware casing. |
| `ascii` | `Inteligencia Artificial & Automatizacion` | Systems without full Unicode support. |
| `ascii_upper` | `INTELIGENCIA ARTIFICIAL & AUTOMATIZACION` | Legacy, matching or rigid exports. |
| `slug` | `inteligencia-artificial-automatizacion` | URLs, keys, anchors or internal integrations. |

### Proposed date formats

Base value:

```text
2026-06-30T18:40:00Z
```

| `format.dates` | `formatted` output | Recommended use |
| --- | --- | --- |
| `iso` | `2026-06-30T18:40:00Z` | APIs, storage and interoperability. |
| `date` | `2026-06-30` | Day-level reports. |
| `datetime` | `2026-06-30 18:40:00 UTC` | Human-readable logs. |
| `localized_date` | `30/06/2026` | Localized UI. |
| `localized_datetime` | `30/06/2026 18:40 UTC` | Localized UI with time. |

### Invalid format error

```json
{
  "error": "invalid_format",
  "message": "Unsupported presentation format.",
  "field": "format.names",
  "allowed_formats": [
    "as_recorded",
    "given_first",
    "surname_first_comma",
    "surname_first_space",
    "locale_title_case",
    "upper",
    "lower",
    "ascii",
    "ascii_upper",
    "initials",
    "initials_dotted"
  ]
}
```

## API keys and credentials

To use the Data API, Acme needs an API key created from the KydHub portal. This key identifies the company querying, applies usage limits and allows auditing which fields were requested.

### Where to get an API key

| Step | To do | Result |
| --- | --- | --- |
| 1 | Enter the KydHub portal with your username. | You access your personal or business space. |
| 2 | Change to the company context, for example `Acme`. | The credentials remain associated with that company. |
| 3 | I opened `Company → API Access → API keys`. | You see the active and revoked API keys and their dates. |
| 4 | Create a new API key and choose whether it expires or never expires. | KydHub generates the entire secret only once. |
| 5 | Copy the secret and save it in the backend of Acme or secret manager. | You can now call the Data API from your server. |

### How to use the API key

The API key travels in the `X-API-KEY` header. It does not identify a final person; identifies the integrating company that is consulting authorized data.

```
curl -X QUERY https://dev.api.kydhub.com/api/v1/persons/query \
  -H "X-API-KEY: $KYDHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"person_kyd":"BOB.CARTER","fields":["full_name"]}'
```

| Atributo | What does it mean | Usage rule |
| --- | --- | --- |
| $KYDHUB_API_KEY | Environment variable with Acme's royal secret. | Do not paste the actual value in code, logs, tickets or documentation. |
| X-API-KEY | Server-to-server authentication header. | It should be sent by the Acme backend, never by the browser. |
| person_kyd | Public KYD of the person consulted. | It can be changed to `person_id` if you already have it. |
| fields | Exact fields you want to read. | I ordered the minimum necessary for your use case. |

### Difference between credentials

| Credential | What is it for? | Where should you live | Can you go to the browser? |
| --- | --- | --- | --- |
| API key | Query Data API from backend. | Backend or secret manager. | No. |
| OAuth client_id | Start Login with KWID/OIDC. | Frontend/backend according to flow. | Yes, it's not a secret. |
| OAuth client_secret | Authenticate a confidential OAuth client. | Backend or secret manager. | No. |
| Secret webhook | Verify signatures of incoming events. | Webhook receiver backend. | No. |

### Company Free Limits

| Area | Limit | What does it imply? |
| --- | --- | --- |
| API keys | 2 active | You can have two active keys per company; revoke one before creating a third. |
| Expiration | Optional | A key may not expire or have an expiration date. |
| Rate limit | 60/min | If you exceed the limit, the API responds with a rate limit error. |
| daily use | 2,000/day | Designed for initial integrations and controlled pilots. |

## Integration by stack

These recipes show how to connect your Acme backend or app with KydHub. The main rule is to separate the integration types well: the **Data API** uses API key server-to-server; **OAuth/OIDC** is optional for Login with KWID; desktop apps should use system browser + PKCE and never distribute secrets.

### What pattern to use according to your stack

| Stack | Recommended use | Secret allowed | Notes |
| --- | --- | --- | --- |
| Python/FastAPI | Server-to-server backend for Data API, OAuth callback and webhooks. | Yes, in environment variables/secret manager. | Ideal for validating tokens, saving API keys and verifying HMAC. |
| Node.js/Express | Server-to-server backend for Data API, OAuth callback and webhooks. | Yes, in environment variables/secret manager. | Good entry point for SPA/SSR web apps. |
| Go | Backend or high-performance internal service. | Yes, in environment variables/secret manager. | Useful for B2B integrations, workers and business services. |
| Tauri | Desktop app with system browser + PKCE. | Not within the binary. | Use custom callback scheme or local loopback; Responsive data API via backend. |
| Electron | Desktop app with system browser + PKCE. | Not within the app package. | Don't use `client_secret` in main/renderer. |
| Rust | CLI/desktop/backend with PKCE or server-side service. | It depends on the type of app. | Rust Server can save secrets; distributed app no. |

### Python / FastAPI · consult person

This example exposes an internal Acme endpoint that queries authorized Bob Carter data on KydHub. The API key lives only in the backend.

```
import os
import httpx
from fastapi import FastAPI, HTTPException

app = FastAPI()
KYDHUB_BASE_URL = os.getenv("KYDHUB_BASE_URL", "https://dev.api.kydhub.com/api/v1")
api_key = os.getenv("KYDHUB_API_KEY", "set-in-secret-manager")

@app.get("/internal/kydhub/person/{person_kyd}")
async def get_person(person_kyd: str):
    payload = {
        "person_kyd": person_kyd,
        "service_name": "acme-checkout",
        "fields": ["full_name", "profile.display_name"],
    }
    headers = {
        "X-API-KEY": api_key,
        "Content-Type": "application/json",
    }
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.request("QUERY", f"{KYDHUB_BASE_URL}/api/v1/persons/query", json=payload, headers=headers)
    if response.status_code >= 400:
        raise HTTPException(status_code=response.status_code, detail=response.json())
    return response.json()
```

### Node.js / Express · consult company

This example queries data authorized for a company by `company_kyd`. The API key remains in the Express backend.

```
import express from "express";

const app = express();
app.use(express.json());

const KYDHUB_BASE_URL = process.env.KYDHUB_BASE_URL ?? "https://dev.api.kydhub.com/api/v1";
const apiKey = process.env.KYDHUB_API_KEY ?? "set-in-secret-manager";

app.post("/internal/kydhub/company", async (req, res) => {
  const response = await fetch(`${KYDHUB_BASE_URL}/api/v1/companies/query`, {
    method: "QUERY",
    headers: {
      "X-API-KEY": apiKey,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      company_kyd: req.body.company_kyd,
      service_name: "acme-supplier-review",
      fields: ["legal_name", "status", "profile.industry"]
    })
  });

  const body = await response.json();
  res.status(response.status).json(body);
});
```

### Go · consult company addresses

Go works great for internal services, workers, or B2B integrations. This example queries locations for a company using `company_kyd`.

```
package main

import (
  "bytes"
  "encoding/json"
  "net/http"
  "os"
  "time"
)

func queryCompanyLocations(companyKYD string) (*http.Response, error) {
  baseURL := os.Getenv("KYDHUB_BASE_URL")
  if baseURL == "" { baseURL = "https://dev.api.kydhub.com/api/v1" }

  payload := map[string]any{
    "company_kyd": companyKYD,
    "service_name": "acme-supplier-review",
    "fields": []string{"locations.city", "locations.country"},
  }
  body, err := json.Marshal(payload)
  if err != nil { return nil, err }

  req, err := http.NewRequest("QUERY", baseURL+"/api/v1/companies/query", bytes.NewReader(body))
  if err != nil { return nil, err }
  req.Header.Set("X-API-KEY", os.Getenv("KYDHUB_API_KEY"))
  req.Header.Set("Content-Type", "application/json")

  client := &http.Client{Timeout: 10 * time.Second}
  return client.Do(req)
}
```

### Desktop apps · Tauri, Electron and Rust

Distributed desktop apps must not include secrets. To Login with KWID use system browser, PKCE, `state` and `nonce`. For responsive Data API, your app must call your backend and your backend calls KydHub.

| Option | Use | Redirect URI | Secret |
| --- | --- | --- | --- |
| Custom scheme | App registered as handler. | `acme://oauth/callback` | Do not use `client_secret`. |
| Local Loopback | App lifts temporary callback. | `http://127.0.0.1:{port}/callback` | Do not use `client_secret`. |
| Backend broker | Desktop talks to Acme backend. | Acme web callback. | The backend can keep secrets. |

### Electron · open Login with KWID

```
import { shell } from "electron";
import crypto from "node:crypto";

const state = crypto.randomUUID();
const nonce = crypto.randomUUID();
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");

const url = new URL("https://dev.auth.kydhub.com/oauth/authorize");
url.searchParams.set("client_id", "acme_desktop_client");
url.searchParams.set("redirect_uri", "acme://oauth/callback");
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "openid kwid profile");
url.searchParams.set("state", state);
url.searchParams.set("nonce", nonce);
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");

shell.openExternal(url.toString());
```

### Tauri / Rust · open system browser

```
use open;
use url::Url;

fn start_kydhub_login(code_challenge: &str, state: &str, nonce: &str) -> anyhow::Result<()> {
    let mut url = Url::parse("https://dev.auth.kydhub.com/oauth/authorize")?;
    url.query_pairs_mut()
        .append_pair("client_id", "acme_desktop_client")
        .append_pair("redirect_uri", "acme://oauth/callback")
        .append_pair("response_type", "code")
        .append_pair("scope", "openid kwid profile")
        .append_pair("state", state)
        .append_pair("nonce", nonce)
        .append_pair("code_challenge", code_challenge)
        .append_pair("code_challenge_method", "S256");

    open::that(url.as_str())?;
    Ok(())
}
```


## HTTP QUERY (RFC 10008) and compatibility

`QUERY` is canonical for KydHub complex read-only queries. It is safe, idempotent and retryable. JSON query content does not modify person or company state.

| Contract | Value |
| --- | --- |
| Canonical operations | `QUERY /api/v1/persons/query` and `QUERY /api/v1/companies/query` |
| Required headers | `X-API-KEY`, `Content-Type: application/json`; use `Accept: application/json` for explicit negotiation. |
| Discovery | `OPTIONS` returns `Allow: QUERY, POST, OPTIONS` and `Accept-Query: application/json`. |
| Body limit | 1 MiB; oversized content returns `413`. |
| Privacy cache policy | `Cache-Control: no-store, private`; shared caching is disabled in the initial rollout. |
| Compatibility | `POST /api/v1/*/query` is a deprecated temporary alias using the same authorization/query pipeline until Gate C proves client, proxy, WAF and gateway support. |

Errors: `400` malformed/missing content metadata or JSON; `401` invalid/missing API key; `403` authorization denied; `406` unacceptable response type; `413` body too large; `415` unsupported query media type; `422` unprocessable query content.

> Use the POST alias only when a client or deployed intermediary cannot emit or forward QUERY. Production readiness requires Gate C end-to-end evidence.

## QUERY · Consult data about a person

This is the first major use case for the Data API: a company like Acme queries authorized data about a person, such as Bob Carter, using their `person_kyd` or `person_id`.

### Minimum request

```
QUERY https://dev.api.kydhub.com/api/v1/persons/query
X-API-KEY: $KYDHUB_API_KEY
Content-Type: application/json

{
  "person_kyd": "BOB.CARTER",
  "service_name": "acme-checkout",
  "fields": ["full_name", "profile.display_name"]
}
```

### Request attributes

| Atributo | Obligatorio | What does it mean | Regla |
| --- | --- | --- | --- |
| X-API-KEY | Yeah | Acme server-to-server API key. | It should live only in backend/secret manager. |
| person_kyd | one of two | Public KYD/KWID of the person, such as `BOB.CARTER`. | Use it when the user or your system knows the public alias. |
| person_id | one of two | Opaque public ID of the person, such as `per_...`. | Use it when you already received it before KydHub. Do not send internal UUIDs. |
| service_name | Yes | Technical slug of the service making the query. | Required format: `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Valid: `acme-checkout`, `billing-v2`. Invalid: `Acme Checkout`, `acme_checkout`, `acmé-checkout`, `acme--checkout`. |
| fields | Yeah | Exact list of fields that Acme wants to read. | I only asked for what was necessary. Each field may require scope/grant. |
| response_profile | Optional | Full payload structure. | Use `standard`, `summary`, `compliance` or `audit`. Omit it for `standard`. |
| format | Optional | Presentation of authorized values. | Configure names, addresses, text, dates, locale and `include_raw`; it does not grant permissions. |

### Expected response

```
{
  "data": {
    "person_id": "per_7Q9M2K4R",
    "person_kyd": "BOB.CARTER",
    "fields": {
      "full_name": "Bob Carter",
      "profile": {
        "display_name": "Bob Carter"
      }
    },
    "grant": {
      "status": "active",
      "scopes": ["person.identity:read", "person.profile:read"]
    }
  }
}
```

### Response attributes

| Atributo | What does it mean | How to use it |
| --- | --- | --- |
| data | Main container of the response. | Read the data from this object; do not assume fields outside the contract. |
| person_id | Bob's opaque public ID on KydHub. | Save it for future reference instead of just relying on the alias. |
| person_kyd | Standard public KYD/KWID. | Show it if your UI needs to refer to the public identity. |
| fields | Object with the approved and returned fields. | Don't expect fields you didn't ask for or that weren't authorized. |
| grant.status | Status of the authorization used to respond. | `active` indicates that the read was authorized. |
| grant.scopes | Reading scopes that support the answer. | Useful for auditing and debugging permissions. |

### Query by `person_id`

If Acme already has Bob's opaque public ID, you can use `person_id` instead of `person_kyd`.

```
{
  "person_id": "per_7Q9M2K4R",
  "service_name": "acme-checkout",
  "fields": ["full_name"]
}
```

### Common mistakes

| Error | What does it mean | To do |
| --- | --- | --- |
| missing_api_key | `X-API-KEY` did not arrive. | Send the key from the backend, never from the browser. |
| invalid_api_key | The key does not exist, was revoked or expired. | Create/rotate a key in the KydHub portal. |
| grant_required | There is no active authorization to read that data. | Start the corresponding authorization/consent flow. |
| field_not_allowed | You requested a field outside of scopes or grant. | Remove the field or request the appropriate permission. |
| person_not_found | There is no person with that `person_kyd` or `person_id`. | Verify the identifier and do not retry with invented data. |

## QUERY · Consult addresses of a person

A company can query a person's registered addresses when the use case warrants it and authorization exists for those fields. For example, Acme may need Bob Carter's city and country to complete a shipping or billing flow.

### Petition for KYD of person

```
QUERY https://dev.api.kydhub.com/api/v1/persons/query
X-API-KEY: $KYDHUB_API_KEY
Content-Type: application/json

{
  "person_kyd": "BOB.CARTER",
  "service_name": "acme-shipping",
  "fields": [
    "addresses.type",
    "addresses.city",
    "addresses.region",
    "addresses.country",
    "addresses.postal_code"
  ]
}
```

### Request attributes

| Atributo | Obligatorio | What does it mean | Regla |
| --- | --- | --- | --- |
| person_kyd | one of two | Public KYD/KWID of the person, such as `BOB.CARTER`. | Use it if you know the person's public alias. |
| person_id | one of two | Opaque public ID of the person, such as `per_...`. | Use it if it was already returned by KydHub in a previous query. |
| service_name | Yeah | Technical slug of the service requesting the address. | Example: `acme-shipping` or `acme-billing`. |
| fields | Yeah | Address fields you want to read. | You should include only necessary and authorized attributes. |

### Common address fields

| Field | What does it return | When to order it |
| --- | --- | --- |
| addresses.type | Address type, for example `shipping`, `billing` or `registered`. | When you need to distinguish use of address. |
| addresses.line1 | Main address line. | Only if you need a complete address to operate. |
| addresses.line2 | Complement, department, floor or reference. | Optional; Order it only if your flow uses it. |
| addresses.city | City or town. | Useful for shipping, coverage or regional validation. |
| addresses.region | Province, state or region. | Useful for taxes, logistics or local rules. |
| addresses.country | Country in readable format or code according to final contract. | Useful for coverage, compliance and billing. |
| addresses.postal_code | Zip code. | Useful for shipping, taxes or territorial validation. |
| addresses.is_primary | Indicates if it is the main address. | Request it if you need to choose a default address. |

### Expected response

```
{
  "data": {
    "person_id": "per_7Q9M2K4R",
    "person_kyd": "BOB.CARTER",
    "fields": {
      "addresses": [
        {
          "type": "shipping",
          "city": "Buenos Aires",
          "region": "Ciudad Autónoma de Buenos Aires",
          "country": "AR",
          "postal_code": "C1000"
        }
      ]
    },
    "grant": {
      "status": "active",
      "scopes": ["person.addresses:read"]
    }
  }
}
```

### Response attributes

| Atributo | What does it mean | How to use it |
| --- | --- | --- |
| fields.addresses | List of authorized and returned addresses. | It may come empty if there are no authorized or registered addresses for the requested filter/fields. |
| type | Use of address. | Don't assume that there is always one address of each type. |
| city ​​/ region / country | General location. | Useful for validation, coverage and regional rules. |
| postal_code | Zip code. | It may be absent if it was not requested, does not exist or was not authorized. |
| grant.scopes | Scopes that support reading. | For person addresses, there must be permission to read addresses. |

### Variant for `person_id`

If Acme already saved the `person_id`, you can query addresses without sending `person_kyd` again.

```
{
  "person_id": "per_7Q9M2K4R",
  "service_name": "acme-shipping",
  "fields": ["addresses.city", "addresses.country"]
}
```

### Common mistakes

| Error | What does it mean | To do |
| --- | --- | --- |
| grant_required | Acme does not have active authorization to read Bob's addresses. | Ask for consent or adjust the flow to not require direction. |
| field_not_allowed | Some field `addresses.*` is not covered by scopes/grant. | Remove the field or request the correct permission. |
| address_not_available | There is no registered/authorized address for the requested fields. | Show an alternative in your flow; Don't invent direction. |
| person_not_found | The `person_kyd` or `person_id` does not exist. | Verify the received identifier. |

## QUERY · Consult data from another company

A company like Acme can query authorized data from another company registered on KydHub using its `company_kyd` or `company_id`. This flow is used to validate suppliers, counterparties, businesses, business clients or B2B relationships.

### Request by `company_kyd`

```
QUERY https://dev.api.kydhub.com/api/v1/companies/query
X-API-KEY: $KYDHUB_API_KEY
Content-Type: application/json

{
  "company_kyd": "ACME.SUPPLIER",
  "service_name": "acme-supplier-review",
  "fields": [
    "legal_name",
    "display_name",
    "status",
    "profile.industry"
  ]
}
```

### Request attributes

| Atributo | Obligatorio | What does it mean | Regla |
| --- | --- | --- | --- |
| X-API-KEY | Yeah | Acme server-to-server API key. | Identifies the consulting company and applies limits/audit. |
| company_kyd | one of two | Public KYD of the queried company, for example `ACME.SUPPLIER`. | Use it when you know the KYD identifier of the target company. |
| company_id | one of two | Opaque public company ID, for example `comp_...`. | Use it when KydHub already returned that ID to you in a previous integration. |
| service_name | Yeah | Technical slug of the service making the query. | Example: `acme-supplier-review`. |
| fields | Yeah | Exact list of business fields to read. | I asked for only the necessary fields. There is no complete answer by default. |

### Common business fields

| Field | What does it return | When to order it |
| --- | --- | --- |
| legal_name | Registered legal name of the company. | Contractual validation, billing or B2B onboarding. |
| display_name | Trade or display name. | Show the company in UI or reports. |
| company_kyd | Standardized enterprise KYD. | Cross references and future consultations. |
| company_id | Opaque public ID `comp_...`. | Save a stable identifier without using internal UUID. |
| status | Public/authorized status of the company on KydHub. | Validate if the company is registered, active or verifiable according to contract. |
| profile.industry | Industry or sector if available/authorized. | Segmentation, compliance or classification of suppliers. |
| profile.website | Public website if available/authorized. | Manual validation or presentation in UI. |

### Expected response

```
{
  "data": {
    "company_id": "comp_42P7K9M2",
    "company_kyd": "ACME.SUPPLIER",
    "fields": {
      "legal_name": "Acme Supplier LLC",
      "display_name": "Acme Supplier",
      "status": "active",
      "profile": {
        "industry": "Logistics"
      }
    },
    "scopes": ["company.identity:read", "company.profile:read"]
  }
}
```

### Response attributes

| Atributo | What does it mean | How to use it |
| --- | --- | --- |
| company_id | Opaque public ID of the queried company. | Save it for future reference. Don't confuse it with internal UUID. |
| company_kyd | Standardized enterprise KYD. | Display it or use it as a public readable identifier. |
| fields | Authorized fields that KydHub returns. | Do not assume unrequested or disallowed fields. |
| status | Company authorized status on KydHub. | Use it for UI/flow decisions, not as a substitute for legal review if applicable. |
| scopes | Scopes that support reading. | Useful for auditing and debugging permissions. |

### Variant for `company_id`

If Acme already has the opaque public company ID, you can use `company_id` instead of `company_kyd`.

```
{
  "company_id": "comp_42P7K9M2",
  "service_name": "acme-supplier-review",
  "fields": ["legal_name", "status"]
}
```

### Common mistakes

| Error | What does it mean | To do |
| --- | --- | --- |
| company_not_found | There is no company with that `company_kyd` or `company_id`. | Verify the received identifier. Don't try to discover companies by brute force. |
| field_not_allowed | You requested a business field that is not allowed for your key/scope. | Remove the field or request the corresponding scope from the portal/contract. |
| company_scope_required | The API key does not have the required company data scope. | Request/enable the needed scope for the key; plan limits control capacity, not the data category itself. |
| invalid_api_key | The API key does not exist, was revoked, or has expired. | Rotate or create a new API key from the portal. |

## QUERY · Consult addresses of a company

Acme may also query another company's registered addresses or locations when it needs to validate billing, operational, branch, or coverage data. Like the rest of the Data API, the query is read-only and requires requesting specific fields.

### Request by `company_kyd`

```
QUERY https://dev.api.kydhub.com/api/v1/companies/query
X-API-KEY: $KYDHUB_API_KEY
Content-Type: application/json

{
  "company_kyd": "ACME.SUPPLIER",
  "service_name": "acme-supplier-review",
  "fields": [
    "locations.type",
    "locations.name",
    "locations.city",
    "locations.region",
    "locations.country"
  ]
}
```

### Request attributes

| Atributo | Obligatorio | What does it mean | Regla |
| --- | --- | --- | --- |
| company_kyd | one of two | Public KYD of the consulted company. | Use it when you know the business KYD identifier. |
| company_id | one of two | Opaque public ID of the company, such as `comp_...`. | Use it if KydHub has already returned it before. |
| service_name | Yeah | Service that consults locations. | Example: `acme-supplier-review`. |
| fields | Yeah | Requested business location/address fields. | Order only what you need for your B2B flow. |

### Common business location fields

| Field | What does it return | When to order it |
| --- | --- | --- |
| locations.type | Location type, for example `headquarters`, `branch`, `billing` or `operations`. | When you need to distinguish headquarters, branch or operation. |
| locations.name | Legible name of the location. | Useful to display it in UI or reports. |
| locations.line1 | Main line of business direction. | Only if you need a complete address. |
| locations.line2 | Complement or reference. | Optional; Order it only if your operation uses it. |
| locations.city | City or town. | Regional coverage, logistics or validation. |
| locations.region | Province, state or region. | Fiscal, legal or logistical rules. |
| locations.country | Country. | Coverage, compliance or international billing. |
| locations.postal_code | Zip code. | Billing, taxes or physical shipping. |
| locations.is_primary | Indicates if it is the main location. | To choose default location. |

### Expected response

```
{
  "data": {
    "company_id": "comp_42P7K9M2",
    "company_kyd": "ACME.SUPPLIER",
    "fields": {
      "locations": [
        {
          "type": "headquarters",
          "name": "Main office",
          "city": "Miami",
          "region": "Florida",
          "country": "US",
          "is_primary": true
        }
      ]
    },
    "scopes": ["company.locations:read"]
  }
}
```

### Response attributes

| Atributo | What does it mean | How to use it |
| --- | --- | --- |
| fields.locations | List of authorized business locations or addresses. | May come empty if no locations are available/authorized. |
| type | Location use or category. | Don't assume that all companies have the same types. |
| name | Legible name of the location. | Use it for UI/reports, not as a unique ID. |
| city ​​/ region / country | General geographic location. | Useful for regional rules and coverage validation. |
| is_primary | Mark main location. | It may be missing if it was not requested or is not defined. |
| scopes | Scopes that support reading. | For locations, read permission for business locations must exist. |

### Variant for `company_id`

```
{
  "company_id": "comp_42P7K9M2",
  "service_name": "acme-supplier-review",
  "fields": ["locations.city", "locations.country"]
}
```

### Common mistakes

| Error | What does it mean | To do |
| --- | --- | --- |
| company_not_found | There is no company with that `company_kyd` or `company_id`. | Verify the identifier through an authorized channel. |
| company_locations_scope_required | The API key does not have permission to read company locations. | Request/enable `company.locations:read`; plan limits control capacity, not the data category itself. |
| location_not_available | There is no available/authorized location for the requested fields. | Show an alternative or request fewer fields. |
| field_not_allowed | Some `locations.*` field is not allowed. | Remove the field or request the corresponding scope. |

## Login with KWID / OAuth OIDC optional

Login with KWID allows Acme to use KydHub as an identity provider using OAuth 2.0 and OpenID Connect. It is useful when you want to delegate login, SSO or identity verification to KydHub.

### When to use it and when to skip it

| Situation | Recommendation | Because |
| --- | --- | --- |
| Acme already has its own login | You can bypass OAuth from KydHub. | Use Data API with API key from backend to query authorized data. |
| Acme wants Login with KWID | Use OAuth 2.0 + OIDC. | KydHub authenticates the user and returns verifiable tokens/claims. |
| Web application with backend | Use Authorization Code + PKCE and confidential client if applicable. | The backend can store secrets securely. |
| Mobile or desktop application | Use PKCE and treat it as a public client unless there is a backend. | Don't put `client_secret` inside a distributed app. |

## GET1 · Start Login with KWID

**What it is for:** Acme redirects the user to KydHub to confirm their identity with KWID. When you return to the Acme callback, the backend receives a `code` which it then redeems for tokens.

```
# redirigí al usuario a KydHub
GET https://dev.auth.kydhub.com/oauth/authorize
  ?client_id=kh_client_…
  &redirect_uri=https://app.acme.example/callback
  &response_type=code
  &scope=openid kwid email profile company:read
  &state=RANDOM
  &code_challenge=…&code_challenge_method=S256
```

| Parameter | ¿Obligatorio? | What is |
| --- | --- | --- |
| client_id | Yeah | The Acme app ID on KydHub. |
| redirect_uri | Yeah | Where the user returns to. It must match **exactly** with the one registered. |
| response_type | Yeah | Always `code`. |
| scope | Yeah | Data you request, separated by space. |
| state | Yeah | Random anti-CSRF value; you validate it when you return. |
| code_challenge | Yeah | PKCE: the hash of your `code_verifier` (method S256). |
| nonce | Optional | Recommended: you validate it in the id_token to avoid replays. |
| login_hint | Optional | Pre-fill the KWID (e.g. `BOB.CARTER`). |
| prompt | Optional | `login` forces re-authentication even if there is SSO. |

## POST2 · Email verification

If the user does not have an active KydHub session, the first `/authorize` may return with `error=login_required` and `verification_required`. **This is an expected intermediate state, not a fatal error.** Acme should display “Check your email” and start verification from its backend.

```
POST https://dev.auth.kydhub.com/oauth/login/start
Content-Type: application/json

{
  "client_id": "kh_client_…",
  "redirect_uri": "https://app.acme.example/callback",
  "scope": "openid kwid email profile company:read",
  "state": "RANDOM",
  "code_challenge": "…",
  "code_challenge_method": "S256",
  "verification_identifier": "BOB.CARTER"
}
```

| Field | ¿Obligatorio? | What is |
| --- | --- | --- |
| verification_identifier | Yeah | The KWID/KYDid. Here **does not** go to `login_hint`. |
| client_id | Yeah | The ID of your app. |
| redirect_uri | Yeah | The same as step 1, exactly. |
| scope | Yeah | String with spaces, not array. |
| state | Yeah | Anti-CSRF value from step 1. |
| code_challenge | Yeah | PKCE from step 1. |
| code_challenge_method | Yeah | `S256`. |
| nonce | Optional | Recommended; validate it in the id_token. |

The response comes wrapped in `data`:

```
{
  "data": {
    "status": "pending_verification",
    "login_attempt_id": "login_attempt_…",
    "expires_at": "2026-06-19T15:30:00Z",  // TTL 15 min
    "challenge_prefix": "abc12345"     // solo diagnóstico
  }
}
```

## POST3 · Exchange the code for the token

**What it's for:** When KydHub returns you to the `redirect_uri` with a `code`, your **backend** exchanges it for the tokens. This is where the `client_secret` comes in.

```
POST https://dev.auth.kydhub.com/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "kh_client_…",
  "client_secret": "CLIENT_SECRET_DEL_BACKEND",
  "code": "CODE_DEL_CALLBACK",
  "redirect_uri": "https://app.acme.example/callback",
  "code_verifier": "EL_VERIFIER_ORIGINAL"
}
```

| Field | ¿Obligatorio? | What is |
| --- | --- | --- |
| grant_type | Yeah | Always `authorization_code`. |
| client_id | Yeah | The ID of your app. |
| client_secret | Yeah | Backend secret. Never on the client. |
| code | Yeah | The code that reached the callback (single use). |
| redirect_uri | Yeah | The same as step 1. |
| code_verifier | Yeah | The original PKCE (the one that generated the `code_challenge`). |

Response (raw OAuth object, no `data` wrapper):

```
{
  "access_token": "…",
  "id_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid kwid email profile company:read"
}
```

## 4 · Validate the token

Before trusting the `id_token`, verify its signature with JWKS and check the claims. Read the `jwks_uri` from discovery — don't hardcode it.

| Chequeo | What to validate |
| --- | --- |
| Signature | Check with the keys in `jwks_uri` (from discovery). |
| iss | Debe ser `https://dev.kydhub.com`. |
| aud | It should be your `client_id`. |
| exp | Not expired. |
| nonce | Same as the one you sent (if you used it). |

## GETUserInfo

**What is it for:** Bring the data of the logged in user. You call with the `access_token` as Bearer.

```
GET /userinfo
Authorization: Bearer ACCESS_TOKEN

{
  "sub": "person_01JDEVEXAMPLE",
  "kwid": "BOB.CARTER",
  "email": "bob.carter@example.test",
  "email_verified": true,
  "name": "Bob Carter",
  "given_name": "Bob",
  "family_name": "Carter",
  "picture": null
}
```

## Claims by scope

Each scope you order enables certain claims:

| Scope | Claims that returns |
| --- | --- |
| openid | `sub, iss, aud, exp, iat, auth_time` (and `nonce` if you sent it). |
| kwid | `kwid`. |
| email | `email, email_verified`. |
| profile | `name, given_name, family_name, picture` (subject to gap above). |

## OAuth errors: intermediate vs terminals

Intermediates are resolved following the flow; terminals require reboot.

#### ↻ Intermediates

- `verification_required` — initiate email verification.
- `pending_consent` / `consent_required` — wait for hosted consent.
- `login_required` — log in if there is no SSO.
#### Terminales

- `invalid_grant` — expired/used code, redirect or PKCE mismatch.
- `invalid_verification_identifier` — Invalid KWID.
- `fresh_authentication_required` — reboot with `prompt=login`.
- `access_denied` — user canceled.
## Quick Reference Data API

This reference brings together the canonical Data API capabilities. Use it as a quick index; for complete details, read each specific section.

**Contract rule:** Data API is credentialed and read-only. It does not create, edit or delete people, companies, addresses, API keys, OAuth clients or webhooks.

**Three-level authorization:** every request needs (1) an active company with a valid server-only `X-API-KEY`, (2) the required scopes, and (3) an active grant, consent, authorized relationship, or another explicit access basis for the data being returned. KydHub derives the requester from the server-only API key. **No open directory:** query company is for known authorized companies.

### Educational endpoints

| Endpoint | Capacidad | Identifieres | Required fields |
| --- | --- | --- | --- |
| QUERY /api/v1/persons/query | Consult person data. | `person_kyd` or `person_id`. | Yes, explicit list. |
| QUERY /api/v1/persons/query | Check person addresses. | `person_kyd` or `person_id`. | Yes, `addresses.*` fields. |
| QUERY /api/v1/companies/query | Query authorized company identity data. | Known `company_kyd` or supported company identifier. | `company.identity:read` plus valid access basis. |
| QUERY /api/v1/companies/query | Query authorized company profile fields. | Known `company_kyd` or supported company identifier. | `company.profile:read` plus valid access basis. |
| QUERY /api/v1/companies/query | Query authorized company locations. | Known `company_kyd` or supported company identifier. | `company.locations:read` plus valid access basis. |

### Base request

```
QUERY https://dev.api.kydhub.com/api/v1/persons/query
X-API-KEY: $KYDHUB_API_KEY
Content-Type: application/json

{
  "person_kyd": "BOB.CARTER",
  "service_name": "acme-checkout",
  "fields": ["full_name"]
}
```

### Base response

```
{
  "data": {
    "person_id": "per_7Q9M2K4R",
    "person_kyd": "BOB.CARTER",
    "fields": {
      "full_name": "Bob Carter"
    }
  }
}
```

### quick rules

| Regla | Detail |
| --- | --- |
| API key | Always in `X-API-KEY` and only from backend. |
| fields | Always explicit; Don't depend on broad defaults. |
| public IDs | Use `per_...` and `comp_...`; never internal UUIDs. |
| Company | Use `company_kyd` or `company_id`; there is no open search. |
| Errors | If scope/grant/field is missing, the API fails closed and does not expose data. |

## POSTWebhooks

Webhooks allow Acme to receive signed events from KydHub when something relevant occurs. They are optional: if your integration only needs to query data on demand, you can use the Data API without webhooks.

### General flow

| Step | What happens | Responsable |
| --- | --- | --- |
| 1 | Acme registers a receiving URL in the KydHub portal. | Acme |
| 2 | KydHub generates or displays the webhook signing secret. | KydHub |
| 3 | When an event occurs, KydHub sends a `POST` to the Acme backend. | KydHub |
| 4 | Acme validates the HMAC signature before processing the event. | Acme |
| 5 | Acme saves the `event_id` to avoid processing duplicates. | Acme |

### Event received by Acme

```
POST https://app.acme.example/kydhub/webhook
X-KydHub-Event-Id: evt_01HVEXAMPLE
X-KydHub-Timestamp: 1760000000
X-KydHub-Signature-256: sha256=HEX_HMAC
Content-Type: application/json

{
  "id": "evt_01HVEXAMPLE",
  "type": "grant.approved",
  "created_at": "2026-06-28T12:00:00Z",
  "data": {
    "person_kyd": "BOB.CARTER",
    "company_id": "comp_42P7K9M2",
    "scopes": ["person.identity:read", "person.addresses:read"]
  }
}
```

### Security headers

| Header | What does it mean | What Acme Should Do |
| --- | --- | --- |
| X-KydHub-Event-Id | Unique ID of the event. | Save it before processing so that retries are idempotent. |
| X-KydHub-Timestamp | Time KydHub signed/submitted the event. | Reject timestamps that are too old to reduce replays. |
| X-KydHub-Signature-256 | Signature HMAC-SHA256 of the raw bodysuit. | Recalculate the signature with the webhook secret and compare timing-safe. |
| Content-Type | Bodysuit format. | Wait for `application/json`. |

### Verify signature in Node.js

```
import crypto from 'node:crypto';

function verifyKydHubWebhook(rawBody, signatureHeader, webhookSecret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', webhookSecret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

### Typical events

| Event | When it happens | What should Acme do? |
| --- | --- | --- |
| grant.approved | Someone approved of Acme reading certain data. | Update internal state and enable Data API query. |
| grant.rejected | One person rejected the request. | Show alternative or ask for less data. |
| grant.revoked | An authorization was revoked. | Stop using data that depends on that grant. |
| webhook.delivery.failed | KydHub failed to deliver an event after attempts. | Check availability of the receiving endpoint. |

### Retries, idempotence and limits

| Tema | Regla | Recommendation |
| --- | --- | --- |
| Successful response | I responded `2xx` only after validating signature and saving the event. | Don't do slow work before responding; send to a queue if necessary. |
| Retries | KydHub may retry failed deliveries. | Process by `event_id` to avoid duplicates. |
| Company Free | 1 active webhook and 1000 delivery attempts per month. | Retries count against your monthly limit. |
| Security | The webhook secret lives only in backend/secret manager. | Do not put it in frontend, logs or public documentation. |

## Security, scopes, grants and limits

KydHub security combines server-only credentials, a complete requestable scope catalog, active authorizations, and usage limits. The base rule is simple: the Data API is read-only and returns only the specific scopes/fields that were requested and authorized.

### Main rules

#### Does

- Save API keys, OAuth secrets and webhook secrets only in backend or secret manager.
- I requested only the necessary fields in `fields`.
- Validate scopes and grants before depending on a response.
- Verify webhook signatures with HMAC and timing-safe comparison.
- Rotate credentials when a person on the team loses access.
- Show alternatives when authorization is missing or a field is not available.
#### don't do

- Do not put API keys in browsers, mobile apps, repositories or documentation.
- Don't put `client_secret` inside a distributed app.
- Do not use KydHub as an open business directory.
- Don't invent data if KydHub doesn't return a field.
- Don't ignore `grant_required` or `field_not_allowed`.
- Do not try to modify people, companies, addresses, API keys, OAuth clients or webhooks through the public Data API.
### Authorization model

| Capa | How valid | Example |
| --- | --- | --- |
| API key | What company is calling and if the key is active. | Acme calls with `X-API-KEY`. |
| Plan/limits | How many credentials, requests or deliveries the company can use. | Company Free allows 2 active API keys. |
| Scopes | What data families a credential can read. | `person.addresses:read`. |
| Grant | Which person/company authorized the reading and for which fields. | Bob Carter authorized reading of identity and addresses. |
| Fields | What exact attributes does the request ask for? | `["full_name", "addresses.city"]`. |

### Documented current scopes

### Scope catalog and user approval

KydHub publishes a complete, granular scope catalog. Pro and Enterprise companies may request any published scope; KydHub does not block a data category by plan. The user approves or rejects the exact person-data scopes requested. Plans limit operational capacity: request volume, rate limits, API keys, OAuth clients, webhooks, retention, support and SLA.

Typical examples:

| App | Typical requested scopes | Why |
|---|---|---|
| qbtChat | `person.identity:read`, `person.name:read`, `person.email:read`, `person.profile.avatar:read` | Login/profile display; no address needed. |
| Mercado Libre | `person.identity:read`, `person.name:read`, `person.identity.document:read`, `person.addresses.primary:read` | Identity and shipping verification after explicit user approval. |


| Scope | Allows you to read | Used in |
| --- | --- | --- |
| person.identity:read | Identifiers and basic name of person. | Consult person. |
| person.profile:read | Basic authorized person profile. | Consult person. |
| person.addresses:read | Registered/authorized addresses of person. | Addresses of person. |
| company.identity:read | Basic company identity. | Consult company. |
| company.profile:read | Authorized company profile. | Consult company. |
| company.locations:read | Registered business locations/addresses. | Company addresses. |
| webhooks.events:read | Webhook events if diagnostics are exposed. | Diagnosis/operation. |
| usage:read | Use and consumption of API if the contract enables it. | Diagnosis/operation. |
| audit:read | Allowed audit events. | Diagnosis/operation. |

### Company Free limits

| Area | Limit | Notes |
| --- | --- | --- |
| People per company | 2 total | 1 owner/founder and 1 collaborator. |
| API keys | 2 active | They may not expire; optional expiration. |
| API rate limit | 60 requests/min | Applied by company/API key according to operating contract. |
| API daily limit | 2,000 requests/day | Designed for pilots and initial integrations. |
| Webhooks | 1 active | 1000 delivery attempts/month; retries count. |
| OAuth / Apps | 1 connected app, 1 OAuth client | Up to 3 redirect URIs. |
| Audit logs | 7 days | Basic retention. |
| Support | Basic / best-effort | No contractual SLA. |

### Permitted and prohibited operations

| Tipo | Status | Explanation |
| --- | --- | --- |
| Read authorized person data | Allowed | With API key, catalog scope, active grant/consent/access basis and explicit fields. |
| Read authorized person addresses | Allowed | With address permission. |
| Read authorized company data | Allowed | With `company_kyd` or `company_id` known. |
| Read authorized company locations | Allowed | With `company.locations:read`. |
| Create/modify people by public API | Forbidden | The person modifies their data within KydHub. |
| Create/modify companies by public API | Forbidden | It is managed from the portal. |
| Manage API keys/OAuth/webhooks by public API | Forbidden | For now it is done from the portal. |
| Search for companies without an identifier | Forbidden | KydHub should not function as an open directory. |

### Safe response to lack of permission

```
{
  "error": "field_not_allowed",
  "message": "One or more requested fields are not allowed for this API key or grant.",
  "fields": ["addresses.line1"]
}
```

## Errors and troubleshooting

This section summarizes the most common mistakes when integrating KydHub. The general recommendation is not to invent data or continue as if the response was successful: each error indicates a specific action to correct credentials, permissions, identifiers or configuration.

### Recommended error format

```
{
  "error": "field_not_allowed",
  "message": "One or more requested fields are not allowed for this API key or grant.",
  "request_id": "req_01HVEXAMPLE",
  "fields": ["addresses.line1"]
}
```

| Atributo | What does it mean | How to use it |
| --- | --- | --- |
| error | Stable error code. | Use it for UI/backend logic. |
| message | Readable explanation. | Show it only if it does not expose internal data; You can map it to your own copy. |
| request_id | Correlation ID. | Include it when requesting support or reviewing logs. |
| fields | Affected fields, when applicable. | Useful for removing fields or requesting correct authorization. |

### API keys and authentication

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| missing_api_key | The `X-API-KEY` header did not arrive. | Send the API key from the Acme backend. Do not send it from a browser. |
| invalid_api_key | The key does not exist, was revoked, expired or was copied incorrectly. | Create or rotate a key from `Company → API Access`. |
| api_key_limit_reached | The company already has the maximum number of active API keys. | In Company Free, revoke an active key before creating another one. |
| rate_limit_exceeded | The limit of requests per minute was exceeded. | Reduce frequency, add backoff and review plan limits. |
| daily_limit_exceeded | The daily limit has been exceeded. | Wait for the window to restart or consult the upgrade/contract. |

### Identifiers and resources

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| person_not_found | There is no person for that `person_kyd` or `person_id`. | Verify the identifier. Don't try to guess identities. |
| company_not_found | There is no company for that `company_kyd` or `company_id`. | Confirm the identifier through authorized channel; KydHub is not an open directory. |
| invalid_identifier | The identifier is in invalid format. | Use `BOB.CARTER`, `per_...`, `company_kyd` or `comp_...` as appropriate. |
| address_not_available | No address of person available/authorized. | I asked for fewer fields or show the user an alternative. |
| location_not_available | No business location available/authorized. | Continue without location or request a less sensitive field. |

### Scopes, grants and fields

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| grant_required | There is no active authorization to read that data. | Start the authorization flow or request fields that do not require that grant. |
| grant_revoked | The authorization existed but was revoked. | Stop using that data and request new authorization if applicable. |
| field_not_allowed | One or more fields are not allowed by scope/grant. | Remove fields, request less data or request appropriate permissions. |
| scope_required | The API key does not have the necessary scope. | Request/enable the exact scope. The user decides person-data authorization; plans limit volume/capacity. |
| company_scope_required | Missing company read scope. | Use scopes like `company.identity:read` or `company.profile:read`. |
| company_locations_scope_required | Permission missing for company locations. | Check if the key has `company.locations:read`. |

### OAuth/Login with KWID

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| login_required | The user does not have an active session. | Show the verification flow or redirect to KydHub. |
| verification_required | You must complete verification by email/magic link. | Don't treat it as a fatal flaw; show him “Check your email”. |
| invalid_redirect_uri | The callback URL does not exactly match the one registered. | Registrá `https://app.acme.example/callback` o la URL real exacta. |
| invalid_client | Incorrect `client_id` or secret. | Check the OAuth client in the portal. Don't put secrets in public apps. |
| invalid_grant | The `code` has expired, has already been used, or does not correspond to the verifier. | Restart login and validate PKCE/state. |
| invalid_token | Invalid, expired or incorrectly signed token. | Validate issuer, audience, expiration and JWKS. |

### Webhooks

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| webhook_signature_invalid | The HMAC signature does not match. | Use the exact raw body and the correct secret; compare timing-safe. |
| webhook_timestamp_expired | The timestamp is too old. | Decline the event and check clock/sync. |
| webhook_duplicate_event | The `event_id` has already been processed. | Don't reprocess it; I returned 2xx if it was already persisted. |
| webhook_delivery_failed | KydHub was unable to deliver the event. | Check public availability, TLS and 2xx response of the endpoint. |
| webhook_limit_reached | The limit of active webhooks or deliveries has been reached. | In Company Free there is 1 active webhook and 1000 attempts/month. |

### Optional post-quantum encryption

| Error | What does it mean | How to solve it |
| --- | --- | --- |
| pq_encryption_not_enabled | The layer is not enabled for the enterprise. | Use normal Data API or request contractual enablement. |
| unknown_key_id | The `kid` does not exist or was rotated. | Register/obtain the current public key. |
| decrypt_failed | AEAD could not be decrypted or failed. | Do not process content; retry with valid password. |
| unsupported_algorithm | The cryptographic suite is not supported. | Negotiate a supported suite or disable this layer. |

## Agent-readable/endpoint array

This section summarizes the KydHub integration in a compact format for agents, technical teams, and internal documentation. It does not replace the previous sections: it serves as a quick map to implement without losing security rules.

### Operational summary

| Elemento | Valor | Nota |
| --- | --- | --- |
| sandbox/dev environment | `https://dev.api.kydhub.com/api/v1` | Used for testing and current documentation. |
| Auth Data API | `X-API-KEY` | Backend/secret manager only. Never browser. |
| Example company | `Acme` | Fictitious integration company. |
| Example person | `Bob Carter / BOB.CARTER` | Fictional person for examples. |
| Company consulted example | `ACME.SUPPLIER` | Fictitious business KYD. |
| API model | Read-only | Does not create, modify or delete data via public API. |

### Available capabilities matrix

| Capacidad | Educational endpoint | Identifieres | Typical scopes |
| --- | --- | --- | --- |
| Query a person | `QUERY /api/v1/persons/query` | `person_kyd` or `person_id` | `person.identity:read`, `person.profile:read` |
| Check person addresses | `QUERY /api/v1/persons/query` | `person_kyd` or `person_id` | `person.addresses:read` |
| Query company identity | `QUERY /api/v1/companies/query` | Known `company_kyd` or supported company identifier | `company.identity:read` |
| Query company profile | `QUERY /api/v1/companies/query` | Known `company_kyd` or supported company identifier | `company.profile:read` |
| Query company locations | `QUERY /api/v1/companies/query` | Known `company_kyd` or supported company identifier | `company.locations:read` |
| Login with KWID | `GET /oauth/authorize` | `client_id`, `redirect_uri` | `openid`, `kwid`, `email`, `profile` |
| Webhooks | `POST https://app.acme.example/kydhub/webhook` | `event_id` | HMAC signature + contracted events. |

### JSON for agents

```
{
  "product": "KydHub",
  "environment": "sandbox",
  "base_url": "https://dev.api.kydhub.com/api/v1",
  "auth": {
    "data_api": {
      "type": "api_key",
      "header": "X-API-KEY",
      "storage": "backend_or_secret_manager_only"
    },
    "oauth_oidc": {
      "optional": true,
      "issuer": "https://dev.kydhub.com",
      "flow": "authorization_code_pkce"
    }
  },
  "read_only_public_api": true,
  "examples": {
    "integrator_company": "Acme",
    "person_kyd": "BOB.CARTER",
    "company_kyd": "ACME.SUPPLIER",
    "person_id": "per_7Q9M2K4R",
    "company_id": "comp_42P7K9M2"
  }
}
```

### Permitted operations

| Operation | Permitida | Condition |
| --- | --- | --- |
| Read person authorized fields | Yeah | Valid API key + catalog scope + grant/consent/access basis + explicit fields. |
| Read authorized person addresses | Yeah | `person.addresses:read` and active authorization. |
| Read company authorized fields | Yeah | `company_kyd` or `company_id` known. |
| Read authorized company locations | Yeah | `company.locations:read`. |
| Use Login with KWID/OIDC | Optional | Only if Acme decides to delegate login/verification. |
| Receive signed webhooks | Optional | Webhook registered in portal + HMAC secret. |

### Prohibited operations for agents

| Operation | Status | Regla |
| --- | --- | --- |
| Modify people data by public API | No | The person manages their data within KydHub. |
| Modify companies by public API | No | The company is managed from the portal. |
| Create API keys by public API | No | API keys are created from the portal. |
| Create OAuth clients by public API | No | OAuth clients are managed from the portal. |
| Search for companies without an identifier | No | KydHub is not an open directory. |
| Expose secrets in code or logs | Nunca | Use environment variables or secret manager. |

### Checklist to implement

| # | Validation | Expected result |
| --- | --- | --- |
| 1 | Create API key in portal. | Secret saved in backend/secret manager. |
| 2 | Make minimum person request. | Response contains only requested/authorized fields. |
| 3 | Test missing scope/field not allowed. | Safe error type `field_not_allowed`. |
| 4 | If there is OAuth, validate `state`, `nonce` and JWKS. | Secure login and verified tokens. |
| 5 | If there are webhooks, validate HMAC and idempotency. | Signed events and no duplicate processing. |
| 6 | Document expected errors in the integration. | UX with alternatives and without inventing data. |

## Optional post-quantum encryption

KydHub can offer an optional post-quantum hybrid encryption layer for especially sensitive payloads. This layer protects the message body in addition to HTTPS/TLS, but does not replace authentication, authorization, scopes, grants, auditing, or signatures.

### When to use it

| Situation | Recommendation | Motivo |
| --- | --- | --- |
| Standard Data API Integration | It is not mandatory. | TLS, API key, scopes and grants are already the basis of security. |
| Payloads with sensitive PII | It can be activated if the contract enables it. | Adds body confidentiality in addition to the TLS channel. |
| Webhooks with sensitive data | Can be used in conjunction with HMAC signature. | The signature verifies integrity/origin; Encryption protects content. |
| Client without cryptographic support | Don't activate it yet. | Integrate normal Data API first and then add this layer. |

### What does not replace

| Control | It is still mandatory | Because |
| --- | --- | --- |
| TLS/HTTPS | Yeah | Protects the transport channel. |
| API key | Yeah | Identify the company that is calling. |
| OAuth/OIDC | Yes, if you use Login with KWID | Authenticates users and issues claims/tokens. |
| Scopes and grants | Yeah | They decide what data can be read. |
| Webhook signing | Yeah | Verify origin and integrity of the event. |
| Audit | Yeah | Allows you to track who ordered what and when. |

### How it works at a high level

| Step | What's happening | Result |
| --- | --- | --- |
| 1 Public key | The recipient publishes or registers a public encryption key. | The sender knows which key to protect the payload with. |
| 2 · Post-quantum KEM | The issuer encapsulates a secret using ML-KEM/Kyber. | A shared secret resistant to the post-quantum model is obtained. |
| 3 · Classic ECDH | An ephemeral secret X25519 is also generated. | Hybrid classical + post-quantum defense. |
| 4 Derivation | Both secrets enter the HKDF. | A symmetric key is derived for encryption. |
| 5 · AEAD | The payload is encrypted with AES-256-GCM or another approved AEAD. | Confidentiality and integrity of the encrypted body. |

### Encrypted Wrapper Example

```
{
  "protected_payload": {
    "alg": "KydHub-HybridPQ-MLKEM768-X25519-HKDF-AES256GCM",
    "kid": "kh_pq_key_2026_06",
    "kem_ciphertext": "BASE64URL_ML_KEM_CT",
    "ecdh_public_key": "BASE64URL_X25519_PUB",
    "nonce": "BASE64URL_96BIT",
    "ciphertext": "BASE64URL_AEAD_CT",
    "aad": "method=QUERY;path=/api/v1/persons/query;event=grant.approved"
  }
}
```

### Wrapper attributes

| Atributo | What does it mean | Regla |
| --- | --- | --- |
| alg | Cryptographic algorithm/package used. | Must be agreed upon by KydHub and Acme. |
| kid | ID of the public key used. | Allows you to rotate keys without breaking integrations. |
| kem_ciphertext | Post-quantum KEM ciphertext. | Used by the recipient to retrieve the shared secret. |
| ecdh_public_key | Ephemeral public key X25519 of the issuer. | Participate in hybrid bypass. |
| nonce | Nonce used by AEAD encryption. | It should not be repeated for the same key. |
| ciphertext | Encrypted payload. | Contains the actual protected data. |
| aad | Unencrypted authenticated data. | Link the payload with method, path, event or context. |

### Errors and fallback

| Error | What does it mean | To do |
| --- | --- | --- |
| pq_encryption_not_enabled | The company does not have this layer enabled. | Use normal Data API or request enablement. |
| unknown_key_id | The `kid` does not exist or was rotated. | Get/register the current public key. |
| decrypt_failed | The payload could not be decrypted or AEAD authentication failed. | Do not process the content; Log the error and retry with a valid key. |
| unsupported_algorithm | The algorithm is not supported by one of the parties. | Negotiate a supported suite or disable this layer. |


## Data API query verb rule

Use `GET` for simple resource reads with one path identifier. Use `QUERY /query` for safe, idempotent read-only queries that need structured JSON content. Canonical query endpoints:

- `QUERY /api/v1/persons/query`
- `QUERY /api/v1/companies/query`

Simple company resource reads use GET with a direct identifier:

- `GET /api/v1/companies/{company_identifier}`
- `GET /api/v1/companies/{company_identifier}/profile`
- `GET /api/v1/companies/{company_identifier}/locations`

Company query response uses `company_id`, `company_kyd`, `service_name`, `response_profile_used`, and `fields.identity/profile/locations`; it is not wrapped in a `data` container for the canonical Data API query response.
