Knowledge Hub API

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:

MethodPath
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.

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 and OAuth 2.0.
  • 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.

ParameterTypeRequiredDescription
country_codestringYesISO 3166-1 alpha-2 country code, for example BR

A request for the Brazilian guide, narrowed to employers using EOR, looks like this:

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 parameterWhat it does
audiencesReturns only the audiences you name, such as client or contractor. Applies to any request
contract_typesReturns 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.

# 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.

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:

ValueApplies to
clientThe employing organization. The only audience that also branches by contract type
eorEmployeeA worker employed through Deel Employer of Record
gpEmployeeA worker paid through Deel Global Payroll
hrisEmployeeA worker managed in Deel HR
peoEmployeeA worker employed through Deel PEO
contractorAn independent contractor

Contract type values

Pass any of these values in contract_types. They apply only alongside audiences=client:

ValueProduct
eorEmployer of Record
gpGlobal Payroll
icContractor Management
peoPEO
uspUS Payroll
deel_hrDeel HR
deel_engageDeel Engage
deel_itDeel IT
deel_servicesDeel Services
equity_servicesEquity Services
immigrationImmigration

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:

FieldTypeDescription
dataarrayAudience objects
has_morebooleanIndicates whether more items are available
next_cursorstringCursor for the next page of results. Nullable
total_countnumberTotal 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:

FieldTypeRequiredDescription
namestringYesDisplay name of the audience, for example Contractor
valuestringYesLowercase identifier for the audience, for example contractor
categoriesarrayNoCategories applicable to this audience
contract_typesarrayNoContract types available for this audience

Contract type object

Each entry in contract_types describes one Deel product an employer uses:

FieldTypeRequiredDescription
namestringYesDisplay name of the contract type, for example Employer of Record
valuestringYesLowercase identifier for the contract type, for example eor
categoriesarrayYesCategories applicable to this contract type

Category object

Each entry in categories groups the fields and articles for one topic:

FieldTypeRequiredDescription
idstringYesIdentifier for the category
namestringYesDisplay name of the category, for example Wages and Working Hours
iconstringYesIcon aligning with the category
fieldsarrayYesInformative fields and their values
articlesarrayYesArticles connected to this category

Field object

Each entry in fields carries one piece of renderable guide content:

FieldTypeRequiredDescription
idstringYesIdentifier of the field
namestringYesTitle of the field, for example EOR Minimum Wage
typestringYesOne of TEXT, LINK, or HTML
valuestringNoThe value associated with the field. Nullable

Article object

Metadata only; see What the API does not provide.

FieldTypeRequiredDescription
idstringYesIdentifier for the article
titlestringYesTitle of the article
descriptionstringYesAn excerpt for the article
typestringYesOne of KNOWLEDGE or COMPLIANCE
featured_imagestringYesURI of the featured image
tagsarrayYesTag objects, each with id, tag, and title
created_atstringYesTimestamp when the article was created
updated_atstringYesTimestamp when the article was last updated
effective_datestringNoDate when the article content becomes effective. Nullable

Example response

A request filtered to the eorEmployee audience returns a payload of this shape:

{
"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": "<p>Deductions are applied monthly.</p>"
}
],
"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.

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:

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

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.

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", [])

Error handling

The endpoint returns the following status codes.

StatusMeaningAction
400The request is invalid, for example a missing or malformed country codeSupply a valid ISO 3166-1 alpha-2 code
401The token is missing, expired, or unauthenticatedRe-authenticate and confirm the token carries the knowledge-hub:read scope
403The token is authenticated but lacks permission for this resourceConfirm the required scope is granted to your application
404No country matches the supplied codeCheck the code. Not every country has a published guide
500The request failed on the Deel sideRetry 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:

{
"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:

{
"request": {
"method": "GET",
"url": "/rest/knowledge-hub/country-guide/BR",
"status": 401,
"api_req_id": "00000000-0000-0000-0000-000000000000"
},
"errors": [
{
"message": "Unauthorized"
}
]
}

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.

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:

  • 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.
  • Respect 429 responses with exponential backoff. See Rate limits.
  • Pin requests with the X-Version header and follow the Deel API changelog for changes.

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: