YMEEYMEE API · DocsAdmin →

Connect a client site to the YMEE API

Every AI feature on a client's site goes through the YMEE gateway instead of calling Google, OpenAI or Anthropic directly. YMEE holds the provider keys, picks the model, meters every call and bills the client. You keep the provider's official SDK and change three things: the base URL, the API key and the model name.

1Setup

Start every integration on staging with a test key. Switch the base URL and key for production only once everything below works.

StagingProduction
Gateway base URLhttps://api.ymeedev.ushttps://api.ymee.us
API keyymee_test_…ymee_live_…
Suggested env varsYMEE_API_URL and YMEE_API_KEYsame names, production values
Keep the key server-side. Only call the gateway from server code (route handlers, server actions, background jobs). Never ship the key to the browser or put it in a NEXT_PUBLIC_ variable.

2Sign in & API keys

You create keys yourself in the YMEE API admin. Everyone signs in with their @ymee.us Google account: there are no passwords.

StagingProduction
YMEE OS (your apps)https://os.ymeedev.ushttps://os.ymee.us
YMEE API adminhttps://api.ymeedev.us/adminhttps://api.ymee.us/admin
Open the admin and click Continue with Google. Staging and production are separate sign-ins, so sign in once on each. As a Member you can see usage at provider cost, create and revoke API keys, pause features and set limits, and change model routing. Billing, prices and client settings are for admins.
Every client gets its own keys. A key belongs to one client, and every call made with it is metered, limited and billed to that client. Never reuse one client's key on another client's site, and never share a key between clients or personal projects.

Create the keys for a client

In each admin, open Clients → the client → API keys. A key is shown only once, so copy it straight into that client's env.

AdminNameEnvironmentBillable
api.ymeedev.us/admin<Client> – devtestoff
api.ymee.us/admin<Client> – productionliveon

Leave Product on “All products”: the model name decides which feature a call is billed to. If a key ever leaks, revoke it there and create a new one.

3Clients & features

What each client can call today. Send the model name instead of a provider model id: YMEE picks the real model and enforces its settings, so models can change without a release on your side. Routing is managed in the admin (Clients → the client → Model routing).

SEASE/docs/sease

FeatureModel name to sendPathRoutes to todayCost
ASK
shown to the client as “Ask Sease”
status key ask
ymee-ask/v1/geminigemini-3.8-flash
Google Gemini
Billed to the client
Translation
status key translation
ymee-translation
set automatically by /v1/translate
/v1/translategpt-5.4-mini
OpenAI
Included (YMEE pays)
Try-On
status key try-on
ymee-try-on/v1/geminigemini-3.1-flash-image
Google Gemini
Billed to the client
One feature, many brand names. Each feature is a single YMEE product, whatever the client calls it. ASK, the shopping concierge, runs as Ask Sease for SEASE (ASK <Brand> for each client), and the client's name for it also appears in its portal and on its invoices. In code, always use the YMEE product: status key and cart attribute ask, model name ymee-ask. Never use the brand name in code.

The response header x-ymee-model shows which model actually answered. A new client or feature appears here as soon as it's set up in the admin.

4Calling a feature

Use the provider's official SDK on the path from the table above. Tools, system instructions, contents and streaming work exactly as with the provider. The examples use the default model names; use the ones in the client's table.

Google Gemini (@google/genai)

ts
import { GoogleGenAI } from "@google/genai";

const ai = new GoogleGenAI({
  apiKey: process.env.YMEE_API_KEY,
  httpOptions: {
    baseUrl: `${process.env.YMEE_API_URL}/v1/gemini`,
    headers: {
      "x-ymee-session": cart.id,            // see Session headers
      ...(customerId ? { "x-ymee-user": customerId } : {}),
    },
  },
});

// A chat feature: ASK (streaming: generateContentStream)
await ai.models.generateContent({
  model: "ymee-ask",
  contents,
  config: { tools, systemInstruction },
});

// An image feature, e.g. Try-On
await ai.models.generateContent({
  model: "ymee-try-on",
  contents: [faceRefs, garment, prompt],
  config: { responseModalities: ["IMAGE"] },
});

Anthropic and OpenAI

ts
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({ apiKey: process.env.YMEE_API_KEY, baseURL: `${process.env.YMEE_API_URL}/v1/anthropic` });

import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.YMEE_API_KEY, baseURL: `${process.env.YMEE_API_URL}/v1/openai/v1` });

A model name belongs to one provider: call it on that provider's path (calling a Gemini model name on /v1/anthropic is refused with a clear error).

