SDK and Codegen Starter
Build a small server-side Lottery Feed API client from the OpenAPI contract without exposing bearer tokens.
Section
05 / 27
Live API examples
Jump from the docs to real public pages that use the same read models.
Endpoints
/openapi.jsonPublic OpenAPI 3.1 contract for code generation.
/postman-collection.jsonPostman collection generated from the same public contract.
/sdk-examples.jsonCacheable starter clients and sandbox smoke examples for SDK onboarding.
/v1/results/latestLatest compact result rows used by SDK smoke tests.
/v1/results/checkChecker response used by SDK POST smoke tests.
/v1/results/historyDate-filtered history rows used by customer backfills.
Start from the public contract
Use /openapi.json as the source of truth for generated clients, integration tests, and typed DTOs. The contract describes the customer-facing /v1 routes and their authentication, validation, response, and error semantics.
curl -sS "https://lotteryfeedapi.com/openapi.json" -o lottery-feed-openapi.json
Machine-readable starter examples
Use /sdk-examples.json for copy-ready server-side starter clients, deterministic sandbox smoke tests, public contract links, and the version policy in one cacheable JSON file. It is noindex, generated from the public API contract, and intentionally excludes live bearer keys and private implementation details.
curl -sS "https://lotteryfeedapi.com/sdk-examples.json" -o lottery-feed-sdk-examples.json
Minimal TypeScript client
Keep this wrapper on the server side. It uses explicit timeouts, sends bearer tokens only through headers, returns the compact JSON model that public endpoints document, and turns 401/403/429 bodies into a typed LotteryFeedApiError for customer logs and backoff decisions.
type LotteryFeedClientOptions = {
apiKey: string;
baseUrl?: string;
timeoutMs?: number;
};
type LatestResultRow = {
game_code: string;
draw_date: string;
numbers_formatted?: string;
numbers_string?: string;
};
type LatestResultsResponse = {
data?: LatestResultRow[];
};
type CheckResultResponse = {
matched: boolean;
match_class: string;
submitted_numbers_formatted: string;
winning_numbers_formatted: string;
};
type LotteryFeedRateLimit = {
scope: 'api_key' | 'workspace' | 'sandbox_client';
window: 'utc_day' | 'utc_month' | 'sliding_window';
limit: number;
used?: number;
remaining: number;
reset_at?: string;
retry_after_seconds: number;
hint: string;
};
type LotteryFeedErrorPayload = {
error?: {
code?: string;
message?: string;
rate_limit?: LotteryFeedRateLimit;
};
};
export class LotteryFeedApiError extends Error {
readonly status: number;
readonly code: string;
readonly endpoint: string;
readonly rateLimit?: LotteryFeedRateLimit;
readonly retryAfterSeconds?: number;
constructor(input: {
status: number;
code: string;
message: string;
endpoint: string;
rateLimit?: LotteryFeedRateLimit;
retryAfterSeconds?: number;
}) {
super('Lottery Feed API ' + input.status + ' ' + input.code + ': ' + input.message);
this.name = 'LotteryFeedApiError';
this.status = input.status;
this.code = input.code;
this.endpoint = input.endpoint;
this.rateLimit = input.rateLimit;
this.retryAfterSeconds = input.retryAfterSeconds;
}
}
export function createLotteryFeedClient({
apiKey,
baseUrl = 'https://api.lotteryfeedapi.com/v1',
timeoutMs = 10000
}: LotteryFeedClientOptions) {
async function request<T>(
path: string,
options: {
method?: 'GET' | 'POST';
params?: Record<string, string | number | undefined>;
body?: unknown;
authenticated?: boolean;
} = {}
) {
const method = options.method || 'GET';
const url = new URL(path.replace(/^\/+/, ''), baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
for (const [key, value] of Object.entries(options.params || {})) {
if (value !== undefined && value !== '') url.searchParams.set(key, String(value));
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers: {
accept: 'application/json',
...(options.authenticated === false ? {} : { authorization: `Bearer ${apiKey}` }),
...(options.body === undefined ? {} : { 'content-type': 'application/json' })
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal
});
if (!response.ok) {
const payload = (await response.json().catch(() => null)) as LotteryFeedErrorPayload | null;
const retryAfterSeconds = parseRetryAfter(response.headers.get('retry-after'));
throw new LotteryFeedApiError({
status: response.status,
code: payload?.error?.code || 'HTTP_' + response.status,
message: payload?.error?.message || response.statusText || 'Request failed.',
endpoint: url.pathname,
rateLimit: payload?.error?.rate_limit,
retryAfterSeconds: retryAfterSeconds ?? payload?.error?.rate_limit?.retry_after_seconds
});
}
return (await response.json()) as T;
} finally {
clearTimeout(timeout);
}
}
return {
latest: (params?: { state?: string; ball_count?: number; game_code?: string; limit?: number }) =>
request<LatestResultsResponse>('/results/latest', { params }),
history: (params: { date: string; state?: string; game_code?: string; ball_count?: number; limit?: number }) =>
request('/results/history', { params }),
check: (body: { game_code: string; draw_date: string; numbers: string[] }) =>
request<CheckResultResponse>('/results/check', { method: 'POST', body }),
lotteries: (params?: { state?: string; ball_count?: number; limit?: number }) => request('/lotteries', { params }),
status: () => request('/status', { authenticated: false })
};
}
function parseRetryAfter(value: string | null) {
if (!value) return undefined;
const seconds = Number(value);
return Number.isFinite(seconds) && seconds > 0 ? seconds : undefined;
}
const client = createLotteryFeedClient({
apiKey: process.env.LOTTERY_FEED_API_KEY || 'lf_test_demo_sandbox'
});
try {
const latest = await client.latest({ state: 'MD', ball_count: 3, limit: 1 });
console.log(latest.data?.[0]?.numbers_formatted);
const check = await client.check({ game_code: 'md-pick3-midday', draw_date: '2026-06-21', numbers: ['2', '3', '6'] });
console.log(check.match_class, check.winning_numbers_formatted);
} catch (error) {
if (error instanceof LotteryFeedApiError && error.code === 'RATE_LIMIT_EXCEEDED') {
console.warn('Pause this key for', error.retryAfterSeconds, 'seconds', error.rateLimit);
}
throw error;
}Rate-limit aware errors
Generated clients should preserve error.code and error.message on every failure, plus error.rate_limit when the response provides it. Quota 429 responses and rate-limit enforcement 503 responses include rate-limit evidence; FEED_SNAPSHOT_UNAVAILABLE instead returns its stable code, message, request_id, and Retry-After. Map 401 to key rotation or credential setup, 403 to scope or plan correction, RATE_LIMIT_EXCEEDED to pause the affected key until Retry-After or error.rate_limit.reset_at, RATE_LIMIT_UNAVAILABLE to temporary enforcement unavailability, and FEED_SNAPSHOT_UNAVAILABLE to a bounded retry after the result-data snapshot recovers. The compact error object is safe to log; Authorization headers and raw lf_live keys are not.
try {
const latest = await client.latest({ state: 'MD', ball_count: 3, limit: 1 });
console.log(latest.data?.[0]?.numbers_formatted);
} catch (error) {
if (error instanceof LotteryFeedApiError && error.code === 'RATE_LIMIT_EXCEEDED') {
const delaySeconds = error.retryAfterSeconds ?? error.rateLimit?.retry_after_seconds ?? 60;
console.warn('Pause this key before retrying', {
delaySeconds,
endpoint: error.endpoint,
rate_limit: error.rateLimit
});
}
throw error;
}Generated clients
Generate clients from the public OpenAPI document in your own CI and keep bearer-token clients behind your server layer. Generated clients should preserve the shared 400, 401, 403, 429, 405, and 503 error envelope and should run deterministic sandbox smoke tests before release.
# Example CI shape: curl -sS "https://lotteryfeedapi.com/openapi.json" -o lottery-feed-openapi.json openapi-generator-cli generate -i lottery-feed-openapi.json -g typescript-fetch -o ./generated/lottery-feed-typescript openapi-generator-cli generate -i lottery-feed-openapi.json -g python -o ./generated/lottery-feed-python openapi-generator-cli generate -i lottery-feed-openapi.json -g php -o ./generated/lottery-feed-php # run deterministic sandbox contract tests before publishing your client
Sandbox contract test
Use sandbox keys to verify client wiring without waiting for live draws. Sandbox requests are deterministic, do not read the live feed, and do not count as production usage.
const client = createLotteryFeedClient({
apiKey: 'lf_test_demo_sandbox'
});
const latest = await client.latest({ state: 'MD', ball_count: 3 });
if (latest.data?.[0]?.numbers_formatted !== '2-3-6') {
throw new Error('Sandbox contract changed or client mapping failed.');
}
const check = await client.check({
game_code: 'md-pick3-midday',
draw_date: '2026-06-21',
numbers: ['2', '3', '6']
});
if (check.match_class !== 'exact_order' || check.winning_numbers_formatted !== '2-3-6') {
throw new Error('Sandbox checker contract changed or POST mapping failed.');
}Operational guardrails
Use idempotent retries only for GET requests, respect HTTP 429 Retry-After and RateLimit headers, store draw_date and numbers_formatted in your own system, and log response status plus endpoint path without logging bearer tokens. For high-traffic polling, prefer feed links or cached latest-result snapshots where the freshness tier allows it.
SDK integration FAQ
Short answers for teams generating clients and testing server-side Lottery Feed API integrations.
Can I ship the generated SDK in a browser app?
Only if it never contains a bearer API key. Browser embeds should use widgets or revocable feed links. Server-side applications should keep lf_live and lf_test bearer tokens in server-side secrets.
How often should generated clients be refreshed?
Refresh clients after OpenAPI or changelog updates, then run sandbox contract tests before publishing the generated package to your application.
Next: Embeddable widgets