n4nAI

curl examples for embeddings and vector search

Hands-on curl embeddings api examples for generating OpenAI-compatible embeddings and querying a vector store, with verifiable runnable steps.

n4n Team3 min read755 words

Audio narration

Coming soon — every post will get a voice note here.

Most embedding pipelines start with a copied snippet that breaks on the first production edge case. These curl embeddings api examples show how to generate vectors from text and search them with a minimal stack, using only standard HTTP tools and a vector database you can run locally.

Step 1: Set up credentials and base URL

Export your API key and the base URL. If you use OpenAI directly, the base is https://api.openai.com/v1. If you front your calls with an OpenRouter-class gateway such as n4n.ai, the same curl shape works against its single OpenAI-compatible endpoint, and you get automatic fallback when a provider is degraded.

export EMBED_API_KEY="sk-..."
export EMBED_BASE="https://api.openai.com/v1"

Keep the key out of shell history in shared environments. Use a secrets file or env injection from your CI. The base URL is the only variable you change when switching providers or routing through a gateway that honors client routing directives.

Step 2: Generate a single embedding

Post to /embeddings with a model id and a single string. The input field accepts a string or an array. Stick to text-embedding-3-small unless you need the larger dimension of text-embedding-3-large.

curl -s "$EMBED_BASE/embeddings" \
  -H "Authorization: Bearer $EMBED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-small",
    "input": "LLM inference gateways simplify multi-provider routing."
  }' | jq '.data[0].embedding | length'

The response follows the OpenAI schema:

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [0.0123, -0.0456, ...]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": { "prompt_tokens": 12, "total_tokens": 12 }
}

Verify success: the jq filter prints the vector dimension. For text-embedding-3-small that is 1536. If you see a number, the call worked and you have a valid float array. The usage block is your per-token metering signal—log it if you need to attribute cost downstream.

Step 3: Batch embed multiple documents

Sending an array cuts latency and avoids redundant TLS handshakes. The batch approach in these curl embeddings api examples reduces round trips and keeps ordering deterministic via the index field.

curl -s "$EMBED_BASE/embeddings" \
  -H "Authorization: Bearer $EMBED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-small",
    "input": [
      "Postgres handles relational data well.",
      "Vector search needs approximate nearest neighbor indexes.",
      "curl is enough for quick API probes."
    ]
  }' > batch.json

Parse with jq to confirm three vectors:

jq '.data | length' batch.json

Batch size limits depend on the provider; stay under token caps per request (typically 300k tokens for the small model). If you need thousands of docs, chunk into groups of ~100 and retry on 429. Preserve order by writing each chunk’s response to a numbered file.

Step 4: Stand up a local vector store

Qdrant ships a plain HTTP API with no client lock-in. Run it:

docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

Create a collection sized to your model dimension. Cosine is the right distance for OpenAI embeddings unless you normalize manually.

curl -s -X PUT "http://localhost:6333/collections/docs" \
  -H "Content-Type: application/json" \
  -d '{
    "vectors": { "size": 1536, "distance": "Cosine" }
  }' | jq '.status'

Expect "ok". If you see "error", check that the size matches the model output exactly—a 3072-dim model will be rejected by a 1536-dim collection.

Step 5: Upsert embeddings with curl

Extract vectors from batch.json and map them to point ids. A short Python script avoids hand-writing JSON and lets you attach payloads for filtering later.

import json, requests

with open("batch.json") as f:
    resp = json.load(f)

texts = [
    "Postgres handles relational data well.",
    "Vector search needs approximate nearest neighbor indexes.",
    "curl is enough for quick API probes."
]

points = [
    {"id": i, "vector": item["embedding"], "payload": {"text": txt}}
    for i, (item, txt) in enumerate(zip(resp["data"], texts))
]

r = requests.put("http://localhost:6333/collections/docs/points",
    json={"points": points})
print(r.json())

Verify: query point count.

curl -s "http://localhost:6333/collections/docs" | jq '.result.points_count'