ProviderForwarded endpoints (anything else returns 404)
Google GeminigenerateContent, streamGenerateContent, countTokens, embedContent, GET /v1beta/models
AnthropicPOST /v1/messages (stream or not), POST /v1/messages/count_tokens, GET /v1/models
OpenAIPOST /v1/chat/completions, POST /v1/responses, POST /v1/embeddings, GET /v1/models

Raw REST works too, e.g. POST {YMEE_API_URL}/v1/gemini/v1beta/models/ymee-try-on:generateContent with the header x-goog-api-key: <YMEE key>.

5Hide paused featuresrequired

The client and YMEE can set monthly spend limits or pause a feature from the client's portal. When a feature is paused the gateway refuses its calls, so the site must hide that feature rather than show an error.

Check status server-side, cached for about a minute

ts
const res = await fetch(`${process.env.YMEE_API_URL}/v1/status`, {
  headers: { authorization: `Bearer ${process.env.YMEE_API_KEY}` },
  next: { revalidate: 60 },
});
const { products } = await res.json();

// One entry per feature, keyed by its status key (see Clients & features)
const show = (key: string) => products[key]?.enabled !== false;
show("ask"); // ASK ("Ask Sease" for SEASE)

Each entry looks like { "enabled": true, "reason": null, "spent_usd": 12.4, "limit_usd": 200, "remaining_usd": 187.6 }. The status call is free and isn't metered. If it fails (network error), keep the features visible: the gateway still enforces pauses on the actual call.

Handle a pause mid-session

