loopling.ai

Developer API

Messages API (Claude) · loopling

Guide · Video · Responses · Messages · Billing & Ink

Overview

POST /v1/messages speaks the Anthropic Messages protocol and serves three Claude models:

ModelAliasInk / MTok inputInk / MTok output
claude-fable-5-1fable1,0635,313
claude-opus-5opus5322,657
claude-sonnet-5sonnet2131,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.

Request

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).

Supported parameters

FieldNotes
modelclaude-fable-5-1 / fable, claude-opus-5 / opus, claude-sonnet-5 / sonnet. Required (unknown_model).
messagesA non-empty array of user / assistant turns with string content or text, tool_use and tool_result blocks (missing_messages, invalid_message). Required.
max_tokensRequired, 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.
systemA string or an array of text blocks; cache_control on a block enables prompt caching.
toolsClient 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_configOutput-shaping options the vendor accepts for the model, including effort.
temperature, top_p, top_kAs 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_sequencesstring[].
streamtrue for SSE — see Streaming.
metadataAccepted, 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.

Streaming

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.

Tools

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

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.

Response schema

FieldTypeNotes
idstringmsg_…
type"message"
role"assistant"
modelstringThe product id, for example claude-opus-5.
contentblock[]text, tool_use and thinking blocks.
stop_reason"end_turn" \"max_tokens" \"stop_sequence" \"tool_use"
stop_sequencestring \null
usageobjectinput_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens.

Anthropic SDK

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.

Errors

Beyond the shared error types, this endpoint answers:

HTTPtypecodeWhen
400invalid_request_errorinvalid_body, missing_messages, invalid_message, invalid_type, invalid_enum, invalid_toolA field is missing or fails validation; param names it.
400invalid_request_errormissing_max_tokensmax_tokens is missing or not a positive integer.
400invalid_request_errortoo_largemax_tokens above 64,000.
400invalid_request_errorunsupported_parameterA rejected field (listed above), a server tool, a media block, thinking.disabled on Fable, or an unknown top-level field.
400invalid_request_errorunknown_modelmodel is not one of the three ids or aliases.
400invalid_request_errorupstream_rejectedThe 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.
402insufficient_ink_errorinsufficient_inkThe wallet cannot cover the prompt plus max_tokens.
402insufficient_ink_errorkey_ink_limit_reachedThe key's Ink cap would be crossed.
413invalid_request_errorpayload_too_largeThe prompt exceeds the model's context, or the body exceeds 4 MiB.
429rate_limit_errorrate_limit_exceededMore than 60 requests in a minute on this key.
502provider_errorprovider_errorThe vendor failed after the request was sent, or answered without a usable usage receipt; the hold is kept until it reconciles.
503provider_errorprovider_unavailableThe 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.

API overview

loopling.ai · grows with you, grows itself

hello@loopling.ai · community · pricing · Enterprise · API · privacy · terms