Loop
How it worksThe loopWhy LoopPricingDocs
docs v0 · open wave

Loop API reference

One endpoint — remote MCP at https://stayinloop.dev/mcp or REST at api.stayinloop.dev/v1/* — where agents search, verify, and act on real local businesses. Four tools. Structured errors. Every field stamped with when it was last seen.

Platform guides

60-second setup guides for the most common MCP clients. Each covers both the settings UI and the config-file approach.

Claude
Desktop · claude.ai · Claude Code CLI
Setup guide →
ChatGPT
MCP connector · Custom GPT via OpenAPI
Setup guide →
Cursor
Settings UI · .cursor/mcp.json
Setup guide →
Windsurf
Settings UI · ~/.codeium/windsurf/mcp_config.json
Setup guide →
VS Code
Command Palette · .vscode/mcp.json · Copilot Agent mode
Setup guide →
Python
requests · httpx async · OpenAI SDK · LangChain
Code examples →
OpenAI Agents SDK
@function_tool · gpt-4o · o3 · async agents
Code examples →
LangChain
@tool · LangGraph · create_react_agent · async
Code examples →
LlamaIndex
FunctionTool · FunctionAgent · AgentWorkflow · async
Code examples →
n8n
HTTP Request · AI Agent · $fromAI() · workflow automation
Workflow guide →
Make
HTTP module · Iterator · MCP Server · automation scenarios
Scenario guide →
Zapier
Webhooks · Looping by Zapier · Zapier MCP + Loop MCP
Zap guide →
Dify
MCP server · HTTP Request · Agent node · chatflow + workflow
Workflow guide →
Flowise
Custom MCP node · Agent flow · chatflow · HTTP transport
Agent flow guide →
Langflow
MCP Tools node · Settings › MCP Servers · Agent flow · HTTP transport
Agent flow guide →
CrewAI
mcps= shorthand · MCPServerAdapter · multi-agent · tool discovery
Multi-agent guide →
AutoGen
mcp_server_tools · RoundRobinGroupChat · multi-agent · StreamableHttp
Multi-agent guide →
Gemini
Gemini CLI · ~/.gemini/settings.json (httpUrl) · google-genai SDK
Setup guide →
Mistral
Le Chat connector · Agents API · mistralai SDK (MCPClientSSE)
Setup guide →
Langdock
Integrations · Connect remote MCP · streamable HTTP
Setup guide →

Quick start

A complete agent workflow — from first search to closing the loop — in four calls. No key required during the open wave; swap in sk_live_… when you have one.

1

Search for results

Natural-language query in, ranked structured results out.

curl · search
curl https://api.stayinloop.dev/v1/search \
  -H "Authorization: Bearer sk_live_…" \
  -G \
  --data-urlencode 'q=quiet vegan table for 4 tonight' \
  --data-urlencode 'location=Kreuzberg, Berlin'
200 OK
{
  "results": [
    {
      "result_id": "7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234",
      "name": "Al Catzone - Pizza Napovegana",
      "vertical": "restaurants",
      "tags": ["outdoor seating", "vegan only"],
      "address": "Brandesstraße 7, Kreuzberg, Berlin",
      "availability": {
        "status": "likely_open_now",
        "inferred": true,
        "basis": "opening hours last observed 2026-06-09",
        "confidence": 0.75
      },
      "confidence": 0.95,
      "cross_source_confirmed": true,
      "observed_at": "2026-06-10T07:12:09Z",
      "relevance": 12.4,
      "restaurant": {
        "cuisine": ["pizza"],
        "price_band": { "value": "€", "inferred": true },
        "vegan": true,
        "vegetarian": false,
        "outdoor_seating": true
      }
    }
  ],
  "coverage": "Restaurants & salons · Kreuzberg, Berlin",
  "note": "availability is inferred — call verify(result_id) before acting on it."
}
2

Get the full record

Use result_id from the search result. This returns the complete record plus a result_token(valid 30 min) you’ll need for report().

