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 | jq below 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."
  }'
&#91;/bash&#93;

<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 &mdash; 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.

  1. Experimenting with Data Contracts — build, validate, and store a contract from scratch.
  2. Playing with Data Products — go from contract to data product, and version both.
  3. Controlling Schema Drift — detect and analyze schema changes before they break consumers, using the compare endpoint.
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *