Product that suits modern B2B Tech companies

Book Demo
B
Book demo call-to-action illustration
BACK
B

The three ways to reach a field your unified API doesn't have

Technical Guides
August 27, 2026
Summarise the blog with AI
Open in ChatGPT
Ask questions about this page
Open in Claude
Ask questions about this page

Key takeaways

  • A missing field usually means normalization dropped it by design. It is not a sign the unified API failed.
  • Check the raw payload first. GET /api/v1/custom-fields/raw-data is read-only, costs nothing, and tells you whether the field is even in the data you already pull.
  • Custom fields are for when you want one specific value, clean, on your own schema, in the same place for every account.
  • Raw data is for when you want to see the whole response the provider actually sent, in the provider's own shape.
  • Passthrough is for a field on an endpoint the sync never calls. It is also the only one of the three that reads live from the provider rather than from the last sync.
  • Preview your JMESPath before you save it. An expression that doesn't resolve returns the literal string INVALID_JSON_PATH in the response instead of raising an error.
  • Bindbee handles authentication on passthrough requests, so calling a provider's own endpoint doesn't mean managing that provider's credentials yourself.

Why the field is missing, and why that's expected

A unified API promises one schema for every HRIS, payroll, ATS, or benefits system you connect. Then an implementation engineer hits the field that schema doesn't have: a working-location attribute only some employers track, a plan detail buried on an endpoint nobody's called yet, a value that depends entirely on how one HRIS shapes its own records. Fork the integration for that one customer, and you're maintaining a branch per client by the time you have real volume. Assume the unified API failed, and you've drawn the wrong conclusion.

A missing field means normalization did its job. There are three documented mechanisms for reaching a field the shared schema doesn't carry: map it as a custom field, read the raw payload normalization is built from, or call the provider directly through passthrough. Each reaches a different kind of gap, and picking the wrong one is what turns a two-line fix into a maintenance problem.

Even a schema built for depth normalizes past the edge cases. Bindbee's unified API spans 40+ models across HRIS, payroll, ATS, and LMS, on the benefits side alone that covers employee benefits, employer benefits, benefit coverages, dependents, and dependent benefits, with plan category, coverage tier, employee and company contributions, and effective, start, and end dates on the enrollment record itself. That coverage still stops at the fields most employers share. A field specific to one employer, or to one HRIS's own quirk, was never going to make a shared schema no matter how many models it has.

The short version: the order to work in

The three mechanisms aren't alternatives you weigh in parallel. They're a sequence, and working them in order saves most of the debugging:

  1. Look at the raw payload first. It's read-only, it costs nothing, and it answers the only question that matters at this stage: is the field even in the data your connector already pulls?
  2. If it's there, map a custom field. One JMESPath expression puts that value onto your own schema attribute.
  3. If it isn't there at all, use passthrough. The field lives on an endpoint the sync never calls, and no amount of parsing reaches a payload that was never requested.

The rest of this page is that sequence with the actual requests.

Custom fields: put one specific value on your own schema

Custom fields let you map a field you can already see in a provider's payload onto an attribute of your own, using JMESPath, a query language for pulling values out of a JSON document.

Say the field is a cost-center code. One HRIS returns it as employee.customFields.costCenter; another nests it three levels deeper as employee.attributes.finance.cost_center. A JMESPath expression scoped to each connector picks the value out of wherever that provider put it and writes it onto your own field, say custom_fields.cost_center. Two providers, two paths, one attribute on your side.

Scope is what makes this scale. A mapping is either organization-scoped, which applies to every connector of that integration in your org, or connector-scoped, which applies to one account and overrides the org-level mapping. So the field lands in the same place on your schema for every customer running that HRIS, and you only write a per-account override where an employer's instance is genuinely different.

Step 1: look at the raw payload before you write anything

Before you write a JMESPath, look at the shape of the upstream JSON instead of guessing at a path. Bindbee's raw-data endpoint is built for exactly this: it returns the connector's latest synced row for one (category, model) pair, and falls back to a canned sample for the integration if that connector has never synced.

GET /api/v1/custom-fields/raw-data?category=HRIS&model=employee&connector_token=<CONNECTOR_TOKEN>
Authorization: Bearer <YOUR_API_KEY>