curl · get_details
curl https://api.stayinloop.dev/v1/details/7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234 \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "result_id": "7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234",
  "name": "Al Catzone - Pizza Napovegana",
  "vertical": "restaurants",
  "area": "Kreuzberg",
  "address": "Brandesstraße 7, Kreuzberg, Berlin",
  "location": { "lat": 52.4891, "lng": 13.4142 },
  "attrs": {
    "opening_hours": "Mo-Sa 12:00-22:00",
    "phone": "+49 30 123456",
    "website": "https://alcatzone.de"
  },
  "tags": ["outdoor seating", "vegan only"],
  "availability": { "status": "likely_open_now", "inferred": true, "confidence": 0.75 },
  "confidence": 0.95,
  "cross_source_confirmed": true,
  "observed_at": "2026-06-10T07:12:09Z",
  "source": "OpenStreetMap + Foursquare OS Places (independently confirmed) + agent reports",
  "result_token": "eyJ…",
  "note": "When the outcome is known, call report(result_token, outcome).",
  "restaurant": {
    "cuisine": ["pizza"],
    "price_band": { "value": "€", "inferred": true },
    "vegan": true,
    "vegetarian": false,
    "outdoor_seating": true
  }
}
3

Verify before acting

Optional but recommended. If the record is stale, Loop re-checks it live against OpenStreetMap right now and returns fresh data with a new observed_at.

curl · verify
curl https://api.stayinloop.dev/v1/verify/7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234 \
  -H "Authorization: Bearer sk_live_…" \
  -G --data-urlencode 'claim=Is it open on Sunday evenings?'
200 OK
{
  "result_id": "7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234",
  "name": "Al Catzone - Pizza Napovegana",
  "claim": "Is it open on Sunday evenings?",
  "latest_observation": {
    "type": "recheck",
    "value": "Mo-Sa 12:00-22:00",
    "source": "osm_recheck",
    "confidence": 0.85,
    "observed_at": "2026-06-11T14:03:22Z"
  },
  "observed_at": "2026-06-11T14:03:22Z",
  "record_confidence": 0.95,
  "availability": { "status": "likely_closed_now", "inferred": true, "confidence": 0.85 },
  "note": "Re-verified live against OpenStreetMap just now."
}
4

Report the outcome

Tell Loop what happened. This immediately mutates the record’s confidence and observed_at. Agents that report earn fresher data and higher rate limits.

curl · report
curl -X POST https://api.stayinloop.dev/v1/report \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"result_token": "eyJ…", "outcome": "correct"}'
200 OK
{
  "ok": true,
  "merchant_id": "7f7250b4-3c1a-4e82-b9d1-0a2f5c8e1234",
  "new_confidence": 0.97,
  "observed_at": "2026-06-11T14:07:55Z",
  "note": "Outcome recorded — the record's freshness and confidence were just updated."
}

Connect

Loop exposes the same data and tools over remote MCP (streamable HTTP) and REST. Pick whichever fits your stack.

MCP server

Paste https://stayinloop.dev/mcp into Claude (Settings → Connectors), Cursor, or any MCP-compatible client. Or add it to a config file:

mcp.json · anonymous (open wave)
{
  "mcpServers": {
    "loop": { "url": "https://stayinloop.dev/mcp" }
  }
}
mcp.json · with API key
{
  "mcpServers": {
    "loop": {
      "url": "https://stayinloop.dev/mcp?key=sk_live_…"
    }
  }
}

REST API

Base URL: https://api.stayinloop.dev/v1. All endpoints are no-store — responses are never cached. Pass your key via the Authorization header or ?key= query parameter.

ChatGPT

ChatGPT Business and Enterprise/Edu accounts accept MCP connectors in developer mode. Paste https://stayinloop.dev/mcp, leave authentication empty, and you’re done. Plus / Pro users: use the OpenAPI spec for a custom GPT via Actions at api.stayinloop.dev/v1/openapi.json.

Vercel AI SDK

TypeScript
import { experimental_createMCPClient } from 'ai';
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

const loop = await experimental_createMCPClient({
  transport: { type: 'sse', url: 'https://stayinloop.dev/mcp?key=sk_live_…' },
});

const result = await streamText({
  model: anthropic('claude-sonnet-4-6'),
  tools: await loop.tools(),
  messages: [{ role: 'user', content: 'Find a vegan restaurant open now in Kreuzberg' }],
});

OpenAI function calling

TypeScript
import OpenAI from 'openai';

const client = new OpenAI();

// fetch tool definitions from the OpenAPI spec
const spec = await fetch('https://api.stayinloop.dev/v1/openapi.json').then(r => r.json());

const response = await client.chat.completions.create({
  model: 'gpt-4o',
  tools: spec.tools,   // drop Loop's OpenAPI tools straight in
  messages: [{ role: 'user', content: 'Vegan dinner Kreuzberg tonight' }],
});

Authentication

No key is required during the open wave — you get 30 read calls/min and 10 report calls/min, attributed to an anonymous agent. With an API key you get named agent attribution, higher limits, and priority freshness windows.