Returns 3. Upserts are idempotent by id—re-running the script overwrites the same points. For large datasets, batch upserts of 100–500 points per request and watch for 422 payload-too-large errors.

Step 6: Run a vector search with curl

Embed a query with the same model, then post it to the search endpoint. Never mix models between embed and search time.

QUERY_VEC=$(curl -s "$EMBED_BASE/embeddings" \
  -H "Authorization: Bearer $EMBED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "text-embedding-3-small", "input": "How do I search vectors?" }' \
  | jq -c '.data[0].embedding')

curl -s -X POST "http://localhost:6333/collections/docs/points/search" \
  -H "Content-Type: application/json" \
  -d "{\"vector\": $QUERY_VEC, \"top\": 2, \"with_payload\": true}" \
  | jq '.result'

The response lists scored hits:

[
  {
    "id": 1,
    "score": 0.82,
    "payload": { "text": "Vector search needs approximate nearest neighbor indexes." }
  },
  {
    "id": 2,
    "score": 0.71,
    "payload": { "text": "curl is enough for quick API probes." }
  }
]

Verify success: the top score is highest for the semantically closest doc. Cosine scores range from -1 to 1; above 0.7 is typically relevant for this model family. Add "score_threshold": 0.75 to the search body to drop weak matches. You can also add a filter on payload fields to scope the search spatially.

Step 7: Handle rate limits and partial failures

Providers return 429 with a Retry-After header. Wrap curl in a retry loop with exponential backoff:

embed() {
  local data="$1"
  for i in 1 2 3 4 5; do
    resp=$(curl -s -w "\n%{http_code}" "$EMBED_BASE/embeddings" \
      -H "Authorization: Bearer $EMBED_API_KEY" \
      -H "Content-Type: application/json" \
      -d "$data")
    code=$(echo "$resp" | tail -1)
    if [ "$code" = "200" ]; then echo "$resp" | head -1; return 0; fi
    sleep $((2**i))
  done
  echo "FAILED" >&2; return 1
}

A 401 means the key is wrong or expired. A 400 usually means malformed JSON or a model name the provider doesn’t host. If you use a gateway that forwards provider cache-control hints, repeated identical inputs may return cached vectors—useful for static corpora.

Step 8: Script the full pipeline

A single flow cements the pattern. Save embeddings to disk, upsert, search, and print the best match’s text.

# generate and store
curl -s "$EMBED_BASE/embeddings" \
  -H "Authorization: Bearer $EMBED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"text-embedding-3-small","input":["doc one","doc two"]}' > batch.json
python upsert.py batch.json

# search
QUERY_VEC=$(curl -s "$EMBED_BASE/embeddings" \
  -H "Authorization: Bearer $EMBED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"text-embedding-3-small","input":"query text"}' \
  | jq -c '.data[0].embedding')

curl -s -X POST "http://localhost:6333/collections/docs/points/search" \
  -H "Content-Type: application/json" \
  -d "{\"vector\": $QUERY_VEC, \"top\": 1, \"with_payload\": true}" \
  | jq -r '.result[0].payload.text'

For production, move the embedding call behind a queue and batch at ~100 items per request. Monitor usage.total_tokens for cost tracking and alert on sudden spikes.

Caveats engineers miss

Dimensions must match the collection schema exactly. Mixing text-embedding-3-small (1536) with text-embedding-ada-002 (1536) happens to align, but text-embedding-3-large is 3072—upsert will reject it with a distance/size error.

Cosine distance in Qdrant expects normalized vectors; OpenAI embeddings are not strictly normalized by default. The DB normalizes internally for cosine, but if you compute similarity in client code, normalize first:

import numpy as np
v = np.array(embedding)
v = v / np.linalg.norm(v)

Cache embeddings for static documents. Re-embedding the same text wastes tokens and introduces drift if the model version changes. Key your cache on model + text hash.

These curl embeddings api examples are deliberately provider-agnostic. Swap the base URL for any OpenAI-compatible service and the commands hold, whether you run a single provider or a routing gateway.

Tagscurlembeddingsvector-searchcookbook

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All curl llm api cookbook posts →