This page is the permalink for the code examples of Chapter 2 of Building Data Products, and the landing page for anything related to the Libot Services API. Bookmark the short link: jgp.ai/api.
The Libot Services are a small set of free REST services that turn ODCS data contracts and ODPS data products from a specification you read into artifacts you can build, version, compare, and score — with nothing more than curl. Point them at a DDL script and you get a contract back. Point them at a contract and you get a data product back.
Sections 1 to 9 are the whole tutorial, and they need nothing but an API key. Appendix A is separate on purpose: it covers OAuth 2.1 and the /v4 API, which you do not need in order to follow along. Read it when you are ready to move from a key you paste to a token that expires.
Everything below was run against the live service on August 14, 2026. Copy, paste, adapt.
1. At a glance
| Base URL | https://api.jgp.ai (canonical) or https://cloud.jgp.ai/api (as printed in the book) — see 2.1 |
| Auth (v1, used in sections 3 to 6) | X-API-KEY + X-USER-PASSWORD headers |
| Auth (v4, the modern surface) | Authorization: Bearer <JWT> — see Appendix A |
| Interactive docs | api.jgp.ai/swagger-ui.html |
| OpenAPI document | api.jgp.ai/api-docs |
| Health check | api.jgp.ai/v1/health |
| Standards emitted | ODCS v3.1.0 (contracts), ODPS v1.0.0 (products) |
| Source files | github.com/jgperrin/building-data-products |
2. Before you start
2.1 The base URL
Two forms work, and you can use whichever your copy of the material prints:
export BITOL_URL=https://api.jgp.ai # canonical export BITOL_URL=https://cloud.jgp.ai/api # as printed in the book and the tutorials
Some history, in case you hit it: the /api prefix stopped working for about six months after a server migration in February 2026 quietly dropped the routing rule that strips it. Everything published before then — Chapter 2 of the book, the four tutorials — answered 404 Not found. That is fixed as of August 14, 2026: both forms reach the service, on api.jgp.ai, cloud.jgp.ai and api.actianlabs.com alike. New code should prefer https://api.jgp.ai, which is the canonical name.
2.2 What you need
- curl and a text editor. Optionally jq to pretty-print JSON — every
| jqbelow can simply be dropped. - A real email address. Registration sends a six-character validation code, and the account does nothing until it is validated.
- Roughly ten minutes.
The examples use a UNIX-like shell (tested on macOS). Nothing in them is macOS-specific.
3. Get an API key
All the values that get reused live in environment variables, so the curl calls stay copy-pasteable.
export BITOL_URL=https://api.jgp.ai export BITOL_USER_EMAIL='you@example.com' export BITOL_USER_PW='<your-password>'
Two things the service is strict about: the email address cannot contain a + sign, and it must be one you can actually read. And the usual reminder — keep passwords, keys, and other credentials out of source control. A mistake comes way too quickly.
Register:
curl -X POST "$BITOL_URL/v1/users" \
-H "Content-Type: application/json" \
-d '{
"email": "'"$BITOL_USER_EMAIL"'",
"password": "'"$BITOL_USER_PW"'",
"firstName": "Your first name",
"lastName": "Your last name",
"company": "Your company",
"dob": "1971-10-05",
"code": "I<3BDP",
"comment": "Something nice about this book is always appreciated."
}'
[/bash]
<p>Only <code>email</code> and <code>password</code> are required; <code>code</code> is the reader code for <em>Building Data Products</em>. Do leave the placeholders behind, though: the service validates those fields and answers <code>400 Invalid first name, don't use the example values!</code> if you post the sample text verbatim.</p>
<p>The reply carries your key:</p>
[js gutter="false" autolinks="false"]
{"email":"you@example.com","firstName":"Your first name",
"lastName":"Your last name","company":"Your company","dob":"1971-10-05",
"comment":"Something nice about this book is always appreciated.",
"createdAt":"2026-08-14T15:50:29.650725",
"updatedAt":"2026-08-14T15:50:29.650736",
"apiKey":"00000000-0000-0000-0000-000000000000"}
[/js]
<p>Export it (with your own value — the one above is a placeholder):</p>
[bash gutter="false" autolinks="false"]
export BITOL_API_KEY=00000000-0000-0000-0000-000000000000
Now check your inbox for the six-character code and validate the account. Until you do, every authenticated call answers 403 Account not validated.
curl -X GET "$BITOL_URL/v1/validate?email=$BITOL_USER_EMAIL&validationCode=AB12CD"
No code in your inbox? Ask for another one:
curl -X POST "$BITOL_URL/v1/users/resend-verification?email=$BITOL_USER_EMAIL"
4. Turn a DDL script into a data contract
The promise comes first, so let’s start with the contract. The example uses a customer-and-addresses schema; the DDL is in the book’s repository.
-- Table: Customer
CREATE TABLE Customer (
customer_id SERIAL PRIMARY KEY,
first_name VARCHAR(255),
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20)
);
-- Table: AddressType
CREATE TABLE AddressType (
address_type_id SERIAL PRIMARY KEY,
address_type VARCHAR(20) NOT NULL
);
-- Table: Address
CREATE TABLE Address (
address_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
street1 VARCHAR(255) NOT NULL,
street2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20),
country_cd CHAR(2) NOT NULL,
address_type_id INT,
CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES Customer(customer_id) ON DELETE CASCADE,
CONSTRAINT fk_address_type FOREIGN KEY (address_type_id) REFERENCES AddressType(address_type_id)
);
Send it up:
cat resources/customer.sql | \
curl -X POST "$BITOL_URL/v1/contracts?sourceFormat=DDL&version=0.1.0&name=CustomerContract&domain=Customer&tenant=QuantumClimate" \
-H "X-API-KEY: $BITOL_API_KEY" \
-H "X-USER-PASSWORD: $BITOL_USER_PW" \
-F "file=@-"
Reading the call: sourceFormat=DDL describes what you are sending, version=0.1.0 is a semantic version, and name, domain, and tenant are the contract’s identity — the business domain and the tenant (brand, business unit, department…) it belongs to.
{"domain":"Customer","name":"CustomerContract",
"id":"34cae6d7-7648-38b2-8f66-8db79e1e2ce4","version":"v0.1.0",
"createdTs":"2026-08-14T12:57:58.123Z","tenant":"QuantumClimate","status":"draft"}
You should get exactly that id: contract ids are derived from the content, so the same DDL always lands on 34cae6d7-7648-38b2-8f66-8db79e1e2ce4. Yes, UUIDs are ugly and unreadable. They are also a very powerful tool — and you only ever need the last four characters to recognize one, so let’s call this contract 2ce4.
export BITOL_CONTRACT_ID=34cae6d7-7648-38b2-8f66-8db79e1e2ce4
4.1 No local file? Use a URL
If you would rather not download anything, hand the service a URL instead of a file. Same result, one fewer step:
curl -X POST "$BITOL_URL/v1/contracts?sourceFormat=DDL&version=0.1.0&name=CustomerContract&domain=Customer&tenant=QuantumClimate&ddlUrl=https://raw.githubusercontent.com/jgperrin/building-data-products/main/chapter-02/resources/customer.sql" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW"
dbmlUrl works the same way for DBML, and sourceFormat=auto lets the service detect the format. GET /v1/contracts/supported-databases lists what it can read directly over JDBC (PostgreSQL, MySQL, Informix, H2 as of today) — that one needs no authentication, so it is a nice first call to try.
4.2 Read it back and enrich it
curl -X GET "$BITOL_URL/v1/contracts/$BITOL_CONTRACT_ID" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" \ --output $BITOL_CONTRACT_ID-0.1.0.odcs.yaml
Without --output you get a long listing in the terminal. The first lines look like this:
apiVersion: "v3.1.0"
contractCreatedTs: "2026-08-14T12:57:58.123+00:00"
dataProduct: ""
description:
usage: "DDL upload for contract generation"
purpose: "Defines schema based on uploaded DDL"
limitations: "None"
domain: "Customer"
id: "34cae6d7-7648-38b2-8f66-8db79e1e2ce4"
kind: "DataContract"
name: "CustomerContract"
schema:
- logicalType: "object"
name: "Customer"
physicalName: "Customer"
physicalType: "table"
properties:
- logicalType: "number"
name: "customer_id"
physicalName: "customer_id"
physicalType: "SERIAL"
primaryKey: true
required: true
- logicalType: "string"
name: "first_name"
physicalName: "first_name"
physicalType: "VARCHAR (255)"
…
That is an embryo, not a finished contract: structure, no meaning. Open it in your favorite editor (mine is still vi; VS Code is a fine alternative), add descriptions, quality rules, an SLA, owners — then save it as 0.1.1 and send it back:
curl -X POST "$BITOL_URL/v1/contracts?version=0.1.1" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" \ -F "file=@./$BITOL_CONTRACT_ID-0.1.1.odcs.yaml"
5. Turn the contract into a data product
Now that there is a contract, a data product is a single call away:
curl -X POST "$BITOL_URL/v1/products?contractId=$BITOL_CONTRACT_ID&contractVersion=0.1.1" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW"
{"id":"3153baf0-0c77-351d-b10c-a98578e10806","version":"0.1.0",
"status":"draft","name":"Default Data Product",
"createdTs":"2026-08-14T12:58:17.503598653Z"}
export BITOL_PRODUCT_ID=3153baf0-0c77-351d-b10c-a98578e10806
And retrieve it:
curl -X GET "$BITOL_URL/v1/products/$BITOL_PRODUCT_ID" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" \ --output $BITOL_PRODUCT_ID-0.1.0.odps.yaml
apiVersion: "v1.0.0"
kind: "DataProduct"
id: "3153baf0-0c77-351d-b10c-a98578e10806"
name: "Default Data Product"
version: "v0.1.0"
status: "draft"
domain: ""
tenant: ""
description:
purpose: "Automatically generated data product output contract"
outputPorts:
- name: "Default Data Product Output Port"
version: "v0.1.0"
contractId: "34cae6d7-7648-38b2-8f66-8db79e1e2ce4"
team:
name: "Data Product Team"
members:
- dateIn: "2026-08-14"
role: "owner"
name: "Your first name Your last name"
username: "you@example.com"
productCreatedTs: "2026-08-14T12:58:17.503598653Z"
A data product with a name, a version, a status, an output port bound to a specific contract version, and an owner — generated, not typed. Note the apiVersion: the generator still emits ODPS v1.0.0, not the v1.1.0 shown elsewhere in the book. It is on the list.
Edit, save as 0.1.1, resubmit:
curl -X POST "$BITOL_URL/v1/products?version=0.1.1" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" \ -F "file=@$BITOL_PRODUCT_ID-0.1.1.odps.yaml"
6. The other calls worth knowing
6.1 List everything you own
curl -X GET "$BITOL_URL/v1/contracts" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" | jq
[
{
"id": "34cae6d7-7648-38b2-8f66-8db79e1e2ce4",
"version": "v0.1.0",
"name": "CustomerContract",
"domain": "Customer",
"tenant": "QuantumClimate",
"status": "draft"
},
{
"id": "34cae6d7-7648-38b2-8f66-8db79e1e2ce4",
"version": "v0.1.1",
"name": "CustomerContract",
"domain": "Customer",
"tenant": "QuantumClimate",
"status": "draft"
}
]
Products work identically:
curl -X GET "$BITOL_URL/v1/products" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" | jq
6.2 Recover a lost API key
It happens — you forget to export it and close the terminal. As long as you know your email and password, the key comes back. There is no way to recover the email or the password.
curl -X GET "$BITOL_URL/v1/users/key?email=$BITOL_USER_EMAIL" \ -H "X-USER-PASSWORD: $BITOL_USER_PW"
The password goes in the header, never in the query string — a query-string password is ignored, and it lands in server logs.
6.3 Compare two versions and get a semver suggestion
The most useful operation when maintaining contracts over time is knowing exactly what changed between two versions, and whether the change is breaking. The compare endpoint returns a structured diff with an impact level per difference, and suggests the next version number.
curl -X GET "$BITOL_URL/v1/contracts/compare?id1=$BITOL_CONTRACT_ID&version1=0.1.0&id2=$BITOL_CONTRACT_ID&version2=0.2.0" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" | jq
{
"diffs": [
{
"type": "ADD",
"section": "schema",
"field": "quality",
"path": "/schema[0]/properties[2]/quality",
"message": "Quality rule added",
"impactLevel": "MINOR"
}
],
"major": 0,
"minor": 1,
"patch": 0,
"id1": "34cae6d7-7648-38b2-8f66-8db79e1e2ce4",
"version1": "v0.1.0",
"id2": "34cae6d7-7648-38b2-8f66-8db79e1e2ce4",
"version2": "v0.2.0",
"suggestedVersion": "v0.2.0"
}
Adding a quality rule is backwards-compatible, so MINOR. Removing or renaming a column would be MAJOR — your consumers’ pipelines break, and the version number should say so before they find out the hard way.
6.4 Score a contract’s maturity
All data contracts are created equal, but they do not stay that way. A contract with a name and three column names is valid ODCS and delivers a fraction of the value of one with descriptions, an SLA, and quality rules. The maturity endpoint scores yours from 1 to 5 and tells you what is missing.
curl -X GET "$BITOL_URL/v1/contracts/maturity-level?contractId=$BITOL_CONTRACT_ID&version=0.1.0" \ -H "X-API-KEY: $BITOL_API_KEY" \ -H "X-USER-PASSWORD: $BITOL_USER_PW" | jq
{
"contractId": "34cae6d7-7648-38b2-8f66-8db79e1e2ce4",
"maturityLevel": "1",
"maturityLevelLabel": "Structural",
"explanation": "## Maturity Assessment: Level 1 — Structural …",
"nextLevelAdvice": "## How to Reach Level 2 — Descriptive …"
}
The freshly generated contract scores level 1, Structural — exactly what you would expect from a schema with no semantics attached. Run it again after each enrichment pass to check you are adding meaning and not noise.
This call also exists one version up, for anyone who has already moved to Bearer tokens (Appendix A) — same path, same parameters, one header instead of two:
curl -X GET "$BITOL_URL/v4/contracts/maturity-level?contractId=$BITOL_CONTRACT_ID&version=0.1.0" \ -H "Authorization: Bearer $BITOL_TOKEN" | jq
7. Endpoint cheat sheet
| What | Call |
|---|---|
| Register | POST /v1/users |
| Validate the account | GET /v1/validate?email=&validationCode= |
| Resend the validation code | POST /v1/users/resend-verification?email= |
| Recover the API key | GET /v1/users/key?email= (password in the header) |
| Contract from DDL / DBML / URL / JDBC | POST /v1/contracts?sourceFormat=&version=&name=&domain=&tenant= |
| Upload a contract | POST /v1/contracts?version= |
| Get a contract | GET /v1/contracts/{id}?version= |
| List contracts | GET /v1/contracts |
| Compare two versions | GET /v1/contracts/compare?id1=&version1=&id2=&version2= |
| Maturity score | GET /v1/contracts/maturity-level?contractId=&version= |
| Supported databases | GET /v1/contracts/supported-databases (no auth) |
| Product from a contract | POST /v1/products?contractId=&contractVersion= |
| Upload a product | POST /v1/products?version= |
| Get a product | GET /v1/products/{id}?version= |
| List products | GET /v1/products |
| Publish to GitHub | POST /v1/contracts/publish-github, POST /v1/products/publish-github |
| Health | GET /v1/health (no auth) |
The full, always-current list lives in Swagger UI.
8. Going further: the four hands-on tutorials
These four walkthroughs go deeper than this page, each on one theme. They use the https://cloud.jgp.ai/api base URL, which works again as of August 14, 2026 (see 2.1) — so they run exactly as published, no substitution needed.
- Experimenting with Data Contracts — build, validate, and store a contract from scratch.
- Playing with Data Products — go from contract to data product, and version both.
- Controlling Schema Drift — detect and analyze schema changes before they break consumers, using the compare endpoint.
- Making Pretty Documentation from Data Contracts — turn a contract into a PDF people actually want to read.
9. The fine print
- The service is free. That might not last forever, and it is not a production data platform — treat what you upload as experimental.
- The API evolves. Paths and payloads in this article are verified against the live service as of August 14, 2026; Swagger UI is always right, this page is a snapshot.
- Contract ids are content-derived and creation is idempotent, so re-running the examples is safe.
- Never commit an API key, a client secret, or a password. Use environment variables, and rotate anything that leaks.
Questions, bugs, or something that does not behave as described here? Tell me — and if you are reading Building Data Products, this page is the copy-paste companion to Chapter 2. It will be kept current as the API moves.
Appendix A. OAuth 2.1 and the v4 API
Optional. Nothing in sections 1 to 9 needs any of this. Read on when your scripts start outliving your terminal session.
A.1 Why move at all
Sections 3 to 6 use the /v1 surface with two headers: an API key and your password. It is the simplest thing that works, it is what the book uses, and it is not going away for contracts, products, and users. But it is legacy: an API key never expires, carries no scope, and cannot be narrowed. Every /v1 response now says so out loud:
Warning: 299 - "Deprecated: Use Bearer JWT authentication via OAuth 2.1 (/oauth2/authorize)"
The modern surface is /v4, and it authenticates with a short-lived Bearer token issued by the OAuth 2.1 authorization server that runs at https://api.jgp.ai. Two flows matter, and which one you want depends on who is holding the credential: A.3 if that is you at a keyboard, A.4 if it is a pipeline.
A.2 The discovery document
Start here. It is public, and it is the source of truth for every path in this appendix — if one of them ever moves, this document is where you find out.
curl -s https://api.jgp.ai/.well-known/oauth-authorization-server | jq
{
"issuer": "https://api.jgp.ai",
"authorization_endpoint": "https://api.jgp.ai/oauth2/authorize",
"token_endpoint": "https://api.jgp.ai/oauth2/token",
"registration_endpoint": "https://api.jgp.ai/oauth2/register",
"device_authorization_endpoint": "https://api.jgp.ai/oauth2/device_authorization",
"grant_types_supported": ["authorization_code", "client_credentials",
"refresh_token", "urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:token-exchange"],
"code_challenge_methods_supported": ["S256"]
}
PKCE is mandatory. There is no password grant and no implicit grant.
A.3 Flow A — you, at a keyboard (authorization code + PKCE)
For a CLI or a script you run yourself. First register a client. Registration is anonymous, and clients are public — no secret is issued. A loopback callback on any port is accepted (RFC 8252), as long as the path is /callback:
curl -X POST https://api.jgp.ai/oauth2/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My contract CLI",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "email workbench:session"
}'
The response hands you the client_id you will use for the rest of the flow:
{"client_id":"cac3d427-84bf-441c-98b4-68c388def283",
"client_id_issued_at":1786712446,
"client_name":"My contract CLI",
"redirect_uris":["http://127.0.0.1:8765/callback"],
"token_endpoint_auth_method":"none",
"grant_types":["authorization_code","refresh_token"],
"response_types":["code"],
"scope":"email workbench:session"}
Then generate a PKCE pair, open the authorization URL in a browser, sign in, and catch the code on your loopback port:
export CLIENT_ID=cac3d427-84bf-441c-98b4-68c388def283 # from the reply above export VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64) export CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 \ | openssl base64 | tr '+/' '-_' | tr -d '=') # macOS uses open; Linux uses xdg-open; or just paste the URL in a browser open "https://api.jgp.ai/oauth2/authorize?response_type=code\ &client_id=$CLIENT_ID\ &redirect_uri=http://127.0.0.1:8765/callback\ &code_challenge=$CHALLENGE&code_challenge_method=S256\ &scope=email%20workbench:session\ &resource=https://api.jgp.ai&state=$RANDOM"
After you approve the consent screen, the browser lands on your loopback URL with ?code=…&state=…. Exchange that code for a token:
export BITOL_TOKEN=$(curl -s -X POST https://api.jgp.ai/oauth2/token \ -d "grant_type=authorization_code" \ -d "code=<the code from the callback>" \ -d "redirect_uri=http://127.0.0.1:8765/callback" \ -d "client_id=$CLIENT_ID" \ -d "code_verifier=$VERIFIER" | jq -r .access_token)
That is the $BITOL_TOKEN used in section 6.4 and in A.5. You get an ES256 JWT, a refresh token, and a short expiry. Refresh with grant_type=refresh_token at the same endpoint; no re-consent needed. If your client cannot open a browser at all, /oauth2/device_authorization implements the device-code grant instead.
A.4 Flow B — a pipeline, unattended (client credentials)
For CI/CD, do not ship a personal key. Use an organization-owned service account: a client_id and client_secret pair that trades for a one-hour token.
Where the two values come from. An admin of the organization creates the account once, using their own Bearer token from A.3. The plaintext secret is returned exactly once, at creation — there is no way to read it back later, and a lost secret means a rotate:
curl -X POST "https://api.jgp.ai/v4/orgs/service-accounts?org=$ORG_SLUG" \
-H "Authorization: Bearer $BITOL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "contract-publisher",
"description": "Publishes contracts from CI",
"scopes": ["contracts:read", "contracts:publish"]
}'
{
"id": "0d9f1c74-...",
"clientId": "sa-7f3c1e08-...",
"orgId": "b21e...",
"name": "contract-publisher",
"description": "Publishes contracts from CI",
"scopes": ["contracts:read", "contracts:publish"],
"createdAt": "2026-08-14T13:05:11.402Z",
"revoked": false,
"clientSecret": "SHOWN-ONCE-COPY-IT-NOW",
"secretWarning": "This is the only time the client_secret is shown. Record it now."
}
Store those two values as CI secrets — BITOL_CLIENT_ID and BITOL_CLIENT_SECRET — and never anywhere else:
# In GitHub Actions these come from repository secrets, not from your shell: # BITOL_CLIENT_ID = the clientId above # BITOL_CLIENT_SECRET = the clientSecret shown once at creation # ORG_SLUG = your organization slug # 1. Exchange the credentials for a one-hour token TOKEN=$(curl -sf -X POST https://api.jgp.ai/oauth2/token \ -u "$BITOL_CLIENT_ID:$BITOL_CLIENT_SECRET" \ -d "grant_type=client_credentials" \ -d "scope=contracts:read contracts:publish" | jq -r .access_token) # 2. Call the API with it curl -sf -X POST "https://api.jgp.ai/v4/contracts/publish-github?contractId=$CONTRACT_ID&org=$ORG_SLUG" \ -H "Authorization: Bearer $TOKEN"
Three things to know. Scopes are explicit: a token request without scope is granted nothing, and a call outside the granted scope returns 403 naming what is missing. Rotating (POST /v4/orgs/service-accounts/{id}/rotate?org={slug}) invalidates the old secret immediately. And service accounts are deliberately additive — they can read, create, publish, and sign, but they can never delete a contract, a product, or a data source. Deletion stays human.
A.5 Mapping the calls in this article to v4
The path shapes are preserved; what changes is the two headers becoming one, and organization context arriving as ?org={slug}.
| v1 (sections 3 to 6) | v4 equivalent |
|---|---|
X-API-KEY + X-USER-PASSWORD |
Authorization: Bearer <JWT> |
POST /v1/users |
POST /v4/users/register then POST /v4/users/verify |
GET /v1/users/key |
— no equivalent; v4 has no API keys, only tokens |
POST /v1/contracts |
POST /v4/contracts |
GET /v1/contracts, GET /v1/contracts/{id} |
GET /v4/contracts, GET /v4/contracts/{id} |
GET /v1/contracts/compare |
GET /v4/contracts/compare |
GET /v1/contracts/maturity-level |
GET /v4/contracts/maturity-level |
POST /v1/products, GET /v1/products, GET /v1/products/{id} |
same paths under /v4 |
| — | every /v4 POST accepts an Idempotency-Key header |
My advice, in one line: learn the service on /v1 with the two headers, because it is the shortest path from zero to a contract in your hands — then move anything that outlives your terminal session to /v4 and OAuth.