ParameterTypeDescription
AuthorizationheaderoptionalBearer sk_live_… — pass your API key here on REST calls.
?key=query paramoptionalAppend key=sk_live_… to any REST or MCP URL as an alternative to the header.

Keys follow the format sk_live_…. To request one, use the form on the homepage or email hello@stayinloop.dev. You can also .

get_details()

Returns the full structured record for a chosen result — all available fields, plus a result_token required to call report(). Calling this records a selection signal.

GEThttps://api.stayinloop.dev/v1/details/:result_id
ParameterTypeDescription
result_idUUIDrequiredThe result_id from a search() response. Must be a valid UUID.

Response: full merchant record including attrs (universal fields: opening_hours, phone, website), a [vertical] sub-object (e.g. restaurant: { cuisine, vegan, … }), location (lat/lng if known), source (data provenance), and result_token.

result_token is valid for 30 minutes. If it expires, call get_details again to get a fresh one. Pass it to report() to close the loop.

verify()

Freshness check before acting. Returns the most recent observation for the record. If the record is stale, Loop re-checks it live against OpenStreetMap right now (demand-driven live re-verification) and returns the fresh result.

GEThttps://api.stayinloop.dev/v1/verify/:result_id
ParameterTypeDescription
result_idUUIDrequiredThe result_id to verify.
claimstringoptionalAn optional natural-language claim to check. Example: "Is it open on Sunday evenings?" Max 200 characters. Echoed back in the response.

Response includes latest_observation (the most recent data point with its source and confidence), record_confidence (current overall confidence of the full record), and availability.

The note field tells you whether data was served from cache or re-verified live: “Re-verified live against OpenStreetMap just now” vs “Last observed from seed data plus agent reports”.

report()

Close the loop. Tell Loop what actually happened — the record’s confidence and observed_at are updated immediately. Agents that report earn fresher data and higher rate limits.

POSThttps://api.stayinloop.dev/v1/report
ParameterTypeDescription
result_tokenstringrequiredThe result_token from get_details(). Valid for 30 minutes.
outcomeenumrequiredOne of: correct · wrong · booked · closed · other. correct = info was accurate. wrong = something was incorrect. booked = reservation/order succeeded. closed = place was closed or gone. other = catch-all.
curl
curl -X POST https://api.stayinloop.dev/v1/report \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"result_token": "eyJ…", "outcome": "correct"}'

Response: { ok: true, merchant_id, new_confidence, observed_at, note }

No free text on the write path. Outcome is an enum only — this prevents injection into the signals surface and keeps the data model clean.

Data model

Every merchant record follows the same normalized schema across all verticals. Fields present depend on available data — missing fields are omitted, never returned as null.

Search result fields

ParameterTypeDescription
result_idUUIDrequiredStable identifier for this merchant across all calls.
namestringrequiredMerchant name.
verticalstringrequiredMerchant category. Determines which vertical sub-object is present. E.g. "restaurants" or "salons".
tagsstring[]optionalDerived feature tags. E.g. ["outdoor seating", "vegan only"].
addressstringoptionalHuman-readable address. Formatted as street + housenumber, area, city.
availabilityobjectrequiredSee Availability object below.
confidencenumberrequired0–1 overall record confidence. See Confidence & freshness.
cross_source_confirmedbooleanrequiredTrue if confirmed by both OpenStreetMap and Foursquare OS Places.
observed_atISO 8601requiredTimestamp of the last observation.
relevancenumberrequiredSearch relevance score for this query (not comparable across queries).
[vertical]objectoptionalVertical-specific attributes keyed by vertical name. Present when vertical-specific data is available. See vertical schemas below.

Availability object

ParameterTypeDescription
statusenumrequiredlikely_open_now · likely_closed_now · unknown
inferredtruerequiredAlways true until a merchant connects directly. Never fabricated.
basisstringoptionalHuman-readable basis for the inference. E.g. "opening hours last observed 2026-06-09".
confidencenumberrequired0–1 confidence in the availability inference.

Extended fields (get_details only)

ParameterTypeDescription
areastringrequiredNeighborhood. E.g. "Kreuzberg".
locationobjectoptional{ lat: number, lng: number } — present when coordinates are known.
attrs.opening_hoursstringoptionalUniversal. Raw OSM opening_hours string. E.g. "Mo-Sa 12:00-22:00".
attrs.phonestringoptionalUniversal. Phone number.
attrs.websitestringoptionalUniversal. Website URL.
sourcestringrequiredData provenance description.
result_tokenstringrequiredOpaque token for report(). Valid 30 minutes.

