Server Client
Server-side SDK with batch operations, streaming, and secret key authentication
The server client is designed for Node.js backends, API routes, and build-time scripts. It uses secret key authentication via the Authorization header and supports batch fetching, streaming, and higher resolution logos.
Installation
npm install @quikturn/logosimport { QuikturnLogos } from "@quikturn/logos/server";Creating a Client
const client = new QuikturnLogos({
secretKey: process.env.QT_SECRET_KEY!, // Required — sk_* key
baseUrl: undefined, // Optional override
maxRetries: 2, // Optional retry count
});Server-side only
The server client requires a secret key (sk_*). Never expose secret keys in client-side code or public repositories.
Key Differences from Browser Client
| Feature | Browser Client | Server Client |
|---|---|---|
| Key type | pk_* (publishable) | sk_* (secret) |
| Auth method | ?token= query param | Authorization: Bearer header |
| Max logo size | 800px | 1200px |
| Output format | Blob URL | Buffer |
| Batch operations | No | Yes |
| Streaming | No | Yes |
Methods
client.get(domain, options?)
Fetches a single logo and returns a Node.js Buffer.
const { buffer, contentType, metadata } = await client.get("stripe.com", {
size: 512,
format: "png",
greyscale: false,
theme: "light",
});
// Write to file
import { writeFile } from "fs/promises";
await writeFile("stripe-logo.png", buffer);Returns:
interface ServerLogoResponse {
buffer: Buffer;
contentType: string; // e.g., "image/png"
metadata: LogoMetadata;
}client.getMany(domains, options?)
Batch-fetches multiple logos with concurrency control. Returns an AsyncGenerator that yields results in order.
const domains = ["stripe.com", "github.com", "vercel.com", "linear.app"];
for await (const result of client.getMany(domains, {
concurrency: 5, // Max parallel requests (default: 5)
continueOnError: true, // Don't stop on individual failures
signal: abortController.signal, // Optional AbortSignal
size: 256,
format: "webp",
})) {
if (result.success) {
console.log(`${result.domain}: ${result.buffer!.length} bytes`);
} else {
console.error(`${result.domain}: ${result.error!.code}`);
}
}Yields:
interface BatchResult {
domain: string;
success: boolean;
buffer?: Buffer;
contentType?: string;
metadata?: LogoMetadata;
error?: LogoError;
}Batch features:
- Preserves input order
- Automatic retry with exponential backoff on rate limits (up to 3 retries per domain)
continueOnError: trueyields failed results instead of throwing- Respects
AbortSignalfor cancellation
client.getStream(domain, options?)
Returns a ReadableStream for zero-copy piping. Ideal for proxying logos to clients or writing large batches to disk.
import { createWriteStream } from "fs";
import { Readable } from "stream";
const stream = await client.getStream("stripe.com", { size: 1200 });
// Pipe to file (Node.js)
const nodeStream = Readable.fromWeb(stream);
nodeStream.pipe(createWriteStream("stripe-logo.png"));client.getUrl(domain, options?)
Returns the URL without the token (since server auth uses the Authorization header, the token is not in the URL).
const url = client.getUrl("stripe.com", { size: 256 });
// → "https://logos.getquikturn.io/stripe.com?size=256"Event Listeners
Same events as the browser client:
client.on("rateLimitWarning", (remaining, limit) => {
logger.warn(`Rate limit: ${remaining}/${limit} remaining`);
});
client.on("quotaWarning", (remaining, limit) => {
logger.warn(`Monthly quota: ${remaining}/${limit} remaining`);
});Error Handling
The server client throws the same typed errors as the browser client:
import { LogoError, RateLimitError, NotFoundError } from "@quikturn/logos";
try {
const { buffer } = await client.get("example.com");
} catch (err) {
if (err instanceof RateLimitError) {
// err.retryAfter — seconds to wait
// err.remaining — requests left
// err.resetAt — Date when window resets
} else if (err instanceof NotFoundError) {
// err.domain — the domain that wasn't found
}
}Use Cases
API Route (Next.js App Router)
Proxy logos through your backend to keep secret keys hidden:
// app/api/logo/[domain]/route.ts
import { QuikturnLogos } from "@quikturn/logos/server";
const client = new QuikturnLogos({
secretKey: process.env.QT_SECRET_KEY!,
});
export async function GET(
_req: Request,
{ params }: { params: { domain: string } }
) {
try {
const { buffer, contentType } = await client.get(params.domain, {
size: 256,
});
return new Response(buffer, {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=86400",
},
});
} catch (err) {
return new Response("Logo not found", { status: 404 });
}
}Batch Download Script
Download logos for an entire portfolio:
import { QuikturnLogos } from "@quikturn/logos/server";
import { writeFile, mkdir } from "fs/promises";
const client = new QuikturnLogos({
secretKey: process.env.QT_SECRET_KEY!,
});
const portfolio = [
"stripe.com", "github.com", "vercel.com", "linear.app",
"figma.com", "notion.so", "slack.com", "shopify.com",
];
await mkdir("logos", { recursive: true });
let success = 0;
let failed = 0;
for await (const result of client.getMany(portfolio, {
concurrency: 3,
continueOnError: true,
size: 512,
format: "png",
})) {
if (result.success) {
await writeFile(`logos/${result.domain}.png`, result.buffer!);
success++;
} else {
console.error(`Failed: ${result.domain} — ${result.error!.code}`);
failed++;
}
}
console.log(`Done: ${success} downloaded, ${failed} failed`);Express Middleware
Serve logos with caching:
import express from "express";
import { QuikturnLogos, LogoError } from "@quikturn/logos/server";
const app = express();
const client = new QuikturnLogos({
secretKey: process.env.QT_SECRET_KEY!,
});
app.get("/logo/:domain", async (req, res) => {
try {
const { buffer, contentType } = await client.get(req.params.domain, {
size: Number(req.query.size) || 128,
});
res.set("Content-Type", contentType);
res.set("Cache-Control", "public, max-age=86400");
res.send(buffer);
} catch (err) {
if (err instanceof LogoError && err.code === "NOT_FOUND_ERROR") {
res.status(404).send("Logo not found");
} else {
res.status(500).send("Internal error");
}
}
});Rate Limit Tiers
Secret keys follow the same tier-based rate limits as publishable keys:
| Tier | Requests/Min | Monthly Quota |
|---|---|---|
| Free | 100 | 500,000 |
| Launch | 500 | 1,000,000 |
| Growth | 5,000 | 5,000,000 |
| Enterprise | 50,000 | 10,000,000 |
The SDK automatically retries on 429 responses with exponential backoff.