Create a provider
By the end of this tutorial you will have a working, validated provider — the model credentials every agent in your project runs against.
Three steps:
- Create a project — the tenancy boundary everything else lives in.
- Register a provider — naturali models (no credential of your own) or BYOK (your key).
- Validate it with one smoke-test generation.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
You need a nat_sk_… API key (or a session JWT from
Auth) exported as NATURALI_TOKEN:
export NATURALI_TOKEN=nat_sk_...
Then set your client up:
- CLI
- SDK
- curl
pnpm add -g @naturali/cli
The CLI reads NATURALI_TOKEN from the environment. See the
CLI guide.
pnpm add @naturali/sdk
import { NaturaliClient } from '@naturali/sdk';
const naturali = new NaturaliClient({ token: process.env.NATURALI_TOKEN });
Every call resolves to { data, error } and never throws on a non-2xx — see the
SDK guide. The snippets below read data directly for
brevity; check error in real code.
Nothing to install. Every request goes to https://api.naturali.ai/v1 and
carries the bearer token:
export NATURALI_API=https://api.naturali.ai/v1
1. Create a project
A project is the tenancy boundary: providers, agents, generations and the usage meter all belong to one.
- CLI
- SDK
- curl
naturali create-project --name my-first-project
const { data: project } = await naturali.projects.createProject({
body: { name: 'my-first-project' },
});
curl -sS -X POST "$NATURALI_API/projects" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "name": "my-first-project" }'
{
"id": "proj_V1StGXR8Z5jdHi6B",
"name": "my-first-project",
"status": "active",
"created_at": "2026-07-18T00:00:00.000Z"
}
Keep that id — every call from here takes it. The CLI can hold it for you, so
the rest of the CLI examples omit --project-id:
export NATURALI_PROJECT=proj_V1StGXR8Z5jdHi6B
For the SDK and curl examples, assume:
export PROJECT=proj_V1StGXR8Z5jdHi6B
2. Register a provider
A provider is the model credentials an agent runs
against. There are two kinds, and this is the one real decision in the
tutorial:
managed — naturali models | byok — your own key | |
|---|---|---|
| Setup | Pick a catalog model. No credential. | Register your provider key. |
| Runs on | naturali's provider account | your provider account |
| Billing | metered and priced by naturali | you pay your provider directly |
| Generation cost | a dollar figure | tokens only, cost_usd is null |
Pick one below; the tabs remember your choice for the rest of the page.
- naturali models (managed)
- Your own key (BYOK)
Managed providers use naturali's own model access, so you supply nothing but a
model id from the catalog. Ask for the models that can
actually be managed — managed=true is exactly the set this step accepts:
- CLI
- SDK
- curl
naturali list-models --managed true --vendor amazon --status available --limit 5
const { data: models } = await naturali.models.listModels({
query: { managed: true, vendor: 'amazon', status: 'available', limit: 5 },
});
curl -sS -G "$NATURALI_API/models" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-d managed=true -d vendor=amazon -d status=available -d limit=5
{
"data": [
{
"id": "amazon.nova-lite-v1:0",
"display_name": "Nova Lite",
"vendor": "amazon",
"provider": "bedrock",
"provider_model": "us.amazon.nova-lite-v1:0",
"input_modalities": ["text", "image"],
"output_modalities": ["text"],
"streaming": true,
"status": "available",
"managed": true
}
],
"next_cursor": null
}
status and managed are different questionsstatus: "available" means the model is in the catalog. managed: true means it
can back a managed provider — a managed provider is priced at naturali's own
cost, so only models with an authoritative published price qualify. The
Anthropic Claude family has none today: it is available in the catalog but
BYOK-only.
Drop the filter and you may pick a model this step cannot use, which fails fast rather than provisioning something unpriced:
{
"error": {
"code": "bad_request",
"message": "`model` is not available as a managed model."
}
}
If you want a model that reports managed: false, register it as
BYOK with your own key instead.
Use the id to create the provider — model on a managed provider takes
the catalog id, not provider_model:
- CLI
- SDK
- curl
naturali create-provider \
--kind managed \
--model amazon.nova-lite-v1:0 \
--name nova-lite-managed
const { data: provider } = await naturali.providers.createProvider({
path: { project_id: PROJECT },
body: {
kind: 'managed',
model: 'amazon.nova-lite-v1:0',
name: 'nova-lite-managed',
},
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/providers" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"kind": "managed",
"model": "amazon.nova-lite-v1:0",
"name": "nova-lite-managed"
}'
{
"id": "aip_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "nova-lite-managed",
"provider": "bedrock",
"default_model": "amazon.nova-lite-v1:0",
"kind": "managed",
"status": "active",
"has_secret": false,
"base_url": null
}
has_secret is false and stays that way: a managed provider has no credential
of yours to hold.
BYOK runs your agents on your own provider account. You supply the provider slug, the model string that provider expects, and the credential:
- CLI
- SDK
- curl
naturali create-provider \
--kind byok \
--provider anthropic \
--default-model claude-haiku-4-5-20251001 \
--api-key "$ANTHROPIC_API_KEY" \
--name anthropic-byok
const { data: provider } = await naturali.providers.createProvider({
path: { project_id: PROJECT },
body: {
kind: 'byok',
provider: 'anthropic',
default_model: 'claude-haiku-4-5-20251001',
api_key: process.env.ANTHROPIC_API_KEY,
name: 'anthropic-byok',
},
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/providers" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg key "$ANTHROPIC_API_KEY" '{
kind: "byok",
provider: "anthropic",
default_model: "claude-haiku-4-5-20251001",
api_key: $key,
name: "anthropic-byok"
}')"
{
"id": "aip_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "anthropic-byok",
"provider": "anthropic",
"default_model": "claude-haiku-4-5-20251001",
"kind": "byok",
"status": "active",
"has_secret": true
}
Note what came back: has_secret: true, and no credential. BYOK secrets are
write-only — no read, on any endpoint, ever returns the key you just sent. What
you can still observe is whether one is on file and whether it is healthy.
provider is one of openai, anthropic, google, xai, groq, ollama,
azure, bedrock, gateway, custom. For a gateway or self-hosted endpoint,
add base_url. For Bedrock with IAM credentials, api_key takes a JSON object
string: {"accessKeyId":"…","secretAccessKey":"…"}.
Either way you now have a provider id:
export PROVIDER=aip_V1StGXR8Z5jdHi6B
3. Validate it with one generation
status: "active" says the provider was accepted, not that its credential and
model actually work. The proof is one round trip through it: a minimal
agent bound to the provider, and a single
generation. This is a smoke test — everything
agents and generations can do is the subject of
Your first agent generation, not this step.
First the agent — provider_id is the only required field:
- CLI
- SDK
- curl
naturali create-agent \
--provider-id aip_V1StGXR8Z5jdHi6B \
--name provider-smoke-test
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: PROJECT },
body: { provider_id: PROVIDER, name: 'provider-smoke-test' },
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/agents" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"provider_id": "aip_V1StGXR8Z5jdHi6B",
"name": "provider-smoke-test"
}'
export AGENT=agent_V1StGXR8Z5jdHi6B
Then one generation through it:
- CLI
- SDK
- curl
naturali create-generation \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait \
--messages '[{"role":"user","content":"Reply with the single word: ok"}]'
const { data: generation } = await naturali.generations.createGeneration({
path: { project_id: PROJECT, agent_id: AGENT },
query: { wait: true },
body: {
messages: [{ role: 'user', content: 'Reply with the single word: ok' }],
},
});
console.log(generation?.status, generation?.text); // 'completed' 'ok'
curl -sS -X POST \
"$NATURALI_API/projects/$PROJECT/agents/$AGENT/generations?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"messages": [
{ "role": "user", "content": "Reply with the single word: ok" }
]
}'
{
"id": "gen_V1StGXR8Z5jdHi6B",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"status": "completed",
"text": "ok",
"object": null,
"tool_calls": null
}
status: "completed" is the validation: the credential resolved, the model
answered, the provider works. If the call fails here, the error names the layer
that rejected it — a bad BYOK key fails at the provider, a wrong model string
at the model.
That's the value delivered: a project with a working provider in it, ready for any agent to run against.
What's next
- Bind a real agent to this provider, run generations and read exactly what each one cost with Your first agent generation.
- Everything a provider can be — kinds, health, rotation — is on the Providers module page.