> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.deel.wtf/api/stable/partners/knowledge-hub-api/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.deel.wtf/_mcp/server. # Knowledge Hub API > Retrieve country guide content segmented by audience and Deel product for use in partner integrations The Knowledge Hub API returns country-specific content, such as employment rules, rates, and how local requirements apply. A single endpoint serves this content. Partner integrations typically use it to show a country reference in their own hiring or HR workflow. This guide covers the request shape, the response structure, and the rendering rules required to display the content correctly. ## What the API provides One endpoint serves the entire guide: | Method | Path | | ------ | --------------------------------------------- | | `GET` | `/knowledge-hub/country-guide/{country_code}` | Everything you render comes from the `fields` array on each category. Fields carry plain text and rich HTML, and hold the substantive guide content, such as a tax rate, a notice period, or an explanation of how deductions work. ### What the API does not provide Before designing an integration with the Knowledge Hub API please note the following: * **Article bodies**: The endpoint does not return the full Knowledge Hub article content. * **A browsable article list**: Category payloads include an `articles` array, but it holds metadata only. There is no body and no URL, so entries cannot be opened or linked to. > **Note** > > If your product requires article content, contact your Deel representative. ## Prerequisites Three things are required before your first call: * **A Deel access token**: Either an OAuth 2.0 token or a personal access token. See [Authentication](/api/authentication) and [OAuth 2.0](/api/oauth). * **The `knowledge-hub:read` scope**: OAuth 2.0 tokens must carry this scope. * **A country code**: The endpoint requires an ISO 3166-1 alpha-2 code. ## Make a request The `country_code` path parameter is the only parameter defined in the API reference. | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ------------------------------------------------- | | `country_code` | `string` | Yes | ISO 3166-1 alpha-2 country code, for example `BR` | A request for the Brazilian guide, narrowed to employers using EOR, looks like this: **`cURL`** ```bash cURL curl -G "https://api.letsdeel.com/rest/knowledge-hub/country-guide/BR" \ -H "Authorization: Bearer $DEEL_ACCESS_TOKEN" \ -H "X-Version: 2026-01-01" \ --data-urlencode "audiences=client" \ --data-urlencode "contract_types=eor" ``` It is important to apply a request filter. If no filter is applied, all results will be returned: all guides for all audiences and all products for all employers. To avoid large response payloads, make an unfiltered request to inspect the response shape, then apply filters to refine. ## Filter the response Two optional query parameters narrow what the endpoint returns. Omitting one returns every value for it. | Query parameter | What it does | | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | `audiences` | Returns only the audiences you name, such as `client` or `contractor`. Applies to any request | | `contract_types` | Returns only the Deel products you name within the `client` audience, such as `eor`. Ignored for worker audiences | ### Passing multiple values To submit multiple values, repeat the parameter for each value, as shown below. Comma-separated values will be treated as a single unrecognized value. ```bash # Correct ?audiences=client&audiences=eorEmployee # Incorrect: silently returns every audience ?audiences=client,eorEmployee ``` ### Invalid filter values A value the endpoint does not recognize is ignored rather than returning an error. If every value you send is invalid, the filter empties and defaults to returning everything. > **Note** > > If a response is substantially larger than you expect, check your filter values for typographical errors. ### Contract type scope Contract types describe which Deel products an employer uses, so they carry no meaning for worker audiences and are ignored there. A request for `audiences=eorEmployee&contract_types=gp` returns the complete `eorEmployee` guide, unfiltered. ### Audience values Pass any of these values in `audiences`: | Value | Applies to | | -------------- | --------------------------------------------------------------------------------- | | `client` | The employing organization. The only audience that also branches by contract type | | `eorEmployee` | A worker employed through Deel Employer of Record | | `gpEmployee` | A worker paid through Deel Global Payroll | | `hrisEmployee` | A worker managed in Deel HR | | `peoEmployee` | A worker employed through Deel PEO | | `contractor` | An independent contractor | ### Contract type values Pass any of these values in `contract_types`. They apply only alongside `audiences=client`: | Value | Product | | ----------------- | --------------------- | | `eor` | Employer of Record | | `gp` | Global Payroll | | `ic` | Contractor Management | | `peo` | PEO | | `usp` | US Payroll | | `deel_hr` | Deel HR | | `deel_engage` | Deel Engage | | `deel_it` | Deel IT | | `deel_services` | Deel Services | | `equity_services` | Equity Services | | `immigration` | Immigration | This list grows as Deel adds products. Don't treat it as a fixed enum; tolerate unknown values instead of failing on them. ## Response structure The response nests audiences, categories, and fields as follows: ``` data[] audience ├─ contract_types[] present when value == "client" │ └─ categories[] └─ categories[] every other audience ├─ fields[] content to render └─ articles[] metadata only ``` The `client` audience nests categories one level deeper than other audiences. Handle both shapes. ### Envelope The top-level object carries the audience array alongside pagination metadata: | Field | Type | Description | | ------------- | --------- | --------------------------------------------- | | `data` | `array` | Audience objects | | `has_more` | `boolean` | Indicates whether more items are available | | `next_cursor` | `string` | Cursor for the next page of results. Nullable | | `total_count` | `number` | Total number of audiences | A guide is currently returned complete in a single response, so `has_more` is `false` and `next_cursor` is `null`. These fields exist to introduce pagination without a breaking change. ### Audience object Each entry in `data` describes one audience: | Field | Type | Required | Description | | ---------------- | -------- | -------- | --------------------------------------------------------------- | | `name` | `string` | Yes | Display name of the audience, for example `Contractor` | | `value` | `string` | Yes | Lowercase identifier for the audience, for example `contractor` | | `categories` | `array` | No | Categories applicable to this audience | | `contract_types` | `array` | No | Contract types available for this audience | ### Contract type object Each entry in `contract_types` describes one Deel product an employer uses: | Field | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------- | | `name` | `string` | Yes | Display name of the contract type, for example `Employer of Record` | | `value` | `string` | Yes | Lowercase identifier for the contract type, for example `eor` | | `categories` | `array` | Yes | Categories applicable to this contract type | ### Category object Each entry in `categories` groups the fields and articles for one topic: | Field | Type | Required | Description | | ---------- | -------- | -------- | ------------------------------------------------------------------- | | `id` | `string` | Yes | Identifier for the category | | `name` | `string` | Yes | Display name of the category, for example `Wages and Working Hours` | | `icon` | `string` | Yes | Icon aligning with the category | | `fields` | `array` | Yes | Informative fields and their values | | `articles` | `array` | Yes | Articles connected to this category | ### Field object Each entry in `fields` carries one piece of renderable guide content: | Field | Type | Required | Description | | ------- | -------- | -------- | -------------------------------------------------- | | `id` | `string` | Yes | Identifier of the field | | `name` | `string` | Yes | Title of the field, for example `EOR Minimum Wage` | | `type` | `string` | Yes | One of `TEXT`, `LINK`, or `HTML` | | `value` | `string` | No | The value associated with the field. Nullable | ### Article object Metadata only; see [What the API does not provide](#what-the-api-does-not-provide). | Field | Type | Required | Description | | ---------------- | -------- | -------- | --------------------------------------------------------- | | `id` | `string` | Yes | Identifier for the article | | `title` | `string` | Yes | Title of the article | | `description` | `string` | Yes | An excerpt for the article | | `type` | `string` | Yes | One of `KNOWLEDGE` or `COMPLIANCE` | | `featured_image` | `string` | Yes | URI of the featured image | | `tags` | `array` | Yes | Tag objects, each with `id`, `tag`, and `title` | | `created_at` | `string` | Yes | Timestamp when the article was created | | `updated_at` | `string` | Yes | Timestamp when the article was last updated | | `effective_date` | `string` | No | Date when the article content becomes effective. Nullable | ### Example response A request filtered to the `eorEmployee` audience returns a payload of this shape: ```json { "data": [ { "name": "EOR Employee", "value": "eorEmployee", "categories": [ { "id": "a3a9e1ce-7afb-40e2-a171-81ad2eec39ff", "name": "Wages and Working Hours", "icon": "MoreTime", "fields": [ { "id": "b3a9e1ce-7afb-41d3-a171-81ad2eec39ff", "name": "EOR Minimum Wage", "type": "TEXT", "value": "ALL 40,000 per month" }, { "id": "c4b8f2df-8bfc-42e4-b282-92be3ffd4a11", "name": "How deductions work", "type": "HTML", "value": "
Deductions are applied monthly.
" } ], "articles": [] } ] } ], "has_more": false, "next_cursor": null, "total_count": 1 } ``` ### Conventions Three conventions hold across the whole response: * **Keys are `snake_case`** throughout the response. * **Enumerated `type` values are uppercase**. Compare case-sensitively against uppercase values, or normalize on input. * **Identifiers are stable UUIDs**. You can store them as foreign keys and rely on them across requests. ## Render the fields Everything you display is a field. Branch on `type` and preserve the order in which fields arrive, because it reflects the order Deel intends for display. > **Warning** > > The API reference defines the field object as `id`, `name`, `type`, and `value` only. Observed `LINK` fields additionally carry `label`, `url`, and `new_tab` in place of `value`. This shape is not yet reflected in the reference. Read `value` defensively and confirm the current shape with your Deel representative. A renderer for the three current field types looks like this: **`Python`** ```python Python import html def render_field(field): field_type = field["type"] if field_type == "TEXT": return {"label": field["name"], "body": html.escape(field.get("value") or "")} if field_type == "HTML": return {"label": field["name"], "body": sanitize(field.get("value") or "")} if field_type == "LINK": return { "label": field["name"], "href": field.get("url"), "text": field.get("label"), "target": "_blank" if field.get("new_tab") else "_self", } # Skip unrecognized types to stay forward compatible return None ``` **`Node.js`** ```javascript Node.js function renderField(field) { switch (field.type) { case 'TEXT': return { label: field.name, body: escapeHtml(field.value ?? '') }; case 'HTML': return { label: field.name, body: sanitize(field.value ?? '') }; case 'LINK': return { label: field.name, href: field.url, text: field.label, target: field.new_tab ? '_blank' : '_self', }; default: // Skip unrecognized types to stay forward compatible return null; } } ``` Four rules govern correct rendering: * **Sanitize `HTML` values before rendering them.** Pass the fragment through a sanitizer such as DOMPurify rather than assigning it directly to `dangerouslySetInnerHTML` or `v-html`. * **Treat `value` as nullable.** The schema permits a null value on any field type. * **Skip types you do not recognize.** New field types may be introduced. A default branch that returns nothing keeps your integration working when that happens. * **Skip categories with no renderable fields.** A category qualifies for inclusion on either fields or articles, so `fields` can arrive empty. ### Handle empty categories Filter out categories with no renderable fields before display; otherwise, they render as a heading with nothing beneath it. **`Python`** ```python Python def renderable_categories(categories): result = [] for category in categories: fields = [f for f in (render_field(field) for field in category["fields"]) if f] if fields: result.append({**category, "fields": fields}) return result # The client audience nests categories one level deeper def extract_categories(audience): if audience["value"] == "client": return [ category for contract_type in audience.get("contract_types", []) for category in contract_type["categories"] ] return audience.get("categories", []) ``` **`Node.js`** ```javascript Node.js const renderableCategories = (categories) => categories .map((category) => ({ ...category, fields: category.fields.map(renderField).filter(Boolean), })) .filter((category) => category.fields.length > 0); // The client audience nests categories one level deeper function extractCategories(audience) { if (audience.value === 'client') { return (audience.contract_types ?? []).flatMap((contractType) => contractType.categories); } return audience.categories ?? []; } ``` ## Error handling The endpoint returns the following status codes. | Status | Meaning | Action | | ------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `400` | The request is invalid, for example a missing or malformed country code | Supply a valid ISO 3166-1 alpha-2 code | | `401` | The token is missing, expired, or unauthenticated | Re-authenticate and confirm the token carries the `knowledge-hub:read` scope | | `403` | The token is authenticated but lacks permission for this resource | Confirm the required scope is granted to your application | | `404` | No country matches the supplied code | Check the code. Not every country has a published guide | | `500` | The request failed on the Deel side | Retry with exponential backoff. Contact Deel if the failure persists | Errors for `400` and `500` return an `errors` array whose entries carry `code`, `message`, and, for validation failures, a `field` naming the parameter at fault: ```json { "errors": [ { "code": "invalid_parameter", "field": "/data/country_code", "message": "Must have required property 'country_code'" } ] } ``` Errors for `401`, `403`, and `404` return the shared Deel error envelope, which carries a `request` object alongside an `errors` array whose entries provide `message` and `path`: ```json { "request": { "method": "GET", "url": "/rest/knowledge-hub/country-guide/BR", "status": 401, "api_req_id": "00000000-0000-0000-0000-000000000000" }, "errors": [ { "message": "Unauthorized" } ] } ``` > **Note** > > Where a machine-readable `code` is present, branch on it rather than on `message`. Message wording is subject to change. Empty responses A `200` response carrying `"data": []` is valid. The country exists, but no guide is published for the audiences you requested. Treat it as an empty state. The same applies per audience. A request for three audiences may return one if only one is published. > **Warning** > > Do not assume `data` mirrors your request, and do not index into it positionally. Match entries on `value`. ## Best practices The following practices keep an integration stable as guide content and the API change: #### Caching and refresh * **No webhooks are available** for this content, and there is no way to request only what changed. Each refresh is a full fetch for a country. * **Guides change on the order of weeks**, not hours. A daily refresh is sufficient for most integrations. Hourly polling provides no benefit. * **Cache the full response per country** and serve your users from your cache. #### Rate limits and versioning * Respect `429` responses with exponential backoff. See [Rate limits](/api/rate-limits). * Pin requests with the `X-Version` header and follow the Deel API changelog for changes. #### Configuration Two things are not discoverable at runtime, so plan to configure them: * **Which countries have published guides.** Request the current list from your Deel representative instead of discovering it by probing codes and collecting 404 responses. * **Which audiences and contract types apply to your integration.** Use the reference tables above and confirm the relevant values with your Deel representative. ## Next steps Continue with the endpoint schema and the related guides: #### [Knowledge Hub reference](/api/reference/endpoints/knowledge-hub) Review the full endpoint schema in the API reference #### [Getting started](/api/partners/getting-started) Build your first OAuth 2.0 app with Deel #### [Authentication](/api/authentication) Learn how to authenticate API requests #### [API versioning](/api/api-versioning) Pin your integration to a specific API version > Build apps and integrations that extend and enhance the Deel services.