Create a provider
By the end of this tutorial you will have a working, validated AI provider — the model credential every agent in your project generates through.
You don't need one. Enable naturali models gives your project a working provider in a single call, with no credential of your own — this tutorial is the path for bringing your own key.
Four steps:
- Create a project — the tenancy boundary everything else lives in.
- Store your model credential as a secret.
- Register the provider that uses it.
- Validate it by asking the vendor which models the credential can run.
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 an account-scoped nat_sk_… API key (or a
session JWT from Auth) exported as NATURALI_TOKEN, and an
API key from a model vendor — this tutorial uses OpenAI. A project-scoped key
cannot create projects: step 1 answers 403 access_denied with one.
export NATURALI_TOKEN=nat_sk_...
export OPENAI_API_KEY=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 });
See the SDK guide.
Nothing to install — every call sends the credential as a bearer header:
curl https://api.naturali.ai/v1/users/me \
-H "Authorization: Bearer $NATURALI_TOKEN"
1. Create a project
A project is the isolation boundary: providers, secrets, agents and everything else belong to exactly one, and your credential is authorized against it per request.
- CLI
- SDK
- curl
naturali create-project --name "getting started"
const { data: project } = await naturali.projects.createProject({
body: { name: 'getting started' },
});
curl -X POST https://api.naturali.ai/v1/projects \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "getting started" }'
{
"id": "proj_V1StGXR8Z5jdHi6B",
"name": "getting started",
"role": "owner",
"owner_user_id": "user_V1StGXR8Z5jdHi6B",
"status": "active"
}
You are the project's owner — the role that may do anything in it, including
delete it — and its billing owner.
export PROJECT=proj_V1StGXR8Z5jdHi6B
2. Store your model credential
Credentials are never passed inline to a provider. Store the key as a secret first: the value is encrypted on write and no endpoint ever reads it back.
- CLI
- SDK
- curl
naturali create-secret \
--project-id "$PROJECT" \
--name openai-production \
--value "$OPENAI_API_KEY"
const { data: secret } = await naturali.secrets.createSecret({
path: { project_id: process.env.PROJECT! },
body: { name: 'openai-production', value: process.env.OPENAI_API_KEY! },
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/secrets" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"name\": \"openai-production\", \"value\": \"$OPENAI_API_KEY\" }"
{
"id": "sec_l4gKTvGnouZ68huG",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "openai-production",
"has_value": true,
"created_at": "2026-10-03T08:12:11.407Z",
"updated_at": "2026-10-03T08:12:11.407Z"
}
has_value is how a read reports that a value is stored — the value itself is
gone from the API surface the moment you write it.
export SECRET=sec_l4gKTvGnouZ68huG
3. Register the provider
The provider record names the vendor, the model to use when an agent names none, and the secret to authenticate with.
- CLI
- SDK
- curl
naturali create-ai-provider \
--project-id "$PROJECT" \
--name openai-production \
--provider openai \
--default-model gpt-4o-mini \
--secret-id "$SECRET"
const { data: provider } = await naturali.aiProviders.createAiProvider({
path: { project_id: process.env.PROJECT! },
body: {
name: 'openai-production',
provider: 'openai',
default_model: 'gpt-4o-mini',
secret_id: process.env.SECRET!,
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/ai-providers" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"openai-production\",
\"provider\": \"openai\",
\"default_model\": \"gpt-4o-mini\",
\"secret_id\": \"$SECRET\"
}"
{
"id": "aip_JPvMSUTATh7C6Bn6",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"secret_id": "sec_l4gKTvGnouZ68huG",
"name": "openai-production",
"provider": "openai",
"default_model": "gpt-4o-mini",
"created_at": "2026-10-03T08:12:12.364Z",
"updated_at": "2026-10-03T08:12:12.364Z"
}
The provider is created whether or not the key works — nothing calls the vendor yet. The next step is what proves it.
export PROVIDER=aip_JPvMSUTATh7C6Bn6
A vendor that needs no credential — a local ollama, say — takes a base_url
and no secret_id instead.
4. Validate it
Ask the vendor
which models this record can actually run.
The call goes out with
this provider's credential, so a 200 proves the whole chain — secret,
provider, vendor — works.
- CLI
- SDK
- curl
naturali list-ai-provider-models \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER"
const { data } = await naturali.aiProviders.listAiProviderModels({
path: {
project_id: process.env.PROJECT!,
ai_provider_id: process.env.PROVIDER!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/ai-providers/$PROVIDER/models" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"provider": "openai",
"models": [
{ "id": "gpt-4o-mini" },
{ "id": "gpt-4o" },
{ "id": "o3-mini" }
]
}
Each id is ready to use as default_model or as an agent's model. A key the
vendor rejects answers 502:
{
"error": {
"code": "MODEL_LISTING_FAILED",
"message": "The openai provider rejected the model listing request (HTTP 401)."
}
}
Rotate the secret's value and try again; the provider record does not need recreating.
What's next
- Your first agent generation — create an agent on this provider and run it.
- Model Routes — put two providers behind one ordered route so a vendor outage fails over instead of failing.
- Secrets — rotation, and how a tool interpolates a secret into an outbound request.