Skip to main content

Providers

Endpoints for managing AI providers (the connections to an AI service such as Anthropic, OpenAI, Google, Ollama, or Amazon Bedrock) and the models each provider exposes.

All provider routes require permission.ai.manage (superuser). A provider's secret is sent in the key field, stored encrypted, and never returned in clear text. The AI module must not be enabled on the tenant to read providers, but creating a custom provider requires BYOK AI (see POST below).

GET /api/providers

List the tenant's providers, sorted by name.

Authentication: permission.ai.manage

Response:

A HAL collection under _embedded["inf:provider"]. Each provider links to its models (inf:models). The key is never returned raw or encrypted: a provider with a stored key serializes the masked indicator "****", and a keyless provider serializes null.

List providersGET /api/providers
Returns the tenant’s configured providers sorted by name. The `key` is never returned raw or encrypted: keyed providers serialize a masked indicator, keyless providers serialize no key (I5-12597).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers"
}
},
"start": 0,
"count": 3,
"total": 3,
"_embedded": {
"inf:provider": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Anthropic",
"slug": "anthropic",
"type": "anthropic",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000002"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000002/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"name": "OpenAI",
"slug": "openai",
"type": "openai",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000003"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000003/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000003",
"name": "OpenAI",
"slug": "openai-1",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

Provider properties:

FieldTypeDescription
idstring (UUID)Provider id
namestringDisplay name
slugstringURL-safe name, auto-derived
typestringProvider-driver id (anthropic, openai, google, ollama, bedrock)
keystring | nullMasked credential indicator ("****") or null
managedbooleanSystem-managed (License Manager controlled) provider
internalbooleanInternal-only provider
settingsobject | nullNon-secret driver settings (e.g. Bedrock region)
dataobject | nullDriver-specific configuration
createdAt / updatedAtdateTimestamps

POST /api/providers

Create a custom provider.

Authentication: permission.ai.manage

Pre-blocks: permission.ai.manage, BYOK check (server.ai.byok())

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
typestringYesProvider-driver id (see GET /api/provider-types)
keystringNoSecret credential (API key, or a JSON envelope for Bedrock). Stored encrypted.
settingsobjectNoNon-secret driver settings
dataobjectNoDriver-specific configuration

There is no credential object; the secret goes directly in key.

Example Request:

{
"name": "Acme OpenAI",
"type": "openai",
"key": "sk-your-api-key"
}

Response:

Responds 201 Created with the provider (key masked) and a Location header.

Create a custom providerPOST /api/providers
Payload is { name, type, settings?, data?, key? } — there is no `credential` object. `type` is a provider-driver id. Secrets go in `key` (stored encrypted, never returned). Requires BYOK; non-BYOK superusers get 403. Responds 201 with a Location header.
Request body
{
"name": "Acme OpenAI",
"type": "openai",
"key": "sk-your-api-key"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"type": "openai",
"key": "****",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"slug": "acme-openai",
"settings": null,
"data": null,
"internal": false,
"managed": false
}
Captured from the API examples test suite; ids and timestamps are normalized.
BYOK required

Custom providers require BYOK AI. Without it the route answers 403, even for a superuser.

Create a provider without BYOKPOST /api/providers
Custom providers require BYOK AI. Without it the route answers 403 regardless of superuser status.
Request body
{
"name": "Blocked Provider",
"type": "anthropic"
}
Response · 403
{
"error": "Forbidden",
"message": "Custom AI providers require BYOK AI"
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/providers/{id}

Retrieve a single provider (key masked).

Authentication: permission.ai.manage

Pre-blocks: provider.lookup, permission.ai.manage

Response:

Get a providerGET /api/providers/00000000-0000-4000-8000-000000000001
Returns one provider with its key masked.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"slug": "acme-openai",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/providers/{id}

Update a provider.

Authentication: permission.ai.manage

Pre-blocks: provider.lookup, permission.ai.manage

Request Body:

FieldTypeDescription
namestringUpdate display name
typestringChange provider type (custom providers only)
keystring | nullRotate the secret; null or "" clears it
settingsobjectDriver settings (merged, not replaced)
dataobjectDriver-specific configuration

settings is always merged with the existing value (partial-update semantics), so driver-derived metadata such as Bedrock's accessKeyIdHint survives a region-only update.

Managed / internal providers accept only a key swap and updates to the driver's allow-listed settings (e.g. Bedrock opts in region). Identity fields (name, type, data) stay License-Manager controlled. A settings-only PUT whose keys are all outside the allowlist collapses to an empty patch and answers 403.

Example Request:

{ "key": "sk-rotated-api-key" }

Response:

Responds 200 with the updated provider.

Rotate a provider keyPUT /api/providers/00000000-0000-4000-8000-000000000001
PUT payload is { name?, type?, settings?, data?, key? }. `settings` is merged (partial update), not replaced. Send a new `key` to rotate the credential.
Request body
{
"key": "sk-rotated-api-key"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"slug": "acme-openai",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/providers/{id}

Delete a provider and cascade-delete its models.

Authentication: permission.ai.manage

Pre-blocks: provider.lookup, permission.ai.manage

Response:

Responds 200 with an empty body.

Delete a providerDELETE /api/providers/00000000-0000-4000-8000-000000000001
Deletes a non-managed provider and cascades to its models. Managed providers cannot be deleted (403).
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Cascade deletion

Deleting a provider removes all of its models. Managed providers cannot be deleted (403).


Driver-specific payload validation

Each provider driver may apply additional validation and normalization to the POST/PUT payload before the row is persisted. The driver receives the parsed payload, may mutate it (e.g., to derive non-secret hints), and may throw a 400 if the shape is invalid for that driver.

Drivers without custom validation (OpenAI, Anthropic, Google, Ollama) accept any payload that satisfies the route-level Joi schema.

Amazon Bedrock

The bedrock provider expects credentials encoded as a JSON envelope in the key field, plus a region in settings. Two envelope shapes are accepted:

Bearer token (Bedrock long-term API key):

{
"name": "AWS Bedrock",
"type": "bedrock",
"key": "{\"apiKey\":\"AWS_BEARER_TOKEN_BEDROCK_value\"}",
"settings": {
"region": "us-east-1"
}
}

AWS access keys (SigV4 authentication):

{
"name": "AWS Bedrock",
"type": "bedrock",
"key": "{\"accessKeyId\":\"AKIA…\",\"secretAccessKey\":\"…\"}",
"settings": {
"region": "us-east-1"
}
}

The driver derives a non-secret accessKeyIdHint (last 4 chars of the access key ID) and stores it in settings for read-back UX. On PUT, settings is merged with existing values rather than replaced, so partial updates (e.g., region only) preserve previously-derived metadata.

Validation rejects:

  • A key that is not valid JSON
  • A JSON envelope missing both apiKey and accessKeyId
  • A POST without settings.region

GET /api/provider-types

Discover the provider drivers registered in the system.

Authentication: permission.ai.manage

Response:

A HAL collection of detective discoveries. Each item's value.name and value.description are i18n message keys (e.g. drivers:providers.openai.name) that the client resolves to the active locale. Use the value.provider.type as the type when creating a provider.

Discover provider typesGET /api/provider-types
Lists the provider drivers registered in the system (anthropic, openai, google, ollama, bedrock, ...). Use these ids as the `type` when creating a provider.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/provider-types"
}
},
"items": [
{
"detective": "openai",
"id": "openai:0",
"value": {
"name": "drivers:providers.openai.name",
"description": "drivers:providers.openai.description",
"provider": {
"type": "openai",
"name": "drivers:providers.openai.name"
}
}
},
{
"detective": "anthropic",
"id": "anthropic:0",
"value": {
"name": "drivers:providers.anthropic.name",
"description": "drivers:providers.anthropic.description",
"provider": {
"type": "anthropic",
"name": "drivers:providers.anthropic.name"
}
}
},
{
"detective": "google",
"id": "google:0",
"value": {
"name": "drivers:providers.google.name",
"description": "drivers:providers.google.description",
"provider": {
"type": "google",
"name": "drivers:providers.google.name"
}
}
},
"… 2 more items (5 total) — omitted from docs"
],
"start": 0,
"count": 5,
"total": 5
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/providers/{id}/models

List the models belonging to a provider.

Authentication: permission.ai.manage

Pre-blocks: provider.lookup, permission.ai.manage

Response:

A HAL collection under _embedded["inf:model"].

List a provider’s modelsGET /api/providers/00000000-0000-4000-8000-000000000001/models
Returns the Models whose providerId is this provider.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"start": 0,
"count": 1,
"total": 1,
"_embedded": {
"inf:model": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models/00000000-0000-4000-8000-000000000002"
}
},
"naturalId": "claude-haiku-45",
"permissions": {},
"promptPpm": null,
"completionPpm": null,
"cacheReadPpm": null,
"cacheWriteShortPpm": null,
"cacheWriteLongPpm": null,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"providerId": "00000000-0000-4000-8000-000000000001",
"referencesId": null,
"name": "Claude Haiku 4.5",
"slug": "claude-haiku-45",
"model": "claude-haiku-4-5",
"options": null,
"chat": false,
"moderated": false,
"contextWindow": 200000,
"maxOutputTokens": null,
"instructions": null,
"description": null,
"managed": false,
"internal": false,
"deprecatedAt": null,
"web": false,
"attachments": false,
"generateImage": false,
"createEmbeddings": false,
"score": null,
"pricingTier": null,
"badges": null,
"tier": null,
"customized": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"provider": {
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Anthropic",
"slug": "anthropic",
"type": "anthropic",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/providers/{id}/models

Add a model to a provider. providerId is taken from the URL.

Authentication: permission.ai.manage

Pre-blocks: permission.ai.manage, provider.lookup

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
modelstringNoProvider's model identifier (e.g. claude-opus-4)
optionsobjectNoDriver-specific model options

Response:

Responds 200 with the created model. Pricing columns (promptPpm, completionPpm, the cache*Ppm family) and capability flags (web, attachments, generateImage, createEmbeddings) default to null/false and are managed separately.

Add a model to a providerPOST /api/providers/00000000-0000-4000-8000-000000000001/models
Payload is { name (required), model?, options? }. providerId is taken from the URL.
Request body
{
"name": "Claude Opus 4",
"model": "claude-opus-4"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"naturalId": "claude-opus-4",
"permissions": {},
"promptPpm": null,
"completionPpm": null,
"cacheReadPpm": null,
"cacheWriteShortPpm": null,
"cacheWriteLongPpm": null,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"web": false,
"attachments": false,
"generateImage": false,
"createEmbeddings": false,
"customized": false,
"name": "Claude Opus 4",
"model": "claude-opus-4",
"providerId": "00000000-0000-4000-8000-000000000001",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"slug": "claude-opus-4",
"options": null,
"instructions": null,
"moderated": false,
"chat": false,
"internal": false,
"contextWindow": 200000,
"description": null,
"referencesId": null,
"deprecatedAt": null,
"managed": false,
"score": null,
"pricingTier": null,
"badges": null,
"maxOutputTokens": null,
"tier": null
}
Captured from the API examples test suite; ids and timestamps are normalized.