Server proxy
The browser talks to your backend and your backend talks to Cord: no key ever reaches the page.
Cord Elements works in two ways: with a publishable key (pk_) straight to Cord, or with proxyUrl, where your own backend serves the requests and calls Cord with a server key. Use the proxy when you want the Builder to read your client directory, email the quote or set negotiated prices: things a pk_ key can never do.
The proxy lives in @flouviahq/node, the server SDK. That package replaces the old @flouviahq/elements/server, which still works but no longer receives changes.
Mount it
npm install @flouviahq/node
// app/api/cord/[...path]/route.ts (Next.js App Router)
import { createElementsProxy } from '@flouviahq/node';
const proxy = createElementsProxy({
secretKey: process.env.CORD_SECRET_KEY!,
authorizeClients: async (req) => Boolean(await getSession(req)),
});
export const GET = proxy;
export const POST = proxy;
<CordProvider proxyUrl="/api/cord">
<CordBuilder />
</CordProvider>
getSession is your own session function. createElementsProxy returns a (Request) => Promise<Response> handler, so it works the same in Next.js, Astro, Remix, Hono, Bun or Cloudflare Workers. cord init creates it for you in a Next.js project.
What it serves
| Browser request | Calls Cord | Condition |
|---|---|---|
GET {proxyUrl}/elements/config |
GET /api/v1/elements/config |
Always. |
GET {proxyUrl}/productos |
GET /api/v1/productos |
Always. |
POST {proxyUrl}/cotizaciones |
POST /api/v1/cotizaciones |
Always, with the body sanitized. |
POST {proxyUrl}/elements/ai-draft |
POST /api/v1/elements/ai-draft |
Always. The AI stream is forwarded as is; files up to 4 MB. |
GET {proxyUrl}/clientes |
GET /api/v1/clientes?limit=200 |
Only if authorizeClients returns true. Otherwise 403 with code: "clients_require_proxy". |
Any other route or method returns 404. The proxy doesn’t retry: the browser already retries on its own.
Options
| Option | Detail |
|---|---|
secretKey |
Required. An sk_ or rk_ key. |
allowedOrigins |
Origins allowed to use the proxy. By default, only the request’s own origin: a proxy open to any site is the same as publishing your key. |
authorizeClients(request) |
Authorizes reading the client directory. Without it, it’s never read. |
authorizeSellerFields(request) |
Authorizes seller fields on creation: cliente_id, send (email it), fiscal_currency, and on each line precio_negociado and costo_unitario. Without it, they’re dropped and the quote is created as a draft, just like with a pk_ key. |
baseUrl |
Cord’s origin. Default https://cordhq.app. |
fetch |
For tests. |
const proxy = createElementsProxy({
secretKey: process.env.CORD_PROXY_KEY!,
allowedOrigins: ['https://portal.yourcompany.com'],
authorizeClients: async (req) => (await getSession(req))?.role === 'seller',
authorizeSellerFields: async (req) => (await getSession(req))?.role === 'seller',
});
The proxy forwards the browser’s Idempotency-Key (the Builder sends one per submission attempt), and returns Cord’s errors with their code, request_id and doc_url.
Give the proxy a restricted key
The proxy doesn’t need a Secret Key with every permission. Create a restricted key (rk_) in Developers › API keys with only this:
| Resource | Level | What for |
|---|---|---|
elements |
write | Configuration and AI line filling |
productos |
read | The Builder’s catalog |
cotizaciones |
write | Creating the quote |
clientes |
read | Only if you use authorizeClients |
If your server is compromised, that key can’t read invoices, collections or webhooks. You can also pin your server’s IPs and set an expiration. See Authentication.
Debugging
Every call from the proxy to Cord shows up in the Workbench’s Developers › Logs with its request_id, method, route, status, duration and IP. The same request_id reaches the browser in the Cord-Request-Id header and in an error’s body.