{
  "data": {
    "employee": {
      "id": "emp_123",
      "first_name": "Ada",
      "cost_center": "ENG-NA-004"
    }
  }
}

That tells you the expression you want is data.employee.cost_center. This endpoint is a discovery tool, it is step two of the custom-fields workflow, not a way to read a field in production. The production path for raw payloads is covered further down.

Step 2: preview the expression before you save it

The preview endpoint dry-runs a JMESPath against the connector's latest synced row. or against an inline payload you supply, without persisting anything. Bindbee's own docs list it as step three of the workflow and call it recommended. The dashboard has a preview button that does the same thing.

POST /api/v1/custom-fields/preview
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json


{
  "connector_token": "<CONNECTOR_TOKEN>",
  "category": "HRIS",
  "model": "employee",
  "json_path": "data.employee.cost_center"
}

{
  "connector_token": "<CONNECTOR_TOKEN>",
  "json_path": "data.employee.cost_center",
  "resolved_value": "ENG-NA-004",
  "resolved_value_type": "string",
  "raw_data_source": "connector_sync"
}

Three things in that response earn their place. resolved_value is what the expression actually returned. resolved_value_type tells you whether you're getting a string, number, boolean, object, array, or null, which matters, because the same field can come back as a string on one connector and a number on the next. And raw_data_source tells you whether the preview hit real sync data (connector_sync), fell back to the canned integration sample because the connector has never synced (integration_sample), or evaluated an inline payload you passed in (inline). A green preview against integration_sample is not the same evidence as a green preview against connector_sync.

Step 3: create the field, then the mapping

POST /api/v1/custom-fields
{
  "name": "cost_center",
  "description": "Employee cost-center code",
  "category": "HRIS",
  "model": "employee"
}

POST /api/v1/custom-fields/mapping
{
  "custom_field_id": "018e586b-7d0b-7bc9-be65-a8fdbc82d734",
  "integration_slug": "workday",
  "json_path": "data.employee.cost_center"
}

Provide exactly one of integration_slug (org scope) or connector_token (connector scope). Only json_path is mutable afterwards, to change a mapping's scope, delete it and create a new one.

Step 4: read it back

GET /api/hris/v1/employees?include_custom_fields=true

{
  "id": "emp_123",
  "first_name": "Ada",
  "custom_fields": {
    "cost_center": "ENG-NA-004"
  }
}

What happens if you skip preview

If a JMESPath expression is invalid or fails to resolve, the response carries the literal string INVALID_JSON_PATH in place of that field's value rather than raising an error. Nothing fails loudly. The field just quietly holds a sentinel string, and whatever consumes it downstream treats that as data.

That is the failure mode preview exists to prevent, which is the whole reason it's worth the extra call.

Checking a mapping still holds across accounts

One mapping, many connectors, and no automatic guarantee it resolves on all of them. The configuration endpoint is the answer: for a given connector and (category, model), it returns every custom field with the mapping actually in effect, and includes unmapped fields with json_path: null so you can see what's left to configure.

GET /api/v1/custom-fields/configuration?connector_token=<CONNECTOR_TOKEN>&category=HRIS&model=employee

{
  "connector_token": "<CONNECTOR_TOKEN>",
  "integration_slug": "workday",
  "category": "HRIS",
  "model": "employee",
  "fields": [
    {
      "name": "cost_center",
      "json_path": "data.employee.cost_center",
      "source": "organization",
      "mapping_id": "018e586b-7d0b-7bc9-be65-a8fdbc82d734"
    },
    {
      "name": "badge_id",
      "json_path": null,
      "source": null,
      "mapping_id": null
    }
  ]
}

source tells you which mapping is winning: connector for a per-account override, organization for the inherited org-level one, null when nothing is configured. Bindbee's docs recommend running this as a health check after provisioning a new connector, which is exactly the moment a mapping silently stops resolving.

Raw data: see the whole response the provider sent

A custom field gives you one value, clean, on your schema. Raw data gives you the entire response the provider returned, in the provider's own shape, before normalization touched it.

The production path is a query parameter on the unified endpoints:

GET /api/hris/v1/employees?include_raw_data=true

Every record then carries a raw_data object alongside the normalized fields:

{
  "id": "018b18ef-c487-703c-afd9-0ca478ccd9d6",
  "first_name": "Ada",
  "work_email": "ada@acme.com",
  "raw_data": {
    "employee": {
      "id": "emp_123",
      "cost_center": "ENG-NA-004",
      "badge_id": "A-88213",
      "custom_attributes": {
        "shift_pattern": "4x10"
      }
    }
  }
}

The parameter is documented as "whether to include the original data Bindbee fetched from the third-party to produce these models," and it's available on the list endpoints across the unified models, not just employees.

That structure is the cost. RFC 8259, the IETF standard that defines JSON as a data-interchange format, guarantees the syntax, valid documents, well-formed objects and arrays. It guarantees nothing about which fields exist or how deep they're nested, which is why one provider's raw payload can look nothing like the next one's. Read raw data from three HRIS systems and you've taken on three shapes to parse, not one mapping shared across all of them.

Custom fields or raw data: the actual dividing line

The two get confused because both read from a payload you already pull. The difference is what you want back and how far it has to travel:

Custom fields Raw data
What you get One specific value, on your own schema attribute The provider's entire response, unmodified
Who it's for You know exactly which value you want, and you want it in the same place for every account You want to see everything the provider sent, or you're handling one integration's quirks
Where the parsing happens Bindbee, via your JMESPath Your code
How it scales One mapping per integration covers every connector of that integration Every provider is a separate shape you handle yourself

Passthrough: call the provider directly

Passthrough steps outside the unified model entirely and makes a request straight to the provider's own API, through the provider's own endpoints and shapes. It's the mechanism for the case neither custom fields nor raw data reaches: a field that lives on an endpoint your connector never calls, because nothing in the unified schema needed it.

Say the field sits on a carrier-specific report endpoint a payroll connector has no reason to hit for anything else it does. Custom-field mapping and raw data both operate on a payload the connector already pulled. Neither helps when the payload was never requested.

POST /api/v1/passthrough
Authorization: Bearer <YOUR_API_KEY>
x-connector-token: <CONNECTOR_TOKEN>
Content-Type: application/json


{
  "method": "GET",
  "path": "/v1/reports/benefit-elections",
  "headers": { "Content-Type": "application/json" },
  "params": { "effective_date": "2026-01-01" }
}

The important part of that request is what isn't in it. You are calling the provider's own endpoint, but you are not holding the provider's credentials, refreshing its tokens, or handling its auth scheme. The connector token identifies the account; Bindbee resolves it to the already-authenticated connection and signs the request. Every provider in the stack has its own OAuth quirks, token lifetimes, and header conventions, and passthrough is the one place where you get direct access to a provider's API without inheriting any of that.

What you do inherit is responsibility for the call itself. RFC 9110, the HTTP semantics standard, defines which methods are safe, they don't change anything on the server, and which are idempotent, meaning repeating them has the same effect as calling once. GET is both. POST is neither, and a passthrough call to an endpoint you've never used before is exactly where that distinction stops being academic: retry a POST that already succeeded and you may create the record twice. Passthrough supports reads and writes, and the unified layer isn't interpreting either one.

Freshness: what's sync-bound and what's live

Custom fields and raw data both read what the last sync stored. Bindbee syncs once every 24 hours by default, and that interval can be lowered based on your requirement. Three things address staleness, and they're worth knowing before you build around any of the three mechanisms:

Force a resync. When a user hits a "Sync now" button in your product, there's a documented endpoint for it:

POST /api/embedded/v1/connectors/resync
x-connector-token: <CONNECTOR_TOKEN>

It isn't meant to be called on a schedule,  Bindbee already syncs periodically, but it's the on-demand answer when someone needs the data refreshed now.

Pull only what changed. Every list endpoint takes modified_after, so you read the delta since your last pull instead of re-reading everything:

GET /api/hris/v1/employees?modified_after=2026-08-24T00:00:00Z

Passthrough is the only sync-independent path. Custom fields and raw data both read the last synced row. Passthrough hits the provider live, at the moment you call it. If the field you need has to be current to the second rather than current to the last sync, that's not a reason to prefer passthrough for convenience, it's the only one of the three that can answer the question at all.

