SDK Requirements
This document defines the requirements for official Market Data SDKs. Use this as acceptance criteria when building or evaluating SDK implementations.
Core Principles
Language-Idiomatic Design
SDKs must feel native to their language. We adapt to the language's conventions—the language does not adapt to us.
This means:
- Use the language's standard patterns for errors, async, and data structures
- Follow the language's naming conventions (snake_case vs camelCase vs PascalCase)
- Return data types that developers in that ecosystem expect
- Don't force patterns from one language onto another
- Follow official style guides and best practices for each language
Easy Default Requests
Customers must be able to make a request and inspect the result in 2–3 lines of code. Every endpoint must have a simple, zero-configuration path (beyond authentication) that returns a useful, printable result.
This means:
- Every endpoint method must work with just required parameters (e.g., a symbol) — no mandatory configuration or boilerplate
- Response objects must support string conversion that produces a readable summary (see §11.6)
- Environment-based token loading (
MARKETDATA_TOKEN) must work automatically so users can skip explicit auth setup - The combination of env-based auth + simple method call + print/log must produce a meaningful result
Examples
Python (3 lines):
from marketdata import MarketDataClient
client = MarketDataClient()
print(client.stocks.quotes("AAPL"))
Go (3 lines, excluding package/import):
client, _ := marketdata.NewClient()
quotes, _ := client.Stocks.Quotes("AAPL")
fmt.Println(quotes)
PHP (3 lines):
$client = new MarketDataClient();
$quote = $client->stocks->quotes("AAPL");
echo $quote;
Required Style Guides
| Language | Style Guide / Best Practices |
|---|---|
| Python | PEP 8, PEP 257 (docstrings) |
| PHP | PSR-12, PSR-4 (autoloading) |
| JavaScript | MDN Guidelines |
| TypeScript | TypeScript Handbook |
| Go | Effective Go, Go Code Review Comments |
| Java | Google Java Style Guide |
| C# | .NET Framework Design Guidelines |
| Rust | Rust API Guidelines |
SDKs must pass the standard linter for their language (e.g., ruff for Python, phpcs for PHP, golint for Go).
| Language | Error Handling | Async Pattern | Default Output | Naming |
|---|---|---|---|---|
| Python | Exceptions* | Sync (async optional) | pandas DataFrame | snake_case |
| JavaScript | Promise rejection | async/await | Plain objects | camelCase |
| PHP | Exceptions | Sync | Objects/arrays | camelCase |
| Go | Return error | Blocking + context | Structs | PascalCase exported |
| Java | Exceptions | CompletableFuture | POJOs | camelCase |
| C# | Exceptions | Task async/await | Objects | PascalCase |
| Rust | Return Result<T, E> | async with runtime | Structs | snake_case |
* Python SDK Technical Debt: The current Python SDK (v1.x) returns
MarketDataClientErrorResultinstead of raising exceptions. This is non-idiomatic and will be fixed in v2.0. New SDKs must use idiomatic patterns from day one.
Quick Reference
| Requirement | Priority |
|---|---|
| Easy default requests (2–3 lines) | Must |
| Bearer token authentication | Must |
| Environment-based configuration | Must |
| Connection pooling | Must |
| Fixed 99-second timeout | Must |
| Typed input validation | Must |
| Typed output models | Must |
| Decimal type for monetary values | Must |
| Structured error handling | Must |
| API credit tracking | Must |
| Retry with exponential backoff | Must |
| API status awareness | Should |
| Multiple output formats | Should |
| Batch/concurrency helpers | Should |
| Ecosystem-native advanced formats | Optional |
1. Client Architecture
1.1 Client Object
The SDK must expose a top-level client object (e.g., MarketDataClient) that:
- Accepts an API token via constructor parameter OR reads from environment variable
MARKETDATA_TOKEN - Allows base URL override (default:
https://api.marketdata.app) - Allows API version override (default:
v1) - Uses a single HTTP client instance with connection pooling
- Provides explicit cleanup/close method for releasing resources
- Exposes current rate limit state
- Sets User-Agent header:
marketdata-sdk-{language}/{version}(e.g.,marketdata-sdk-php/1.0.0) - Fixed HTTP timeout: 99 seconds
1.2 Resource Groupings
The client must expose these resource namespaces:
client.stocks
client.options
client.funds
client.markets
client.utilities
2. Required API Coverage (Canonical Docs Are Source of Truth)
Do not duplicate REST paths, payload schemas, or parameter contracts in this document.
- Canonical REST documentation is authoritative for endpoint paths, request parameters, response schemas, and behavior.
- If this SDK requirements doc conflicts with REST API docs, the REST API docs win.
- SDK acceptance should verify method coverage against canonical docs, not against hardcoded paths here.
2.1 Canonical API Documentation
2.2 Required SDK Method Coverage
SDKs must provide first-class methods for the following capabilities (language-idiomatic naming is expected):
| Resource | Required SDK Methods (capabilities) |
|---|---|
stocks | prices, quotes, candles, earnings, news |
options | chain, expirations, quotes, lookup |
funds | candles |
markets | status |
utilities | status, headers, user |
3. Universal Parameters
All endpoint methods must support these parameters:
| Parameter | Values | Description |
|---|---|---|
dateformat | timestamp, unix, spreadsheet | Controls date output format |
columns | array of strings | Filter response to specific columns |
headers | boolean | Include headers in CSV output |
human | boolean | Use human-readable field names |
4. Configuration Cascade
All SDKs must implement a three-tier configuration system where more specific settings override less specific ones:
Priority Order (Lowest → Highest)
| Priority | Source | Scope | Example |
|---|---|---|---|
| 1 (lowest) | .env file | Project-level | .env file in project root |
| 2 | Environment variables | System/shell | export MARKETDATA_TOKEN=... |
| 3 | Client defaults | Per-client instance | client.default_params.output_format = "json" |
| 4 (highest) | Method parameters | Per-request | client.stocks.prices("AAPL", output_format="csv") |
SDKs should automatically load .env files from the current working directory if present.
How It Works
When making a request, merge configuration in order:
- Load
.envfile values (if present) - Override with system environment variables
- Override with client default parameters
- Override with direct method parameters
Example Flow
.env file: MARKETDATA_OUTPUT_FORMAT=internal
Shell env: MARKETDATA_OUTPUT_FORMAT=json
Client default: output_format=dataframe
Method call: client.stocks.prices("AAPL", output_format="csv")
Result: output_format="csv" (method parameter wins)
Supported Environment Variables
| Variable | Purpose | Default |
|---|---|---|
MARKETDATA_TOKEN | API authentication token | (none) |
MARKETDATA_BASE_URL | API base URL | https://api.marketdata.app |
MARKETDATA_API_VERSION | API version | v1 |
MARKETDATA_LOGGING_LEVEL | SDK logging level | INFO |
MARKETDATA_OUTPUT_FORMAT | Default output format | (language default) |
MARKETDATA_DATE_FORMAT | Default date format | timestamp |
MARKETDATA_COLUMNS | Columns to include | (all) |
MARKETDATA_ADD_HEADERS | Include headers in CSV | true |
MARKETDATA_USE_HUMAN_READABLE | Human-readable field names | false |
MARKETDATA_MODE | Data mode (live/cached/delayed) | live |
5. Authentication
Requirements
- Set
Authorization: Bearer {token}header on all requests - Token resolution follows the configuration cascade:
.envfile (lowest)- System environment variable
MARKETDATA_TOKEN - Client constructor parameter (highest)
- Validate token on startup by default: SDKs should call
/user/during client initialization and fail fast on invalid tokens. - Constructor override required: SDKs must provide a client-constructor option to disable startup validation for short-lived runtimes (e.g., serverless/CLI jobs). When disabled, token/auth errors must surface on the first authenticated request.
- Never log tokens - redact in all log output
Demo Mode
When no token is provided:
- Omit Authorization header
- Log a warning about demo mode limitations
- Skip rate limit initialization
6. Error Handling
6.1 Error Taxonomy
All SDKs must define these error types:
| Error Type | When to Use |
|---|---|
AuthenticationError | 401 responses, invalid/missing token |
BadRequestError | 400 responses, invalid parameters |
PaymentRequiredError | 402 responses, request valid but not included in the plan |
ForbiddenError | 403 responses, token rejected for this request |
NotFoundError | 404 responses |
RateLimitError | 429 responses, rate limit exceeded |
ServerError | 5xx responses |
NetworkError | Connection failures, timeouts |
ParseError | Failed to parse response |
Use the naming convention the language already uses for its other error types. Go returns a sentinel alongside the type, so PaymentRequiredError pairs with ErrPaymentRequired and ForbiddenError with ErrForbidden.
Why 402 is its own type. A 402 says the token is valid and the request is well-formed — only the plan does not cover it. That is a different fact from a 401, where no usable identity was presented, and from a 403, where an identity was presented and refused. A caller that must tell "you may not ask this" apart from "you asked this wrong" cannot do so once all three collapse into one class, and must not have to read .status_code off a generic HTTP error to find out. Capability and entitlement discovery is the concrete case: a consumer probing what its plan allows needs 402 to be a typed, catchable answer. See 402: Payment Required for the canonical semantics.
6.2 Support Context
Every error must include these fields for support staff troubleshooting:
| Field | Source | Example |
|---|---|---|
request_id | cf-ray response header | "8a1b2c3d4e5f6g7h-SJC" |
request_url | Full request URL | "https://api.marketdata.app/v1/stocks/quotes/" |
status_code | HTTP status code | 429 |
timestamp | SDK-generated (US/Eastern) | "2025-02-21 12:00:00" |
message | Error description | "Rate limit exceeded" |
exception_type | Error class name | "RateLimitError" |
In exception-based SDKs (Python, PHP, Java, C#, JavaScript/TypeScript), these support-context fields must be properties on the thrown exception object. In returned-error SDKs (Go, Rust), these fields must be present on the returned error value/type.
6.3 Support Info Helper
SDKs must provide a formatted support_info string for users to send to support:
--- MARKET DATA SUPPORT INFO ---
request_id: 8a1b2c3d4e5f6g7h-SJC
request_url: https://api.marketdata.app/v1/stocks/quotes/
status_code: 429
timestamp: 2025-02-21 12:00:00
message: Rate limit exceeded
exception_type: RateLimitError
--------------------------------
This must be accessible via error.support_info or equivalent.
6.4 Language-Specific Patterns
Follow the idiomatic error handling pattern for each language:
| Language | Pattern | Example |
|---|---|---|
| Python | Raise exceptions | raise RateLimitError(...) |
| JavaScript | Reject promises | throw new RateLimitError(...) |
| PHP | Throw exceptions | throw new RateLimitException(...) |
| Go | Return error | return nil, &RateLimitError{...} |
| Java | Throw exceptions | throw new RateLimitException(...) |
| C# | Throw exceptions | throw new RateLimitException(...) |
| Rust | Return Result | Err(MarketDataError::RateLimit {...}) |
Do not force exception-based error handling onto Go or Rust. Do not force Result types onto Python or PHP.
7. Logging
Log Format
Use a consistent log format across all SDKs:
{timestamp} - {logger_name} - {level} - {message}
Example: 2025-02-21 12:00:00 - marketdata.client - INFO - Making request to /v1/stocks/quotes/
Log Levels
Configurable via MARKETDATA_LOGGING_LEVEL environment variable:
| Level | What to Log |
|---|---|
| DEBUG | Token (redacted), request details, response headers |
| INFO | Client initialization, base URL, API version |
| WARNING | Demo mode, deprecated features |
| ERROR | Request failures, rate limit errors |
Required Log Points
- Client initialization (INFO)
- Token value - redacted (DEBUG): show only last 4 chars
- Request URL (DEBUG)
- Response status and timing (DEBUG/INFO based on status)
- Errors with support context (ERROR)
Token Redaction
Never log full tokens. Redact to show only last 4 characters:
Token: ************************************YKT0
8. Rate Limiting
8.1 Initialization
On client creation (with valid token):
- Call
GET /user/to fetch rate limit info - Store initial credit state:
credit_limit,credits_remaining,reset_time,credits_consumed
8.2 Tracking
After every request:
- Parse rate limit headers from response:
x-api-ratelimit-limitx-api-ratelimit-remainingx-api-ratelimit-resetx-api-ratelimit-consumed
- Attach parsed rate limit metadata to the returned response/result object (request-scoped)
- Update client-level rate limit state as a convenience latest snapshot
8.3 Pre-flight Check
Before each request:
- Check if
credits_remaining <= 0 - If exhausted, throw
RateLimitErrorimmediately (don't make the request)
8.4 Expose State
Provide method to check current rate limits:
client.rate_limits
// Returns: { limit: 50000, remaining: 49500, reset: 1708531200, consumed: 1 }
client.rate_limits is a client-level latest snapshot and is not request-scoped. In concurrent/async execution, this value is non-deterministic (last completed request wins). Per-request logic must use the rate limit metadata attached to each returned response/result.
9. Retries and Backoff
9.1 HTTP Status Code Handling
| Status Code | Action |
|---|---|
| 200 | Success - parse response |
| 203 | Success - non-authoritative (cached data) - parse response |
| 400 | Throw BadRequestError - do not retry |
| 401 | Throw AuthenticationError - fail immediately, do not retry |
| 402 | Throw PaymentRequiredError - do not retry |
| 403 | Throw ForbiddenError - do not retry |
| 404 | Return empty/no-data response (not an error for most endpoints) |
| 429 | Throw RateLimitError - do not retry, expose retry-after |
| 500 | Throw ServerError - do not retry |
| 501-599 | Retry with exponential backoff |
Note on 404: Most endpoints return 404 when no data exists (e.g., no quotes for a delisted symbol). SDKs should return an empty result object with no_data = true rather than throwing an exception.
Note on 402: Unlike 404, a 402 must never be returned as an empty or no-data result. 402: Payment Required states that the request itself is valid — the API declined to answer it, and 402 is that refusal. Rendering it as no-data tells the caller the data does not exist, which is false, and it fails silently: there is no exception anywhere for the caller to catch. An SDK that probes a customer's history depth by walking back until the API stops answering reads a plan denial as the end of the archive, and silently truncates that customer's price history. 402 must throw.
Note on 402 and 403 retries: Both rows forbid the SDK's automatic retry, and they are terminal for different lengths of time. A 402 stands until the plan changes, so an immediate retry only spends a request. A 403 is terminal for that request, but the condition behind it can clear on its own — the common cause is the multi-IP block, which lifts after about 5 minutes. The SDK must not retry either code itself; the caller may try a 403 again once the block clears.
9.2 Retry Conditions
Retry requests only when:
- HTTP status > 500 (server errors)
- Network/connection errors
Never retry: 4xx errors, rate limit errors
9.3 Backoff Strategy
- Use exponential backoff:
initial * 2^retry, whereretryis the 0-indexed retry number (0 = first retry, 1 = second retry, 2 = third retry) - Initial delay: 1 second (fixed)
- Default max retries: 3 (in addition to the initial attempt, for up to 4 total attempts)
- Backoff schedule at the default of 3 retries:
- Before retry 0 (1st retry): 1s (
1 * 2^0) - Before retry 1 (2nd retry): 2s (
1 * 2^1) - Before retry 2 (3rd retry): 4s (
1 * 2^2)
- Before retry 0 (1st retry): 1s (
Max retries is user-configurable via a client constructor parameter (e.g., max_retries). The default applies when not specified. The initial delay and exponential base are fixed and not configurable.
9.4 Retry-After Header
If response includes Retry-After header:
- Respect the server-specified delay
- Override calculated backoff with server value
9.5 API Status Check
Before each retry attempt for retryable server errors (501-599), apply this status-check workflow:
- Use cached
/status/data with two thresholds:- Refresh interval: 270 seconds (4.5 minutes)
- Cache validity: 300 seconds (5 minutes)
- If cache age is < 270s: use cached status, no refresh.
- If cache age is 270s to < 300s: use cached status and trigger non-blocking async refresh.
- If cache is stale or empty (>= 300s): trigger non-blocking async refresh and treat current status as
unknownfor this attempt. - Retry decision:
- If service status is
offline, do not retry - fail immediately. - If service status is
onlineorunknown, proceed with retry/backoff.
- If service status is
Do not perform blocking status refresh inside retry loops.
10. Timeouts
Requirements
- Default request timeout: 99 seconds
- Timeout is fixed and must not be configurable
- If the HTTP client supports a separate connection timeout, set connection timeout to 2 seconds (fixed)
- If separate connection timeout is not supported, use the fixed 99-second request timeout
- Apply timeout to all HTTP requests
11. Output Formats
11.1 Wire Format Decoding (Required)
The API wire format uses compressed, array-keyed JSON. SDKs must decode this into user-usable typed models before returning results.
- Expand compressed response payloads into named fields
- Return idiomatic typed models for the target language
- Do not expose raw compressed arrays as the default SDK output
- Raw compressed payloads may be exposed only as an explicit opt-in debug/raw mode
11.2 Language-Specific Defaults
Each SDK should return data in the format most natural for that language ecosystem:
| Language | Default Output | Notes |
|---|---|---|
| Python | pandas DataFrame | DataFrames are the standard for financial data in Python |
| R | data.frame / tibble | DataFrames are standard in R workflows |
| JavaScript | Plain objects/arrays | Native JS objects, easily JSON-serializable |
| PHP | Objects or associative arrays | Follow PHP conventions |
| Go | Typed structs | Strongly typed, no reflection magic |
| Java | POJOs | Standard Java bean patterns |
| C# | Typed objects | .NET conventions |
| Rust | Typed structs | With serde for serialization |
11.3 Additional Format Support
SDKs may support additional formats only when they are idiomatic for that language:
| Language / Ecosystem | Additional Formats to Support |
|---|---|
| Python | Optional Polars DataFrame output |
| R | Optional tibble/data.table output |
| JavaScript/TypeScript | Plain JSON objects/arrays (no DataFrame requirement) |
| PHP | Objects/arrays and optional CSV export |
| Go/Java/C#/Rust | Typed models; no DataFrame requirement |
Do not add ecosystem-misaligned formats just for parity across SDKs.
11.4 Date Handling
- Parse Unix timestamps to the language's native datetime type
- Normalize to US/Eastern timezone by default
- Respect
dateformatparameter for output
11.5 Monetary Value Precision
Money must not be represented as a binary floating-point number in typed models when the language provides an exact decimal type.
Binary floats cannot represent decimal money exactly (0.1 + 0.2 != 0.3), so any comparison, rounding, or accumulation a customer performs on SDK output drifts. Prices are the primary product of this API; they must round-trip exactly.
Language Applicability
| Language | Exact Decimal Type | Monetary Fields |
|---|---|---|
| Python | decimal.Decimal | Must use decimal |
| Java | java.math.BigDecimal | Must use decimal |
| C# | decimal | Must use decimal |
| Go | None native | Exempt — float64 |
| Rust | None native | Exempt — f64 |
| PHP | None native | Exempt — float |
| JavaScript/TypeScript | None native | Exempt — number |
| R | None native | Exempt — numeric |
Exempt SDKs must not add a third-party decimal library to satisfy this requirement. An ecosystem-foreign money type in public model fields conflicts with the Language-Idiomatic Design principle and forces a dependency on every consumer. Float is the correct choice where the language offers no exact decimal type.
Preserve Precision at the Parse Boundary
Converting an already-parsed float to a decimal does not satisfy this requirement. The exact value is destroyed at the JSON decode step, and a later conversion only locks in the float error. SDKs subject to this requirement must decode the response so the API's decimal string reaches the decimal type directly.
| Language | Decimal-Preserving Decode |
|---|---|
| Python | json.loads(response.text, parse_float=Decimal) |
| Java | Jackson DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS |
| C# | System.Text.Json reader → GetDecimal() on the raw token |
What Counts as Money
The rule: if the value is denominated in currency, it is money. Ratios, statistics, and counts are not.
- Monetary (decimal): bid, ask, mid, last, change, open/high/low/close, strike, intrinsic value, extrinsic value, underlying price, and EPS figures (reported, estimated, surprise)
- Not monetary (float or int): Greeks (delta, gamma, theta, vega), implied volatility, percentages (e.g.
changepct,surpriseEPSpct), and sizes/counts (bidSize,askSize,volume,openInterest,dte)
Canonical REST documentation remains the source of truth for which fields exist on each endpoint; apply the rule above to that schema.
Format Exemptions
This requirement governs typed models only. The following output formats are exempt:
| Output Format | Money Type | Why Exempt |
|---|---|---|
| DataFrame (pandas, polars, R) | float64 | Analytics surface for vectorized math; pandas has no decimal dtype |
| CSV | Raw text | Already byte-faithful exact decimal strings straight from the API |
| JSON | Native decode | "JSON output" means the decoded object; exactness is the typed model's job |
On DataFrames: a DataFrame is an analytics surface — users load data to run vectorized statistics (returns, rolling means, volatility). In pandas, decimals degrade to object dtype, which kills vectorized math on the default output. Where a backend does offer a decimal dtype (polars), still use float64: making only one backend exact would mean identical code returns different dtypes depending on which optional dependency is installed. Consistency across interchangeable backends wins.
Customers who need exactness go through the typed model. SDK documentation must state plainly which formats are exact and which are not.
Arithmetic
SDKs must not perform their own price arithmetic. Computed values (mid, change, and similar) are supplied by the API and are passed through as received. This requirement is about representation, not calculation.
11.6 Response Object Features
All response objects must provide:
Format Detection Methods
| Method | Returns |
|---|---|
isJson() | true if response is JSON format |
isCsv() | true if response is CSV format |
isHtml() | true if response is HTML format |
File Saving
response.saveToFile("/path/to/output.csv")
- Save response data to a file
- Automatically determine format from extension or response type
- Return the file path or a success indicator
No-Data Detection
response.no_data // or response.hasData()
- Indicate when a 404 response returned no data
- Allow callers to distinguish "no data" from errors
String Conversion (Required)
All public response models must support idiomatic string conversion for fast inspection/logging in example code and day-to-day usage.
- Python: implement
__str__(and__repr__where appropriate) - PHP: implement
__toString() - JavaScript/TypeScript: implement
toString() - Go: implement
String()on formatter types where applicable - Java/C#: provide
toString()/ToString() - Rust: implement
Display(and derive/provideDebugas needed)
String conversion should produce concise, developer-friendly summaries (not raw compressed payloads).
12. Concurrency Helpers
Global Concurrency Pool
SDKs must enforce a maximum of 50 concurrent requests at the client level using a sliding window (not batching):
- All endpoints share a single concurrency pool
- Maintain exactly 50 active requests at all times (when demand exists)
- As soon as one request completes, start the next immediately
- Not batching: Don't wait for all 50 to finish before starting more
- Must be thread-safe / goroutine-safe / async-safe depending on language
# Correct (sliding window):
# Request 1 completes → Request 51 starts immediately
# Request 2 completes → Request 52 starts immediately
# Always 50 in-flight until queue is exhausted
# Wrong (batching):
# Wait for all 50 to complete → Start next 50
# This wastes time waiting for slowest request
stocks.candles() - Date Range Splitting
For intraday resolutions with date ranges > 1 year:
- Split into year-sized chunks
- Fetch chunks concurrently (respects global pool limit)
- Merge results into single response
options.quotes() - Batch Processing
For multiple option symbols:
- Fetch quotes concurrently (respects global pool limit)
- Merge results into single response
13. Testing Requirements
Unit Tests (No Real API Calls)
Unit tests must use mocks for all HTTP requests - never hit the real API:
- Use language-appropriate HTTP mocking library
- Mock
/user/and/status/endpoints in test fixtures - Test both success and error scenarios for each endpoint
Must cover:
- URL construction for all endpoints
- Parameter validation and error messages
- Error type mapping (status code → error type)
- Retry behavior and backoff timing
- Rate limit header parsing
- Request-scoped rate limit metadata (
consumedincluded) under concurrent requests - Response parsing and typed model creation
- Date/time normalization
Integration Tests (Real API Calls)
Integration tests make actual REST requests to the live API:
- Gated by environment variable (e.g.,
MARKETDATA_RUN_INTEGRATION_TESTS=true) - Require valid API token
- Test real request/response cycles
- Verify actual API behavior matches expectations
- Run in CI by default for all pull requests and release pipelines
- Not run for every commit/push and not part of standard default/local test runs
Minimum coverage: at least one live test per endpoint
Every endpoint in §2.2 must have at least one integration test that calls it against the live API. A mocked test does not satisfy this: a mock proves the SDK parses the response the author expected, which is precisely the assumption that goes stale when the API changes. Only a live call can tell you the contract still holds.
| Resource | Endpoints requiring live coverage |
|---|---|
stocks | prices, quotes, candles, earnings, news |
options | chain, expirations, quotes, lookup |
funds | candles |
markets | status |
utilities | status, headers, user |
Each test must assert on the shape of the decoded response — that the fields the SDK models actually arrived and carry usable values — not merely that the call returned a 2xx. A test that checks only the status code passes against an endpoint that has silently started returning an empty body.
Where an endpoint has materially different response shapes, cover each one: a no-data answer is a different contract from a populated one, and both are worth a witness.
A missing token must fail the suite, never skip it. An integration suite that skips when unauthenticated reports success while testing nothing, which is worse than having no suite at all — it produces a green check that means nothing. If the token is absent, fail and say so.
Code Coverage
- 100% code coverage required
- Lines that cannot be tested must be explicitly marked with coverage ignore comments
- Python:
# pragma: no cover - PHP:
// @codeCoverageIgnore - Go: Use build tags or explicit skip
- Use language-appropriate ignore syntax
- Python:
- Coverage ignore must include a comment explaining why
CI Requirements
- Run unit tests on every PR and push to main
- Run on multiple runtime versions
- Produce coverage reports (must be 100% or CI fails)
- Integration tests: required on every PR and in release pipelines; excluded from per-commit/push jobs and default test runs
14. Documentation Requirements
README Must Include
- Installation instructions
- Quick start example showing a complete request in 2–3 lines of code
- Environment variable configuration
- Error handling example
- String conversion example(s) showing ergonomic object output
- Link to full documentation
API Documentation
- Document all public methods
- Include parameter types and descriptions
- Show example usage for each endpoint
- Include examples that demonstrate string conversion on response models
- Document error types and when they occur
15. Packaging Requirements
Version & Changelog
- Use Semantic Versioning (MAJOR.MINOR.PATCH)
- Maintain CHANGELOG.md following Keep a Changelog format
- Version must be automatically detected from the package manager metadata (e.g.,
composer.json,setup.py,go.mod) - do not hardcode version strings
Package Registry
| Language | Registry |
|---|---|
| Python | PyPI |
| JavaScript | npm |
| Java | Maven Central |
| Go | Go modules (pkg.go.dev) |
| C# | NuGet |
| Rust | crates.io |
License
- MIT license required
- Include LICENSE file in distribution
16. Security Requirements
- Never log API tokens - redact/obfuscate in all output
- Support environment variable token loading (no hardcoding)
- Error messages must not contain sensitive data
- Validate TLS certificates (no skip-verify options)
17. Repository and Release Configuration
Sections 1–16 govern the SDK's code. This section governs the repository that ships it. These items are invisible in a code review and are exactly the ones that get skipped: a documented label that was never created, a security policy pointing at a disabled button, a release cut by hand because no workflow existed.
Every SDK repository must satisfy this section, regardless of language.
17.1 Required Repository Files
| Path | Purpose |
|---|---|
SECURITY.md | Vulnerability reporting instructions and the security fix policy |
.github/RELEASE_PROCESS.md | How to cut a release for this SDK |
.github/ISSUE_WORKFLOW.md | How a maintainer triages an incoming bug report |
.github/BUG_FINDING.md | How to hunt for bugs proactively, before users hit them |
.github/ISSUE_TEMPLATE/bug.yml | Structured bug report form |
.github/ISSUE_TEMPLATE/config.yml | Contact links; blank_issues_enabled: false |
.github/dependabot.yml | Dependency updates (see §17.4) |
.github/FUNDING.yml | github: MarketDataApp |
CHANGELOG.md | Keep a Changelog format (see §15) |
LICENSE | MIT (see §15) |
These are language-adapted, not copied. A bug template must ask for the runtime version, package manager and platform values that actually exist for that language, and must name real methods and error types from that SDK. A template that asks a Go user for their target framework is worse than no template.
17.2 Repository Settings
- Private Vulnerability Reporting is enabled.
SECURITY.mdtells reporters to use Security → "Report a vulnerability". If the setting is off, that button does not exist and the instruction is a dead end. - Every label referenced by
ISSUE_WORKFLOW.mdexists. At minimumbug,accepted,needs-info,wontfix,dependencies. A documented triage step that applies a non-existent label fails on execution. - Description is set and names the language and what the SDK covers.
- Topics are set — at minimum the language,
sdk, and the asset classes the SDK actually supports. Remove topics for resources the SDK does not cover; they misrepresent the package in search. -
ISSUE_TEMPLATE/config.ymllinks only to destinations that exist. If Discussions are disabled on the repository, the config must not link to them. - Default branch is
main, with adevelopmentbranch when the CI workflows reference one. - Branch protection on the default branch, or a deliberate, recorded decision not
to have it.
RELEASE_PROCESS.mdmust state which, because it determines whether a release merge needs an approval.
17.3 Required Secrets
| Secret | Required | Purpose |
|---|---|---|
MARKETDATA_TOKEN | Yes | Live integration suite (§13). Its absence must fail the job, never skip it |
CODECOV_TOKEN | Optional | Coverage badge; absence must not break CI |
| Registry credentials | Per language | Publishing (see §15) — e.g. Maven Central signing keys, NuGet Trusted Publishing |
A skipped integration suite must never report success. The failure mode this prevents is a green pipeline that ran nothing. If the token is absent, the job fails and says so.
17.4 Dependency Updates
-
dependabot.ymlcovers the CI ecosystem (github-actions) and every package manifest in the repository — including example or sample applications that carry their own manifest. - Updates are grouped. Ungrouped Dependabot opens one pull request per dependency per manifest. Where several sample apps share a dependency set, one upstream release becomes a wall of near-identical pull requests and the updates stop being read.
- Major versions are kept out of the routine group — either in a group of their own or as individual pull requests — so routine bumps merge without waiting on a major, and each major still gets read on its own.
- Dependencies satisfied by a local path override are ignored (for example a Go
replacedirective, or an npmfile:dependency). A pull request bumping one changes nothing and can never merge. - Dependabot pull requests carry the
dependencieslabel.
17.5 Release Automation
Every SDK needs a release workflow. Cutting a release by hand is not acceptable as a steady state: the steps that get skipped under pressure are exactly the ones that matter.
- A
tag-and-releaseworkflow exists, triggered only by manualworkflow_dispatch. No push, merge, or tag event may reach it. - It requires a typed confirmation input (
confirm: RELEASE). - It validates before spending runner time: the version is well-formed SemVer, the
tag does not already exist, and
CHANGELOG.mdcontains the section for the version. - It runs the full test suite, including live integration tests, before the tag is cut, against the exact ref being released. Reuse the existing CI workflow rather than copying it — a second copy of the suite will drift.
- Release notes are extracted from
CHANGELOG.md, never written by hand at release time and never generated back into the changelog. The heading format is matched exactly (## [X.Y.Z]), so the changelog must carry a## [Unreleased]section for maintainers to promote. - The workflow resolves the ref to a concrete commit SHA and tags that SHA, so a branch moving mid-run cannot change what ships.
- It verifies the release after publishing: the artifact resolves from the public registry, and a throwaway project can install it and report the expected version.
A tag or release created by a workflow using the default
GITHUB_TOKENdoes not start another workflow run. GitHub suppresses this to prevent recursive triggering. Any workflow gated onrelease: publishedorpush: tagstherefore stays silent for an automated release. Do not rely on it to test or publish; chain the work explicitly from the release workflow, and say so inRELEASE_PROCESS.md.
Registries and irreversibility
RELEASE_PROCESS.md must state where the point of no return is and what the remedy is,
because it differs sharply by language:
| Language | Publication | Can a bad version be withdrawn? |
|---|---|---|
| Python, JavaScript, Java, C#, PHP | Upload to a registry | No — versions are immutable. Unlist or yank where supported, then ship a patch |
| Go | The git tag itself. proxy.golang.org reads the tag; sum.golang.org logs the hash | No. Deleting the tag does not withdraw it. The only remedy is a later version carrying a retract directive |
Go has no publish step and no staging feed: the moment the tag exists and one person fetches it, the version is public and permanent. Its release workflow therefore ends in verification rather than upload, and all validation must precede the tag.
17.6 Verifying a Repository
These checks are mechanical and should be run against every SDK repository periodically, not only at acceptance.
REPO=MarketDataApp/sdk-<lang>
# Required files
for f in SECURITY.md .github/RELEASE_PROCESS.md .github/ISSUE_WORKFLOW.md \
.github/BUG_FINDING.md .github/dependabot.yml .github/FUNDING.yml \
.github/ISSUE_TEMPLATE/bug.yml .github/ISSUE_TEMPLATE/config.yml; do
gh api "repos/$REPO/contents/$f" --jq '.name' >/dev/null 2>&1 \
&& echo "ok $f" || echo "MISSING $f"
done
# Private Vulnerability Reporting
gh api "repos/$REPO/private-vulnerability-reporting" --jq '.enabled'
# Labels the triage workflow depends on
for l in bug accepted needs-info wontfix dependencies; do
gh api "repos/$REPO/labels/$l" --jq '.name' >/dev/null 2>&1 \
&& echo "ok label:$l" || echo "MISSING label:$l"
done
# Integration token, description, topics
gh api "repos/$REPO/actions/secrets" --jq '[.secrets[].name] | join(", ")'
gh api "repos/$REPO" --jq '{description, topics: (.topics | length), discussions: .has_discussions}'
Acceptance Checklist
Before accepting an SDK, verify:
Core Functionality
- Every endpoint works in 2–3 lines of code (env auth + method call + print)
- All required SDK method capabilities implemented (as defined in this doc and canonical API docs)
- Bearer authentication works
- Token validation defaults to startup (fail fast) and supports constructor override to disable startup validation
- Environment variable configuration works
- Configuration cascade (env → client defaults → method params)
- Rate limits are tracked and exposed
- Errors are typed with support context
- 401 errors fail immediately (no retry)
- 404 returns no-data response (not exception)
- Retries use exponential backoff (>500 only)
- Fixed timeout policy enforced (99s request timeout; 2s connect timeout when supported)
- Concurrency pool enforced (50 max sliding window)
- User-Agent header set correctly
Code Quality
- Unit tests exist and pass (mocked, no real API calls)
- Integration tests exist (gated by env var)
- At least one live integration test per §2.2 endpoint, asserting on the decoded response shape
- A missing API token fails the integration suite rather than skipping it
- 100% code coverage (or explicit ignores with comments)
- CI pipeline configured
- Code follows language style guide (PEP 8, PSR-12, etc.)
- Passes standard linter for language
- No security issues (token leakage, etc.)
Response Objects
- Format detection methods (isJson, isCsv, isHtml)
- File saving capability (saveToFile)
- No-data detection property
- Monetary fields use the language's exact decimal type, or the language is exempt per §11.5
- JSON decoding preserves decimal precision (no float round-trip before the model)
- Non-monetary fields (Greeks, IV, percentages, sizes) remain float/int
- DataFrame output remains float64 on every supported backend
Documentation
- README with quick start
- All methods documented
- Examples provided
Distribution
- Published to appropriate registry
- SemVer versioning
- Version auto-detected from package metadata
- Changelog exists
- MIT license included
Repository & Release Configuration
- All required repository files present (§17.1), language-adapted rather than copied
- Private Vulnerability Reporting enabled
- Every label referenced by
ISSUE_WORKFLOW.mdexists in the repository - Description and topics set, and topics do not claim unsupported resources
-
ISSUE_TEMPLATE/config.ymllinks only to destinations that exist -
MARKETDATA_TOKENsecret present; a missing token fails the integration job rather than skipping it - Dependabot covers every manifest, groups its updates, and separates majors
- Manual-dispatch release workflow with typed confirmation, gated on the full suite
- Release notes extracted from
CHANGELOG.md; an## [Unreleased]section exists -
RELEASE_PROCESS.mdstates where the point of no return is and how to remedy a bad release