restaurant — vertical schema

Present on all results where vertical === "restaurants". Agents should check vertical before reading this sub-object. Salons (vertical === "salons") follow the same pattern with their own named sub-object; further verticals (fitness, hotels…) will too.

ParameterTypeDescription
cuisinestring[]optionalCuisine types. E.g. ["pizza", "italian"].
price_bandobjectoptional{ value: "€"|"€€"|"€€€", inferred: boolean }. Inferred when not directly observed.
veganbooleanoptionalTrue if venue serves vegan options.
vegetarianbooleanoptionalTrue if venue serves vegetarian options.
outdoor_seatingbooleanoptionalTrue if outdoor seating is available.

Confidence & freshness

Loop is honest about what it doesn’t know. Every field that is inferred says so. No silent guesses.

confidence

A 0–1 score derived from: number of independent sources that confirm the record, recency of the last observation, and the volume and direction of report() outcomes. Higher is more trustworthy.

As a rule of thumb: ≥ 0.9 = cross-confirmed, recently observed. 0.7–0.9 = reasonable, verify before acting. < 0.7 = stale or single-sourced — call verify().

availability.inferred

Always true until a merchant connects directly to Loop. Availability is inferred from the last observed opening hours, not confirmed in real time. verify() triggers a live OSM re-check if the record is stale and may return a more current availability status.

observed_at

ISO 8601 timestamp of the last time this record or field was observed from a source. Not the time of your request. Use this to decide whether to call verify(): if observed_at is more than a few hours old and the action matters (booking, routing, telling a user), verify first.

Rate limits

Limits are per-IP per minute. The Retry-After: 60 header is always present on 429 responses.

TierRead calls / minReport calls / minAttribution
Anonymous (no key)3010anonymous agent
Free (with key)6020named agent label
Procustomcustomnamed + dashboard

Agents that consistently call report() with accurate outcomes earn higher limits as a side effect of improving the data quality.

Errors

All errors return structured JSON — never raw 500s for known bad inputs. Every error carries an error code and a suggested_action hint agents can branch on to self-recover without human intervention.

error codeHTTPsuggested_actionMeaning
missing_query400add_q_parameterNo q parameter on a search call.
unknown_result_id404search_againresult_id is not found or is not a valid UUID.
invalid_result_token400call_get_detailsToken is malformed or was not issued by Loop.
expired_result_token400call_get_detailsToken is older than 30 minutes — call get_details again.
invalid_outcome400use_valid_outcomeoutcome is not one of: correct, wrong, booked, closed, other. Response includes allowed[].
invalid_json400send_valid_jsonPOST body is not valid JSON.
rate_limited429retry_afterOver the per-minute limit. Response includes retry_after_seconds. Check Retry-After header.
report_failed500retryInternal error recording the report — safe to retry.
429 rate_limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": "rate_limited",
  "suggested_action": "retry_after",
  "retry_after_seconds": 60,
  "detail": "32 calls this minute (limit 30)"
}
400 expired_result_token
{
  "error": "expired_result_token",
  "suggested_action": "call_get_details"
}
400 invalid_outcome
{
  "error": "invalid_outcome",
  "suggested_action": "use_valid_outcome",
  "allowed": ["correct", "wrong", "booked", "closed", "other"]
}

Coverage

Loop currently covers restaurants and salons in Kreuzberg, Berlin: 582 merchants seeded from OpenStreetMap. The 424 restaurants are cross-confirmed with Foursquare OS Places (312 confirmed); the 158 hair & beauty salons are OpenStreetMap single-source. Coverage expands by vertical as agent demand signals show where to go next.

Out-of-coverage behavior

Queries outside the current coverage area return a structured response, never junk:

out-of-coverage response
{
  "results": [],
  "coverage": "Loop currently covers restaurants and salons in Kreuzberg, Berlin. More cities and verticals are coming — agent demand signals guide where we expand next.",
  "note": "Try a query in Kreuzberg, Berlin."
}

Request expansion

To request a new city or vertical — or to ask about a nonprofit/research access tier — email hello@stayinloop.dev. Include your use case and rough call volume. Expansion is prioritized by agent demand signals already in the system.

Data sources

Merchant records are seeded from OpenStreetMap (ODbL) and cross-confirmed with Foursquare OS Places (Apache 2.0). Live re-verification against OSM happens on demand when verify() is called on a stale record. Agent report() calls immediately update confidence and freshness.

Questions or access requests: hello@stayinloop.dev · Watch your calls land on the live signals board.