Why Norbelys's API uses givenName, not first_name
Norbelys's API uses Schema.org fields — givenName, affiliation, sameAs — instead of bespoke names like first_name. The reasoning, plus the mapping table.
By Gabriel Lara, Developer Relations, Norbelys
Founder-reviewed ·How we research and correct articles
Open the response from GET /v1/people/:id and you won’t see first_name,
company, or linkedin_url. You’ll see givenName, affiliation, and
sameAs. That’s not an aesthetic choice or a nod to some internal style
guide — it’s Schema.org, the vocabulary schema.org’s Person and
Organization types already define, reused instead of reinvented. The
reasoning behind it is worth understanding, because it explains a design
decision that shows up in every response the API returns.
The problem with inventing your own names
Every API that models a person ends up needing a name for “the person’s first
name.” Most invent one: first_name, firstName, fname, given_name. All
four exist across real production APIs today, and none of them is wrong —
they’re just arbitrary, which means every integration has to look them up.
That’s a cost paid once per API, forever, by every developer and every tool
that touches it, for a decision that had no reason to be made fresh in the
first place.
Schema.org already solved this. It’s the vocabulary search engines,
structured-data tooling, and a large share of the web’s own JSON-LD already
speak — Person.givenName, Person.familyName, Organization.name,
Thing.sameAs. It’s not a cold-email-specific vocabulary; it’s a general one
that happens to cover exactly the entities a CRM-shaped API needs to name:
people, organizations, addresses, URLs. Reusing it means the names on the
wire are recognizable before you’ve read a single line of documentation,
because they’re the same names schema.org, and by extension a large amount of
existing tooling, already treats as standard.
curl https://api.norbelys.com/v1/people/con_8h3k \
-H "Authorization: Bearer ak_live_..."
{
"id": "con_8h3k",
"email": "jane@acme.com",
"givenName": "Jane",
"familyName": "Doe",
"jobTitle": "VP Marketing",
"affiliation": "Acme Corp",
"url": "https://acme.com",
"sameAs": ["https://www.linkedin.com/in/janedoe"],
"dateCreated": "2026-05-11T14:02:00Z"
}
Every one of those field names — givenName, jobTitle, affiliation,
sameAs, dateCreated — is a real Schema.org property. None of it is
Norbelys-invented.
Why this matters more with AI clients in the loop
The “already invented” argument gets stronger, not weaker, once you consider
who else is reading these field names. A large language model has seen
schema.org/Person and schema.org/Organization millions of times in its
training data — in JSON-LD embedded across the web, in documentation, in
other APIs that made the same choice. An AI agent driving Norbelys through
the MCP server or generated SDKs is far more likely to
correctly guess that affiliation means “company” than it is to guess that a
bespoke company_name field does, because affiliation is vocabulary it’s
already seen attached to exactly this meaning, thousands of times over. That’s
the “AI-first” half of developer-first-and-AI-first: pick names a model
already understands instead of ones it has to learn from your docs alone.
The full mapping
The person resource is the clearest example, and it’s also where this
mattered enough to become a real migration: the resource used to be called
contact, with firstName/lastName/phone fields, and both the API and
the underlying Postgres columns were renamed to match Schema.org — not
translated by a serializer, physically renamed in the database. There’s no
wrapper layer converting first_name to givenName on the way out; the
column is named given_name and the wire field is givenName. The mapping
below is what that rename produced, and it’s the same mapping applied
everywhere a comparable concept appears in the API:
| DB column | API field (Schema.org) | Type |
|---|---|---|
given_name |
givenName |
Person |
family_name |
familyName |
Person |
email |
email |
Person |
telephone |
telephone |
Person |
title |
jobTitle |
Person |
company_name |
affiliation (or worksFor.name) |
Organization |
website |
url |
Thing |
linkedin |
sameAs |
Thing |
location |
address |
PostalAddress |
custom_fields |
additionalProperty |
PropertyValue[] |
created_at |
dateCreated |
— |
updated_at |
dateModified |
— |
additionalProperty is worth calling out specifically — it’s Schema.org’s
own mechanism for “a named value that doesn’t fit the standard vocabulary,”
which is exactly what a custom field is. Rather than inventing yet another
bespoke shape for user-defined fields, the API reuses the vocabulary’s own
extension point.
Where Schema.org doesn’t reach
Not everything in a cold-email platform has a Schema.org equivalent, and the
rule isn’t to force one where it doesn’t fit. Fields with no real
counterpart — status, source, dailyLimit, and similar
Norbelys-specific concepts — stay as descriptive lowerCamelCase names,
exactly as bespoke as they’d be in any other API. The rule is narrower and
more useful than “always use Schema.org”: use it where it already exists,
and don’t invent around it where it doesn’t. That keeps the vocabulary
honest instead of stretching it to cover things it was never meant to
describe.
No wrapper layer, on purpose
It’s worth being precise about what “the DB column names already equal the
wire names” actually means, because it’s a stronger claim than most APIs can
make. In a typical setup, an API field like givenName is produced by a
serializer that reads a differently-named database column and renames it on
the way out — a translation layer that has to be kept correct forever,
usually in a file nobody enjoys touching. After the migration that renamed
the contact resource to person, that translation layer doesn’t exist for
these fields at all. The Postgres column is literally given_name; the API
field is givenName; the difference between them is snake_case versus
camelCase, nothing more. There’s no intermediate mapping to get wrong,
because the database and the API are describing the same fact using the same
name, just formatted for their respective conventions.
That matters for correctness, not just cleanliness. A wrapper layer is a
place bugs hide — a rename on one side that doesn’t get mirrored on the
other, a field that’s present in the database but silently dropped by the
serializer. Removing the layer removes the class of bug along with it. It’s
the same “fewest moving parts” instinct that puts a plain
Idempotency-Key header
directly on state-changing requests instead of asking integrators to build
their own out-of-band deduplication — solve the problem once, at the source,
rather than papering over it downstream.
The resource rename that made this literal
The person resource is also a useful case study in what it costs to take
Schema.org naming seriously instead of half-adopting it. It would have been
easy to keep the database as contact/first_name/last_name and just
rename the fields in an API response layer — cheaper, lower-risk, shippable
in an afternoon. Norbelys did the more expensive thing instead: an actual
schema migration that renamed the table and its columns, so contact became
person, first_name became given_name, last_name became family_name,
phone became telephone, and import_batch_id became import_id. The ID
prefix (con_…) stayed the same on purpose — it’s an opaque identifier, not
a name, so there was nothing to correct there, and changing it would have
broken every existing reference for no benefit.
The IDs are unaffected, but every other name in the resource now matches Schema.org from the database up, which is the version of this decision that actually pays off — a half-measure translation layer would have left the “no wrapper” claim false the moment anyone looked at the schema underneath.
What this buys you as an integrator
In practice this means you can often guess a field name correctly before
checking the OpenAPI spec, and when
you do check it, the name will already make sense rather than needing a
lookup table of your own. It’s the same principle behind the five endpoint
patterns the whole API follows —
fewer arbitrary decisions on the wire means less the API asks you to memorize
and more it lets you predict. A Person is the same Person whether
Schema.org, Norbelys, or the LLM reading your integration code is the one
looking at it.
Frequently asked questions
Why does the person resource still use the con_ ID prefix if it's no longer called contact?
IDs are opaque identifiers, not names — they're prefixed cuid2 values used to identify a record type in logs and URLs, not to describe what the resource is called in the API. The rename changed every field name to Schema.org vocabulary; it left the con_ prefix alone because there was nothing wrong with it.
Do all Norbelys resources use Schema.org names?
Only where a genuine equivalent exists, which is mostly the person and organization-shaped fields. Domain-specific concepts like sending status, daily limits, or campaign state have no Schema.org counterpart and use descriptive camelCase names instead, same as any other API.
What is additionalProperty for?
It's Schema.org's own extension mechanism for values that don't map to a standard property. Norbelys uses it to represent custom fields you define on a person, so user-defined data has a real Schema.org shape instead of an ad hoc key-value bag.