Developer API
Guide · Video · Responses · Messages · Billing & Ink
POST /v1/messages speaks the Anthropic Messages protocol and serves three Claude models:
| Model | Alias | Ink / MTok input | Ink / MTok output |
|---|---|---|---|
claude-fable-5-1 | fable | 1,063 | 5,313 |
claude-opus-5 | opus | 532 | 2,657 |
claude-sonnet-5 | sonnet | 213 | 1,063 |
Cache reads are billed at the cache-read rate and cache writes at the 1-hour cache-write rate; the full table is on Billing & Ink. A 10,000-in / 2,000-out call is 11 Ink on Opus 5, 5 Ink on Sonnet 5 and 22 Ink on Fable 5.1.
The Claude lane opens after its vendor certification. Until then the three catalogue cards report available: false and calls answer 503 provider_unavailable; GET /v1/models/opus says where a deployment stands.
The request and response shapes are the ones the Anthropic SDK produces, so pointing the SDK at Loopling is a base URL change — see Anthropic SDK. v1 is text only: text content blocks in and out, client tools, thinking, prompt caching, streaming. Image, document, video and audio blocks answer 400 unsupported_parameter.
curl
curl https://loopling.ai/v1/messages \
-H "Authorization: Bearer $LOOPLING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Give me three shot ideas for a night harbour scene."}
]
}'Node
const res = await fetch('https://loopling.ai/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Give me three shot ideas for a night harbour scene.' }],
}),
})
const message = await res.json()
const text = message.content.filter((b) => b.type === 'text').map((b) => b.text).join('')Python
res = requests.post(
"https://loopling.ai/v1/messages",
headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
json={
"model": "claude-sonnet-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Give me three shot ideas for a night harbour scene."}],
},
)
message = res.json()
text = "".join(block["text"] for block in message["content"] if block["type"] == "text")Anthropic SDK
import anthropic
# auth_token sends "Authorization: Bearer …", which is what Loopling reads.
client = anthropic.Anthropic(
base_url="https://loopling.ai/v1",
auth_token=os.environ["LOOPLING_API_KEY"],
)
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Give me three shot ideas for a night harbour scene."}],
)
print(message.content[0].text)The response is the standard message object:
{
"id": "msg_01J9X6K3M8Q2",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-5",
"content": [
{ "type": "text", "text": "1. A lone lantern …" }
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 27,
"output_tokens": 96,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
usage is the vendor's own count and is what Loopling settles on: input_tokens, output_tokens, cache_creation_input_tokens (cache writes, at the 1-hour rate) and cache_read_input_tokens each at their published rate. A non-streaming response also carries X-Loopling-Ink-Charged (the settled Ink) and X-Loopling-Request-Id (the id of the request record in Settings → API).
| Field | Notes | |||
|---|---|---|---|---|
model | claude-fable-5-1 / fable, claude-opus-5 / opus, claude-sonnet-5 / sonnet. Required (unknown_model). | |||
messages | A non-empty array of user / assistant turns with string content or text, tool_use and tool_result blocks (missing_messages, invalid_message). Required. | |||
max_tokens | Required, a positive integer up to 64,000 (missing_max_tokens, too_large). Loopling reserves Ink for this cap, so set it to what the reply needs. | |||
system | A string or an array of text blocks; cache_control on a block enables prompt caching. | |||
tools | Client tools (name, description, input_schema), with type omitted or "custom". Server tools answer unsupported_parameter; a tool without a name answers invalid_tool. | |||
tool_choice | `{ type: "auto" \ | "any" \ | "tool" \ | "none" }`. |
thinking | { type: "adaptive" }, { type: "enabled", budget_tokens } or { type: "disabled" } — except that Fable 5.1 does not accept disabled (unsupported_parameter, with the hint to use adaptive and output_config.effort). | |||
output_config | Output-shaping options the vendor accepts for the model, including effort. | |||
temperature, top_p, top_k | As in the protocol; the vendor's per-model rules apply and a rejection comes back as 400 upstream_rejected with the vendor's message. | |||
stop_sequences | string[]. | |||
stream | true for SSE — see Streaming. | |||
metadata | Accepted, but Loopling replaces it on the wire with { user_id: <a hash of your key> } so the vendor sees one stable anonymous actor per key. |
Not supported in v1, answered with 400 unsupported_parameter: context_management, service_tier, mcp_servers, container, speed, fallbacks, fallback_credit_token, inference_geo, output_format (use output_config.format), betas, media content blocks, server tools, thinking.type: "disabled" on Fable, and any top-level field not listed above.
Headers: Loopling sets anthropic-version itself. Of the anthropic-beta values a client may send, only prompt-caching-2024-07-31 is forwarded; others are dropped silently.
Each request holds Ink for the prompt (counted as cache writes at the tier) plus max_tokens, and settles to the vendor's usage when the message completes — see Reserve, then settle. A prompt beyond the model's 200,000-token context answers 413 payload_too_large, as does a body above 4 MiB.
With stream: true the response is text/event-stream, and the events are the protocol's own — message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop — followed by one Loopling frame after message_stop:
event: loopling.usage
data: {"type": "loopling.usage", "ink_charged": 5, "usd_payg_charged": 0.05, "request_id": "req_01J9X6K3M8Q2"}
ink_charged is the settled figure for this call; usd_payg_charged is the same at $1 = 100 Ink; request_id is the id of the request record in Settings → API. The Anthropic SDK ignores the event; read Ink from Settings → API or GET /v1/me when using it.
curl
curl -N https://loopling.ai/v1/messages \
-H "Authorization: Bearer $LOOPLING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-fable-5-1",
"max_tokens": 512,
"stream": true,
"messages": [{"role": "user", "content": "Write a two-line logline for a harbour film."}]
}'
# … event: content_block_delta frames, then:
# event: message_stop
# event: loopling.usage
# data: {"ink_charged": 5, "usd_payg_charged": 0.05, "request_id": "req_…"}Node
const res = await fetch('https://loopling.ai/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-fable-5-1',
max_tokens: 512,
stream: true,
messages: [{ role: 'user', content: 'Write a two-line logline for a harbour film.' }],
}),
})
// Split the SSE body on blank lines exactly as in the Responses sample; the
// text arrives in content_block_delta frames as data.delta.text.Python
with requests.post(
"https://loopling.ai/v1/messages",
headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
json={
"model": "claude-fable-5-1",
"max_tokens": 512,
"stream": True,
"messages": [{"role": "user", "content": "Write a two-line logline for a harbour film."}],
},
stream=True,
) as res:
event = None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = json.loads(line[6:])
if event == "content_block_delta" and data["delta"].get("type") == "text_delta":
print(data["delta"]["text"], end="", flush=True)
elif event == "loopling.usage":
print("\n", data)Anthropic SDK
import anthropic
# auth_token sends "Authorization: Bearer …", which is what Loopling reads.
client = anthropic.Anthropic(base_url="https://loopling.ai/v1", auth_token=os.environ["LOOPLING_API_KEY"])
with client.messages.stream(
model="claude-fable-5-1",
max_tokens=512,
messages=[{"role": "user", "content": "Write a two-line logline for a harbour film."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)If the reply reaches max_tokens, message_delta carries stop_reason: "max_tokens" and the call is charged for what the vendor reports. A client that disconnects mid-stream is still charged for what the vendor produced — Loopling does not abort the vendor when the client leaves. If the vendor's stream ends without a usage receipt, the stream ends with an error frame — code: "usage_unknown" — and the Ink hold is kept until the vendor's invoice reconciles it; it is never silently charged.
Client tools work as in the protocol: declare them in tools, receive tool_use blocks with an id, name and input, run the tool yourself, and continue the conversation with a user turn carrying a tool_result block for that id. Every turn is a full request — Loopling keeps no conversation state, so send the whole transcript each time.
curl
curl https://loopling.ai/v1/messages \
-H "Authorization: Bearer $LOOPLING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"tools": [{
"name": "get_weather",
"description": "Current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}],
"messages": [{"role": "user", "content": "Should we shoot the harbour scene in Lisbon tonight?"}]
}'
# The reply carries a tool_use block; run the tool and send a tool_result block back.Node
const res = await fetch('https://loopling.ai/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-opus-5',
max_tokens: 1024,
tools: [{
name: 'get_weather',
description: 'Current weather for a city.',
input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
}],
messages: [{ role: 'user', content: 'Should we shoot the harbour scene in Lisbon tonight?' }],
}),
})
const message = await res.json()
const toolUse = message.content.find((b) => b.type === 'tool_use')
// Run the tool, then continue with a tool_result block carrying toolUse.id.Python
res = requests.post(
"https://loopling.ai/v1/messages",
headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
json={
"model": "claude-opus-5",
"max_tokens": 1024,
"tools": [{
"name": "get_weather",
"description": "Current weather for a city.",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}],
"messages": [{"role": "user", "content": "Should we shoot the harbour scene in Lisbon tonight?"}],
},
)
message = res.json()
tool_use = next(b for b in message["content"] if b["type"] == "tool_use")
# Run the tool, then continue with a tool_result block carrying tool_use["id"].Anthropic SDK
import anthropic
client = anthropic.Anthropic(base_url="https://loopling.ai/v1", auth_token=os.environ["LOOPLING_API_KEY"])
message = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Current weather for a city.",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}],
messages=[{"role": "user", "content": "Should we shoot the harbour scene in Lisbon tonight?"}],
)
tool_use = next(b for b in message.content if b.type == "tool_use")stop_reason is "tool_use" when the model is waiting on a result.
thinking is passed through as sent. Fable 5.1 runs adaptive thinking: send { "type": "adaptive" } and steer depth with output_config.effort; { "type": "disabled" } answers 400 unsupported_parameter with that hint. Opus 5 and Sonnet 5 accept { "type": "enabled", "budget_tokens": N } with budget_tokens below max_tokens, or { "type": "disabled" }. Thinking tokens are output tokens and are billed as such; thinking blocks arrive in content (and as thinking_delta frames when streaming) and must be sent back unchanged when you continue a tool-use turn.
| Field | Type | Notes | |||
|---|---|---|---|---|---|
id | string | msg_… | |||
type | "message" | ||||
role | "assistant" | ||||
model | string | The product id, for example claude-opus-5. | |||
content | block[] | text, tool_use and thinking blocks. | |||
stop_reason | "end_turn" \ | "max_tokens" \ | "stop_sequence" \ | "tool_use" | |
stop_sequence | string \ | null | |||
usage | object | input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens. |
Loopling reads the key from Authorization: Bearer, which the Anthropic SDK sends when the key is given as auth_token (Python) or authToken (JavaScript) — not as api_key, which becomes the x-api-key header Loopling does not read. Set base_url to Loopling and you are done.
import anthropic
client = anthropic.Anthropic(base_url="https://loopling.ai/v1", auth_token=os.environ["LOOPLING_API_KEY"])
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({ baseURL: 'https://loopling.ai/v1', authToken: process.env.LOOPLING_API_KEY })
const message = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Give me three shot ideas for a night harbour scene.' }],
})
console.log(message.content[0].text)
The SDK's anthropic-version header is replaced by Loopling's own; its anthropic-beta header is forwarded only for prompt-caching-2024-07-31.
Beyond the shared error types, this endpoint answers:
| HTTP | type | code | When |
|---|---|---|---|
| 400 | invalid_request_error | invalid_body, missing_messages, invalid_message, invalid_type, invalid_enum, invalid_tool | A field is missing or fails validation; param names it. |
| 400 | invalid_request_error | missing_max_tokens | max_tokens is missing or not a positive integer. |
| 400 | invalid_request_error | too_large | max_tokens above 64,000. |
| 400 | invalid_request_error | unsupported_parameter | A rejected field (listed above), a server tool, a media block, thinking.disabled on Fable, or an unknown top-level field. |
| 400 | invalid_request_error | unknown_model | model is not one of the three ids or aliases. |
| 400 | invalid_request_error | upstream_rejected | The vendor's gateway refused the request before running the model (its 400 / 404 / 413 / 415 / 422, including a sampling parameter the model does not accept); the message is the vendor's; the hold is released. |
| 402 | insufficient_ink_error | insufficient_ink | The wallet cannot cover the prompt plus max_tokens. |
| 402 | insufficient_ink_error | key_ink_limit_reached | The key's Ink cap would be crossed. |
| 413 | invalid_request_error | payload_too_large | The prompt exceeds the model's context, or the body exceeds 4 MiB. |
| 429 | rate_limit_error | rate_limit_exceeded | More than 60 requests in a minute on this key. |
| 502 | provider_error | provider_error | The vendor failed after the request was sent, or answered without a usable usage receipt; the hold is kept until it reconciles. |
| 503 | provider_error | provider_unavailable | The model is not enabled or not yet certified on this deployment, the vendor is at capacity, or the vendor is rate-limiting; retry after Retry-After. |
On a stream, a vendor failure after dispatch arrives as a final event: error frame instead — code: "usage_unknown" when no usage receipt arrived (hold kept), code: "incomplete_stream" when the stream ended without a terminal response.
loopling.ai · grows with you, grows itself
hello@loopling.ai · community · pricing · Enterprise · API · privacy · terms