Custom Schemas
Reshape API responses to match your data model.
TL;DR
Define field mappings once, then send X-Schema: your-schema on any read endpoint to receive responses in your own field names — no client-side transform layer required.
What It Is
Every team that integrates with a vacation-rental API ends up writing the same code: a transform layer that renames Repull's fields into the fields their app already uses. Custom Schemas remove that layer. You define the mapping once, store it on your workspace, and reference it by name on any read request. Repull does the transform server-side and gives you back a payload that drops straight into your domain model.
Think of it like Stripe Connect's Direct Charges vs Destination Charges — same underlying API, different response shapes for different consumers. You stay in control of what your app sees.
Built-In Schemas
Three schemas ship with every workspace. You can use them as-is or use them as the starting point for your own.
| Schema | Description |
|---|---|
native | Repull's flat snake_case format. Best for new integrations. |
calry | Calry v2 compatible. camelCase, nested primaryGuest / occupancy / financials. Default when no header is sent. |
calry-v1 | Calry v1 compatible. For developers still on the legacy v1 shape. |
Quickstart
Three steps from zero to a fully-customised response.
1. Create a schema
curl -X POST https://api.repull.dev/v1/schema/custom \
-H "Authorization: Bearer $REPULL_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"description": "Maps Repull responses to our internal field names",
"mappings": {
"id": "id",
"arrival": "checkIn",
"departure": "checkOut",
"guest_name": "primaryGuest.firstName + \' \' + primaryGuest.lastName"
}
}'2. Send the schema name on any read
curl https://api.repull.dev/v1/reservations \ -H "Authorization: Bearer $REPULL_KEY" \ -H "X-Schema: my-app"
3. Get back a response in your shape
{
"data": [
{
"id": "215906",
"arrival": "2026-06-01",
"departure": "2026-06-05",
"guest_name": "Sarah Mitchell"
}
]
}Mapping Syntax
Each entry in mappings is "outputField": "expression". The expression runs against each item in the response. Five forms are supported.
Direct rename
Reference a top-level field by name to copy it under a new key.
{ "arrival": "checkIn" }
// checkIn: "2026-06-01" → arrival: "2026-06-01"Dot-notation paths
Reach into nested objects with dots. Missing intermediate values resolve to undefined and the field is skipped from the output.
{ "first_name": "primaryGuest.firstName" }
// primaryGuest: { firstName: "Sarah" } → first_name: "Sarah"String concatenation
Join values with + (spaces required around the plus). Single-quoted or double-quoted literals are inserted verbatim. Missing values are treated as the empty string.
{ "guest_name": "primaryGuest.firstName + ' ' + primaryGuest.lastName" }
// → guest_name: "Sarah Mitchell"
{ "label": "'Booking #' + id" }
// → label: "Booking #215906"Arithmetic
Division ( / ) and multiplication ( * ) are supported between two field references. Spaces around the operator are required. Results are rounded to 2 decimal places. Division by zero returns 0.
{ "nightly_rate": "financials.totalPrice / nights" }
// totalPrice: 1800, nights: 4 → nightly_rate: 450.00
{ "tax_amount": "financials.totalPrice * taxRate" }
// totalPrice: 1800, taxRate: 0.12 → tax_amount: 216.00Nested output paths
The output key can also use dot notation to build nested objects.
{
"guest.first": "primaryGuest.firstName",
"guest.last": "primaryGuest.lastName"
}
// → { guest: { first: "Sarah", last: "Mitchell" } }Not supported
Conditionals, function calls, addition/subtraction, regex, and any JavaScript-style expression beyond the five forms above will be rejected at create time. Expressions containing keywords like function, eval, require, import, process, __proto__, constructor, or prototype are blocked.
Constraints
- Schema name — 3–100 characters, lowercase letters, digits, and hyphens. Must start and end with a letter or digit.
- Reserved names —
native,calry, andcalry-v1cannot be used. - Mappings per schema — up to 50 fields.
- Field name length — output field names up to 100 characters.
- Expression length — each expression up to 500 characters.
- Uniqueness — schema names must be unique within a workspace. The same name is allowed in different workspaces.
Endpoints That Respect X-Schema
All read endpoints honour the header. Write endpoints currently use the workspace default schema for request parsing — see Schema Adapters for the request-side story.
| Method | Endpoint | Resource |
|---|---|---|
| GET | /v1/listings | Listings |
| GET | /v1/listings/{id} | Listings |
| GET | /v1/reservations | Reservations |
| GET | /v1/reservations/{id} | Reservations |
| GET | /v1/conversations | Conversations |
| GET | /v1/conversations/{id} | Conversations |
| GET | /v1/conversations/{id}/messages | Conversations |
| GET | /v1/guests | Guests |
| GET | /v1/guests/{id} | Guests |
| GET | /v1/reviews | Reviews |
| GET | /v1/reviews/{id} | Reviews |
Managing Schemas
Full CRUD via /v1/schema/custom. All operations are scoped to the authenticated workspace.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/schema/custom | Create a new schema |
| GET | /v1/schema/custom | List schemas in this workspace |
| GET | /v1/schema/custom/{id} | Fetch one schema by id |
| PATCH | /v1/schema/custom/{id} | Update mappings, description, or active flag |
| DELETE | /v1/schema/custom/{id} | Remove a schema |
List all schemas in the current workspace:
curl https://api.repull.dev/v1/schema/custom \ -H "Authorization: Bearer $REPULL_KEY"
Update an existing schema:
curl -X PATCH https://api.repull.dev/v1/schema/custom/sch_01HXYZ \
-H "Authorization: Bearer $REPULL_KEY" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"id": "id",
"arrival": "checkIn",
"departure": "checkOut",
"guest_name": "primaryGuest.firstName + \' \' + primaryGuest.lastName",
"nights": "nights"
}
}'TypeScript SDK
The official SDKs accept a schema option on any read method. Available in @repull/sdk v0.1.2+.
import { Repull } from '@repull/sdk'
const repull = new Repull({ apiKey: process.env.REPULL_KEY! })
const reservations = await repull.reservations.list({ schema: 'my-app' })
// reservations[0] now uses your field names — arrival, departure, guest_nameCommon Patterns
Mapping to your existing PMS schema
If you're migrating from another vendor, model your mappings on the field names already in your codebase. New Repull data flows through the same models you have today — no domain rewrites.
{
"name": "legacy-pms",
"mappings": {
"booking_id": "id",
"property_id": "listingId",
"arrival_date": "checkIn",
"departure_date": "checkOut",
"guest_full_name": "primaryGuest.firstName + ' ' + primaryGuest.lastName",
"guest_email": "primaryGuest.email",
"total_amount": "financials.totalPrice",
"currency_code": "financials.currency"
}
}Thin proxy endpoint
For internal services, expose a tiny proxy that forwards to Repull with your schema header set. Your downstream code never sees Repull's native shape.
// app/api/internal/reservations/route.ts
export async function GET() {
const res = await fetch('https://api.repull.dev/v1/reservations', {
headers: {
Authorization: `Bearer ${process.env.REPULL_KEY}`,
'X-Schema': 'my-app',
},
})
return Response.json(await res.json())
}Versioning your schemas
Suffix the schema name with a version (my-app-v1, my-app-v2) so you can roll out new field shapes without breaking existing clients. Keep both alive during the transition and delete the old one when traffic has migrated.
Gotchas
- Workspace-scoped — schemas live on a single workspace. Multi-workspace apps need to manage schemas per workspace (or create the same schema in each one during onboarding).
- Unmapped fields are dropped — only fields you explicitly list in
mappingsappear in the response. If you want a field, map it. This keeps the payload tight and predictable but is the opposite of how the built-innative/calryschemas behave (those return everything). - Unknown schema name — sending an
X-Schemavalue that doesn't exist in your workspace falls back to the workspace default (calry). Watch the response shape if you suspect a typo. - Mapping errors at request time — expressions are validated when you create or update a schema, not at every request. If a runtime evaluation fails (for example, a
/against a non-numeric field), the field resolves to0or is skipped rather than failing the whole response. - Read-only today — custom schemas only transform responses. Request bodies (POST/PATCH) still need to match the workspace default schema. If you need a different request shape, use
X-Schema: nativeorX-Schema: calryper request — see Schema Adapters.
Related
- Schema Adapters — built-in formats and request-side parsing
- Migrate from Calry — drop-in compatibility
- Get Reservations — the most common endpoint to reshape
Questions or a mapping form you wish was supported? Email hello@repull.dev.