Embeddings
POST /v1/embeddings turns text into vectors: lists of 768 numbers in which texts
that mean similar things sit close together. Use them to:
- search by meaning: find the listings that match what someone typed, even when they share no words with it;
- match: rank freelancers for a job, or jobs for a freelancer;
- find duplicates: catch the same listing posted twice in different words.
The model is a multilingual embedding model at 768 dimensions, the same model and size
the platform’s own knowledge index uses; the model field names it, and stays the same
for as long as vectors from it stay comparable. It is multilingual, so Lao and English texts
can go in the same request. The call is synchronous: JSON in, vectors out.
Authorization
Call it with an API key that carries the embeddings scope, or from a dashboard
session. Add the scope to a key on the API keys page; you can change an existing key’s
scopes without issuing a new one. A key without it gets 403 SCOPE_FORBIDDEN.
Create embeddings
/v1/embeddings API key JSON body
texts * string[] | 1 to 100 texts, each 1 to 8,000 characters and not blank. |
taskType * string | RETRIEVAL_DOCUMENT, RETRIEVAL_QUERY or SEMANTIC_SIMILARITY. Pick it by what the text is; see below. |
dimensions integer | Optional, and 768 is the only value accepted. Send it so that a change would be refused loudly instead of reaching your vector column. |
curl https://ai-api.vanguardinitiative.com/v1/embeddings \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"texts": [
"Professional logo design for cafes and small shops, two revisions included.",
"Lao to English translation of contracts and business documents."
],
"taskType": "RETRIEVAL_DOCUMENT",
"dimensions": 768
}'type TaskType = 'RETRIEVAL_DOCUMENT' | 'RETRIEVAL_QUERY' | 'SEMANTIC_SIMILARITY';
async function embed(texts: string[], taskType: TaskType): Promise<number[][]> {
const res = await fetch('https://ai-api.vanguardinitiative.com/v1/embeddings', {
method: 'POST',
headers: { Authorization: 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ texts, taskType, dimensions: 768 }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.code + ': ' + body.message);
return body.embeddings; // embeddings[i] belongs to texts[i]
}
function cosine(a: number[], b: number[]): number {
let dot = 0;
let na = 0;
let nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
// When a listing is created or edited: embed it as a document and store the vector.
const gigs = [
'Professional logo design for cafes and small shops, two revisions included.',
'Lao to English translation of contracts and business documents.',
];
const gigVectors = await embed(gigs, 'RETRIEVAL_DOCUMENT');
// When someone searches: embed what they typed as a query, then rank.
const [query] = await embed(['logo for my coffee shop'], 'RETRIEVAL_QUERY');
const ranked = gigs
.map((title, i) => ({ title, score: cosine(query, gigVectors[i]) }))
.sort((a, b) => b.score - a.score);{
"status": "success",
"model": "<embedding model id>",
"dimensions": 768,
"embeddings": [
[0.0123, -0.0456, 0.0089, ...],
[-0.0211, 0.0337, 0.0154, ...]
],
"credits": 1,
"latencyMs": 412
}embeddings[i] always belongs to texts[i]. credits is what the request cost.
Choosing a task type
Pick taskType by what the text is, not by what you will do with it.
taskType | For | Example |
|---|---|---|
RETRIEVAL_DOCUMENT | What you store and search over | Gig and job listings, profiles, help articles |
RETRIEVAL_QUERY | What someone types into a search box | “logo for my coffee shop” |
SEMANTIC_SIMILARITY | Comparing two things of the same kind | Is this new listing a copy of an existing one? |
Index your catalogue with RETRIEVAL_DOCUMENT and embed searches with RETRIEVAL_QUERY. Mixing them up still returns vectors; they just retrieve worse.
Using the vectors
- Compare with cosine similarity. The vectors come back as the model produces them,
not scaled to length 1, so a raw dot product is skewed by their length. In Postgres
with pgvector, store them in a
vector(768)column and order by the<=>operator, or normalize them yourself before using an index that ranks by inner product. - Chunk long text yourself. The model reads at most 2,048 tokens of each text, roughly 8,000 characters of English and well under that of Lao, and ignores the rest. The platform’s own index uses chunks of about 1,500 characters.
- Batch when you index. Up to 100 texts fit in one request, and the price is set by the total characters, so 100 short listings in one request cost 1 credit where 100 separate requests cost 100.
- Debounce a search box. Every request costs at least 1 credit, so embed when the person stops typing, not on every keystroke.
- Re-embed when the text changes, and store the vector next to the id of what it came from.
Pricing
1 credit per started 50,000 characters, counted across every text in the request, with a minimum of 1 credit per request.
| Request | Characters | Credits |
|---|---|---|
| One search query | 40 | 1 |
| 100 listings of 400 characters each | 40,000 | 1 |
| 100 texts of 8,000 characters (the largest request) | 800,000 | 16 |
The full price is reserved before the call and refunded in full if it fails.
Limits
| Limit | Value |
|---|---|
| Texts per request | 1 to 100 |
| Characters per text | 1 to 8,000, not blank |
dimensions | 768 only |
| Tokens the model reads per text | 2,048 |
| Time per request | 25 seconds, then 504 TIMEOUT and a refund |
Your plan’s per-minute rate limit applies as it does everywhere else.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_REQUEST | The body is not JSON or breaks the shape above. The message names the field, for example texts.3: must not be blank |
| 402 | QUOTA_EXCEEDED | Not enough credits |
| 403 | SCOPE_FORBIDDEN | The key does not carry embeddings |
| 403 | SERVICE_DISABLED | Embeddings are switched off for now |
| 403 | VERIFICATION_REQUIRED | Your organization has to complete identity verification first |
| 429 | RATE_LIMITED | Too many requests this minute |
| 429 | USAGE_LIMIT_REACHED | Your daily or monthly credit cap is reached |
| 502 | GENERATION_FAILED | The model failed. Refunded |
| 503 | EMBEDDINGS_UNAVAILABLE | Embeddings are not configured on this deployment |
| 504 | TIMEOUT | No answer within 25 seconds. Refunded |
See Errors for the shared envelope.