Webhooks are worth naming for what they are, because they're easy to mistake for a freshness mechanism. Bindbee's five webhook events, Connector Sync Started, Connector Synced, Connector Sync Error, Employee Data Changed, Connector Data Modified, are sync-driven, and every payload carries a top-level sync object with sync_id, sync_type (MANUAL or AUTOMATED), and sync_name. employee_data_changed fires because a sync ran and found a change. It tells you a sync produced new data, which is genuinely useful for knowing when to re-read; it cannot tell you a value moved upstream ahead of the sync.

Which one, when

Your situation Mechanism What you get Freshness Main cost
The field is already in a payload you pull, and you want that one value on your schema for every account Custom fields The value mapped onto your own attribute, org- or connector-scoped Sync-bound You author and maintain the mapping; preview it or it fails silently
The field is in a payload you pull, and you want the provider's full response Raw data The entire upstream response, in the provider's own shape Sync-bound Structure varies per integration; parsing is on you
The field lives on an endpoint the sync never calls Passthrough A direct response from the provider, for that one endpoint Live You own method safety and error handling; nothing is normalized

Which mechanism to reach for was never a question about how complete the unified schema is. It's a routing decision, made once per field.

Inside a single connection, all three sit next to each other across the 67+ HRIS, payroll, ATS, and benefits systems Bindbee connects to, and all three run on the same authenticated connection the rest of the integration uses.

Every one of these is buildable directly against a provider's API, and none of them requires a unified API to exist. What doesn't scale is building all three, for every provider, for every field a customer might eventually ask for. That's the part a unified API is for. We build the integrations. You build the product. Book a demo.

What these mechanisms are not

  • Current values only. All three show what a field is now, not a record of what it used to be. A change-history question isn't something custom fields, raw data, or passthrough can answer.
  • An authored mapping, not a validated one. An unresolvable JMESPath returns INVALID_JSON_PATH rather than erroring, so a mapping should be previewed against a real payload before it ships and re-checked with the configuration endpoint after a new connector is provisioned.
  • A mirror, not a translation. Raw data reflects whatever shape the provider returns, and that shape can change from the provider's side without anything in the pipeline catching it.
  • No schema guarantees on passthrough. Passthrough doesn't interpret what comes back, so error handling and retries on that request are the caller's responsibility.

FAQ

Do I need a different token for custom fields than for the unified endpoints?

The Custom Fields endpoints are organization-scoped and take a Bearer API key in the Authorization header. The unified data endpoints additionally require an x-connector-token header identifying the account. Passthrough takes both: a Bearer key plus the x-connector-token for the connection you're calling through.

What data types can a custom field return?

Whatever the JMESPath resolves to. The preview response reports it as resolved_value_type, one of string, number, boolean, object, array, or null. Because the same upstream field can be typed differently across connectors of the same integration, it's worth previewing on more than one connector before committing an org-scoped mapping.

What happens when a JMESPath expression stops resolving?

Nothing raises. The field comes back as the literal string INVALID_JSON_PATH in the response. GET /api/v1/custom-fields/configuration shows which mapping is in effect on a given connector, and POST /api/v1/custom-fields/preview re-tests the expression against that connector's latest synced row.

Can I preview a JMESPath before a connector has ever synced?

Yes. Preview falls back to the integration's canned sample and reports raw_data_source: "integration_sample" so you know the expression was validated against a sample rather than that account's real data.

Does passthrough mean I have to handle the provider's authentication?

No. You send Bindbee a connector token and it resolves that to the already-authorized connection for that account, so you get the provider's own endpoint without managing its OAuth flow, tokens, or refresh cycle.

How fresh is the data these mechanisms return?

Custom fields and raw data return what the last sync stored. The default sync runs once every 24 hours and can be lowered based on requirement, POST /api/embedded/v1/connectors/resync forces one on demand, and modified_after on the list endpoints returns only what changed since a given timestamp. Passthrough is the only one of the three that reads live from the provider.

When should I use raw data instead of a custom field?

Use a custom field when you know exactly which value you want and you want it in the same place on your schema for every customer. Use raw data when you want the whole response the provider sent, usually because you're working through one integration's particular shape or you haven't settled on which fields you need yet.

Kunal Tyagi
CTO
Bindbee
VIEW AUTHOR
BLOG_

Related blogs