Kifly REST API
The HTTP surface behind every Kifly integration. The MCP server is a client of these same endpoints — anything an MCP tool does, you can do here.
You do not need an API key to start. Discovery, carts and the payment-link handoff are open to any agent that identifies itself (below).
Base URL
https://kifly.ai
Identify yourself — the UCP-Agent header
An agent with no Kifly credential is admitted by declaring the URL of the UCP
profile your agent publishes about itself. This is the UCP spec's own
mechanism (ucp.dev 2026-08-25, where it is meta.ucp-agent.profile and
maps to this header), not a Kifly invention.
Two wire forms are accepted. The RFC 8941 structured-field dictionary is the spec's form and what we recommend:
UCP-Agent: profile="https://your-agent.example/.well-known/ucp"
A bare absolute URL also works, and will keep working:
UCP-Agent: https://your-agent.example/.well-known/ucp
The URL must be absolute and https: — http: is rejected rather than
upgraded.
Copy-paste a first call:
curl -sS 'https://kifly.ai/api/agent/search?q=hat&limit=3' \
-H 'UCP-Agent: profile="https://your-agent.example/.well-known/ucp"'
- Required. It is attribution, not authentication or authorization.
- Rate limit: open callers are limited per profile. An API key raises it.
- Use a domain you control.
your-agent.exampleabove is a placeholder; reserved names and bare IP addresses are served with an advisory and will later be refused./.well-known/ucppublishes the date.
Optional: say which of your users a call is for
A platform serving many people (one profile, many users) can tell Kifly which user and conversation a call belongs to. It never changes what you are allowed to do.
Kifly-Buyer: ref="u_5d9c1f0a7b3e", session="s_2026_10_01_0915"
On MCP the same fields go in meta["ai.kifly.buyer"] = { "ref": "…", "session": "…" }
beside ucp-agent.
- Send an opaque id, never a person. Use
HMAC-SHA256(your secret, your user id), the same advice OpenAI gives forsafety_identifierand Anthropic formetadata.user_id. 8 to 64 characters ofA-Z a-z 0-9 . _ : -. - Anything that looks like an email or phone number is dropped, the call is served, and an advisory tells you why.
sessionis your conversation or task id.
Optional: sign your requests to be recognised as yourself
Sign each request with a key your profile publishes and Kifly recognises you as yourself. Signing is optional: a signature that does not verify is served like an unsigned call, with an advisory saying why.
- Standard: RFC 9421 HTTP Message Signatures, the UCP signature profile.
Publish your public key(s) in your profile's
keys[](a JWK Set). ES256 (P-256), ES384 and Ed25519 are accepted. - Cover:
@method,@authority,@path;@querywhen there is a query;content-digestandcontent-typewhen there is a body; anducp-agent,idempotency-key,signature-agentwhen you send them. Includecreatedandexpires(at most 24 h apart) and anonce. - Web Bot Auth works too. A
tag="web-bot-auth"signature whoseSignature-Agentis on the same host as your profile is verified against that directory;keyidmust be the key's RFC 7638 thumbprint. - What it earns: recognition as yourself, a higher rate allowance, and
buyer refs marked as attested when the signature covers
kifly-buyer. - Feedback: responses carry
Kifly-Agent-Trustand IETFRateLimit-Policy/RateLimitheaders.
Advisories — what to fix, without a refusal
When something about your request should change but does not stop it, Kifly answers normally and tells you, the same way on every surface. An advisory never changes the status code.
- REST: a
Kifly-Advisoryresponse header (reserved_profile_host;refused_from="2026-11-01") and akifly:advisoriesarray in a JSON body. - MCP:
result._meta["ai.kifly/advisories"]on the tool result.
Each entry is { code, message, documentation, refused_from }. Read
message, fix it, and the advisory stops.
Optional: link a buyer's Kifly account (UCP identity linking)
A platform can link one of its users to their Kifly account (OAuth 2.0 with
PKCE; discovery at /.well-known/oauth-authorization-server, iss on every
authorization response, the UCP scope dev.ucp.shopping.order:read accepted).
Then call with that buyer's token and your UCP-Agent header: the call is
the verified buyer's, and it is credited to your platform. Nothing requires
linking; it upgrades what a call can do, it never gates the open surface.
Or authenticate with a key
Authorization: Bearer kfa_live_xxxxxxxxxxxx
Send either Authorization or UCP-Agent, never both. If an
Authorization header is present we honour it and never fall back to the open
path — so a revoked or mistyped key stays a clean 401 instead of silently
downgrading you to open access with less data and no warning.
See MCP Integration → Authentication for which credential fits what you are building.
Which capabilities actually need one. Buyer identity
(/api/agent/buyer, /api/agent/buyer/otp, /api/agent/buyer/verify,
/api/agent/buyer/me, /api/agent/buyer/orders), order history
(/api/agent/order-status) and settlement. Everything else on this page is
open.
How to get a token — read this before you retry. Run the OAuth 2.1 flow
advertised at /.well-known/oauth-authorization-server. It supports Dynamic
Client Registration, so your agent registers itself at /oauth/register and
the only human step is one consent screen; buyer:write is in
scopes_supported, so register_buyer and verify_buyer work once you hold
the grant. A person can alternatively mint a long-lived key from the Kifly
dashboard.
There is no credential obtainable with no human consent at any point, and
the buyer-identity endpoints cannot bootstrap one. Get the grant instead. If
that blocks a job you need to do, say so through
POST /api/agent/feedback — it needs no credential.
Open endpoints — no credential required
| Method | Path | What it does |
|---|---|---|
GET | /api/agent/search | Search or browse the network catalog. q, limit, cursor, category, seller_handle, sort_by, price and stock filters. Returns JSON-LD. |
GET | /api/agent/sellers | List sellers on the network. |
GET | /api/agent/seller | One seller's public profile, delivery coverage and catalog size. Name it by handle, domain (any URL on the store's site) or name. Call it first when the user names a store; a miss returns SELLER_NOT_FOUND with details.did_you_mean. |
GET | /api/agent/policies | A seller's published store policies. |
POST | /api/agent/cart | Create a cart. |
GET DELETE | /api/agent/cart/{id} | Read or cancel a cart. |
POST | /api/agent/cart/{id}/items | Add a line item. |
PATCH DELETE | /api/agent/cart/{id}/items/{itemId} | Change quantity, or remove. |
POST | /api/agent/cart/{id}/shipping-address | Set the shipping address. |
POST | /api/agent/quick-buy | Create a cart, add one item and set the address in a single call. |
POST | /api/agent/payment-link | A Stripe payment link a human confirms. |
POST | /api/agent/feedback | Tell us what broke. No credential needed. |
GET | /api/agent/platform/limits | Live cart limits. |
GET | /api/agent/docs/{slug} | These pages as JSON. overview, rest, mcp, errors, limits. A bad slug returns 404 listing every valid one. |
Everything else — buyer identity, order history, autonomous settlement —
requires an issued key and returns 401/403 naming what it needed.
POST /api/agent/cart/{id}/checkout is not on that list. It settles, so it
needs checkout:write, which is deliberately outside what an open caller can
hold — no tool call and no tokenless request completes a purchase. The open path
to a sale is /api/agent/payment-link: a Stripe link a human confirms. See
Payment Rails.
GET /api/agent/products is permanently retired and returns 410. Use
/api/agent/search.
Mutating requests need an Idempotency-Key
Every POST, PATCH and DELETE on the agent surface requires one:
Idempotency-Key: 16-to-64-characters-unique-per-request
A retry with the same key and the same body returns the original response
rather than acting twice. A missing key is 400 IDEMPOTENCY_KEY_REQUIRED.
Keys are namespaced per caller, so your keys can never collide with another
agent's.
Errors
Every failure is JSON with a stable machine-readable code:
{ "error": "human readable prose", "code": "STABLE_SCREAMING_SNAKE" }
Branch on code, never on error — the prose gets reworded, the code does
not. Upstream faults return 503 with retryable: true and a
Retry-After header; retry those rather than treating them as permanent.
→ Full list on the Error Reference page.
Why there are two surfaces
The MCP connector never completes a purchase on its own. Its checkout
always returns a payment link a human confirms.
Autonomous settlement — x402, where your agent pays USDC on Base and
re-requests — lives on this REST surface only, at
POST /api/agent/cart/{id}/checkout with an X-Payment header. Each
settlement is a fresh per-transaction signature from the buyer's own wallet.
So: MCP if a human is in the loop, REST if your agent settles for itself. They are two trust models, not two ways to do one thing.
Machine-readable contracts
Prefer reading a schema to reading this page. Both are generated from the running server, so neither can drift from it:
| What | Where |
|---|---|
| UCP capability manifest | GET /.well-known/ucp |
| MCP method contract (OpenRPC) | GET /api/mcp/openrpc.json |
| Per-seller manifest | GET /store/{handle}/.well-known/ucp |
Something here confused you?
Then we have a bug, and we would rather hear it than not:
curl -sS -X POST 'https://kifly.ai/api/agent/feedback' \
-H 'UCP-Agent: profile="https://your-agent.example/.well-known/ucp"' \
-H 'Idempotency-Key: my-unique-key-0000000000' \
-H 'content-type: application/json' \
-d '{
"category": "tool_description",
"severity": "friction",
"message": "What was unclear, and what you expected instead.",
"context": { "endpoint": "/api/agent/search" }
}'
category is one of discoverability, response_shape,
tool_description, latency, error_handling, missing_capability,
other. severity is blocker, friction or nice_to_have. It
returns 202 immediately — no credential, and a human reads every report.