A feature can pause between two status checks. Its calls then return HTTP 403 (or 402 if YMEE's own cap is reached) with:

json
{ "error": { "type": "product_paused", "reason": "product_limit_reached",
             "product": "try-on", "source": "ymee-gateway" } }

When error.type === "product_paused", treat the feature as unavailable: hide it, show a friendly message, and don't retry.

6Translationrequired where enabled

Catalogue translation has its own endpoint, so you don't need an OpenAI SDK or key: it uses the client's YMEE key and the client's translation model name is set for you.

http
POST {YMEE_API_URL}/v1/translate
Authorization: Bearer <YMEE key>
Content-Type: application/json

{ "texts": ["Giacca 3L Shell", "Pantaloni"], "source": "it", "target": "en",
  "format": "text", "context": "Luxury ski apparel product copy",
  "glossary": { "SEASE": "SEASE", "sci alpinismo": "ski touring" } }

→ { "items": [{ "id": "…", "key": null, "text": "3L Shell Jacket", "status": "pending", "version": 1, "published": false }, …],
    "translations": ["3L Shell Jacket", "Trousers"], "published": [false, false], "model": "…", "usage": { … } }

format: "html" keeps tags intact (use it for product descriptions). source is optional (detected when omitted). Translations come back in the same order as texts.

Review, keys and sync

Every translation is kept for the client's team to review in their YMEE portal (Translations), where they edit and publish it. Send your own keys so results can be matched to your rows, then sync what the team publishes. Send the same target code every time (always it, not sometimes it-IT): each code is its own list.

http
POST {YMEE_API_URL}/v1/translate
{ "target": "it", "source": "en",
  "texts": [{ "key": "pdp.addAll", "text": "Add all to bag", "source_hash": "<your sha256>" },
            { "key": "pdp.count", "text": "{count, plural, one {# piece} other {# pieces}}" }] }

→ { "items": [
      { "id": "6f1c…", "key": "pdp.addAll", "text": "Aggiungi tutto alla borsa", "status": "pending", "version": 1, "published": false },
      { "id": "a03e…", "key": "pdp.count", "text": "{count, plural, one {# capo} other {# capi}}", "status": "published", "version": 2, "published": true } ],
    "translations": [ … ], "published": [false, true], "model": "gpt-5.4-mini-…", "usage": { … } }
  • One version per source text. An item whose source text already has a version is answered from it without the model: the published text, else the pending machine text. When nothing needed the model, model is "stored" and usage is zero.
  • Changed source = new version. Older versions are marked stale; a published one stays published until the new version is published.
  • force: true (on the request or one item) translates again into a new pending version; the published one stays served meanwhile.
  • Style guide. The client's team can set house rules and a glossary per language in the portal; every new machine translation follows them (your request's glossary wins on conflicts). Existing versions don't change.
  • Statuses: pending, published, superseded (replaced by a newer version), archived (key removed). Publishing is blocked when placeholders ({name}, plural/select, #) or tags (<b>) don't match the original.
http
GET {YMEE_API_URL}/v1/translations?target=it&updated_since=2026-10-01T00:00:00Z&limit=500
→ { "items": [{ "id": "6f1c…", "key": "pdp.addAll", "target": "it", "format": "text",
                "source_text": "Add all to bag", "source_hash": "<your sha256>", "text": "Aggiungi tutto al carrello",
                "status": "published", "stale": false, "version": 1,
                "updated_at": "2026-10-05T10:12:03.418211+00:00", "published_at": "2026-10-05T10:12:03.418211+00:00" }],
    "next_cursor": "eyJ0Ijoi…" }      // null on the last page; keep the same filters while paging

POST {YMEE_API_URL}/v1/translations/import          // at most 500; no model; idempotent on key + target + source text
{ "items": [{ "key": "pdp.addAll", "target": "it", "source_text": "Add all to bag", "text": "Aggiungi tutto",
              "status": "published", "source_hash": "<your sha256>" }] }
→ { "created": 1, "unchanged": 0, "failed": 0, "items": [{ "index": 0, "result": "created", "id": "…", "version": 1 }] }

POST {YMEE_API_URL}/v1/translations/archive         // at most 500 keys; no target = every language
{ "keys": ["pdp.oldBanner"], "target": "it" }
→ { "archived": 1 }

The sync returns every version whose status or text changed since updated_since, oldest first, including ones sent back to review. Store the last updated_at you processed and pass it next time (a minute of overlap is safe: items are idempotent by id). These three endpoints use the same key, aren't metered and ignore spend limits; /v1/translate still follows pause and limits.

For a catalogue job

  • Translate once, store the result. Run it when a product is created or its copy changes (a Shopify product webhook or a back-office action) and save the output to Shopify Translations or the CMS. Never translate on page load.
  • Batch. Up to 100 strings and 60,000 characters per request. Group short fields (titles, options, tags); send long HTML descriptions in smaller batches.
  • One target language per request. Loop over the languages you need.
  • Use context and glossary for consistent terms: the brand, product line names and technical terms that must not change.
  • Allow time. A large batch can take tens of seconds. Run it from a background job, not inside a shopper's request, and retry 429/5xx with backoff.

7Session headersrequired

Send the shopper's session on every AI call (not needed for translation jobs). It lets YMEE count conversations and link AI use to orders. Nothing extra runs in the shopper's browser: no cookies, no pixels.

HeaderValue
x-ymee-sessionOn Shopify storefronts, the cart ID, e.g. gid://shopify/Cart/…. Elsewhere, the site's own session or conversation ID.
x-ymee-userOptional. The platform's customer ID when logged in. Never an email or name.
x-ymee-tagsOptional. A JSON object stored with the call, e.g. {"product":"shell-jacket"}.

With an SDK, pass them in the client's default headers (httpOptions.headers for Gemini). With fetch, send them as normal headers.

8Cart attributerequired on Shopify

On Shopify storefronts, when a shopper uses an AI feature, mark the cart once per feature, server-side, with the Storefront API. The attribute travels with the cart to the order, so YMEE can report which orders involved which feature.

graphql
mutation {
  cartAttributesUpdate(
    cartId: $cartId,
    attributes: [{ key: "ymee_ai", value: "ask,try-on" }]
  ) { cart { id } }
}

The value lists the features used, by status key. Keep any existing cart attributes when you set it (merge, don't replace), and list each feature once. Nothing else is needed for orders: YMEE reads them through its own Shopify app.

9Errors & retries

Errors from the gateway itself carry "source": "ymee-gateway". Errors from the provider are passed through unchanged.

StatusMeaningWhat to do
400Model name called on the wrong path, or a malformed requestFix the request
401Missing or wrong YMEE keyCheck YMEE_API_KEY for this environment
403Feature paused, client suspended or feature not enabled for this clientHide the feature (Hide paused features)
402YMEE spend cap reachedHide the feature
404Unknown model name, or an endpoint that isn't forwardedCheck the model name and the endpoint list
429 / 5xxProvider busy or failingRetry with exponential backoff

When Google or Anthropic report overload (503 / 529), the gateway already retries once on a fallback model before answering, so an overload error that reaches you means both were busy.

10Go-live checklist

For each client:

11Not in scope

  • Billing, invoices and payment: YMEE handles these with each client directly.
  • Client portals (e.g. sease.ymee.us): run by YMEE.
  • Revenue reporting from orders: YMEE builds it from the session headers and cart attribute.

Questions or a stuck call: send James the time of the call and the x-ymee-model header (or the error body). Every call is logged on YMEE's side, so it can be traced.