Skip to main content

Configure your SDKs

Stainless automatically generates idiomatic yet flexible SDKs for your API. But every API is different. Create the perfect SDK for your API and your users by configuring the Stainless Generator.

When you first generate an SDK with Stainless, we create a reasonable draft configuration for you. You can see and edit it in the SDK Studio.

Resources

[reference]

The most important consideration when designing an SDK is having the correct names for your resources and CRUD operations. The Stainless configuration defines resources as models and methods, grouped by primitive.

Consider the following:

resources:
accounts:
models:
account: '#/components/schemas/Account'
methods:
list: get /accounts
create: post /accounts
retrieve: get /accounts/{account_id}
delete: delete /accounts/{account_id}
update: put /accounts/{account_id}
subresources:
friends:
models:
friends: '#/components/schemas/Friends'
methods:
list: get /accounts/{account_id}/friends
create: post /accounts/{account_id}/friends
delete: delete /accounts/{account_id}/friends/{friend_id}

In this example, we have an accounts resource and an accounts.friends resource nested within. The resulting SDK usage is:

# `foo` is the instantiation of the library.
account = foo.accounts.create(...)
friends = foo.accounts.friends.list(...)

Depending on the constraints and how you want your users to reason about your API, you may want to restructure the resources to look like:

resources:
accounts:
models:
account: '#/components/schemas/Account'
friends: '#/components/schemas/Friends' # Moved here
methods:
list: get /accounts
create: post /accounts
retrieve: get /accounts/{account_id}
delete: delete /accounts/{account_id}
update: put /accounts/{account_id}
# Below items were moved here and renamed
list_friends: get /accounts/{account_id}/friends
add_friend: post /accounts/{account_id}/friends
remove_friend: delete /accounts/{account_id}/friends/{friend_id}

That resulting SDK usage is:

# `foo` is the instantiation of the library.
account = foo.accounts.create(...)
friends = foo.accounts.listFriends(...)

Which approach is better is ultimately up to how you want the user to reason about your API. In general, a good resource is a conceptually distinct, representational object in your API.

Models

[reference]

A model is a schema that Stainless generates as a distinct, named type. Consider the following configuration:

resources:
accounts:
methods:
list: get /accounts
create: post /accounts
retrieve: get /accounts/{account_id}

By default, the Stainless generator creates methods with roughly the following signatures, even if they all reference the same #/components/schemas/Account in their response. (Note the return types.)

class Accounts extends APIResource {
list(...): AccountListResponse[];
create(...): AccountCreateResponse;
retrieve(...): AccountRetrieveResponse;
}

// usage:
const account: AccountRetrieveResponse = client.accounts.retrieve()
info

We do not consider $ref to be part of a public-facing API. OpenAPI specs are difficult to deal with and $ref is often used to circumvent that complexity, which may not perfectly align with the API you want your users to have. For example, changing a $ref for internal organization purposes should never cause a backwards-breaking change to the users of your SDK.

Alternatively, you can configure a model to belong within a resource:

resources:
accounts:
models:
account: '#/components/schemas/Accounts'
methods:
list: get /accounts
create: post /accounts
retrieve: get /accounts/{account_id}

That configuration creates these methods:

class Accounts extends APIResource {
list(...): Account[];
create(...): Account;
retrieve(...): Account;
}

// usage:
const account: Account = client.accounts.retrieve()

In general, a model belongs under the most closely related resource, and each resource should have at least one model. All of our SDKs are designed so that you can put a model where it most logically belongs without worrying about dependency issues.

There are cases where a model doesn't belong under one resource. A common example of that might be something like #/components/schemas/Address or #/components/schemas/Contact. We have a special resource called $shared where these kinds of models should be defined.

resources:
$shared:
models:
address: '#/components/schemas/Address'
contact: '#/components/schemas/Contact'

Models need not only fall under #/components/schemas/*. You may specify any valid JSON Reference from the root.

Finally, the model configuration can also accept an object rather than just a string for even more options, if you need them. Please check out our full reference docs here.

Methods

[reference]

Methods most commonly define how an endpoint maps to a resource, by providing a string value:

resources:
accounts:
methods:
list: get /accounts

You can configure the method in more detail by providing an object:

resources:
accounts:
methods:
list:
type: http
endpoint: get /accounts
deprecated: Use the /user endpoint instead

Authentication

The Stainless Generator defines your SDK's authentication using the #/security and #/components/securitySchemes particulars from your OpenAPI spec. By default, Stainless SDKs are set to authenticate using an environment variable:

client_settings:
opts:
auth_token:
type: string
# Whether this client option is required to instantiate the client:
nullable: false
# Whether this value should be read from an env:
read_env: ORG_AUTH_TOKEN
auth: { security_scheme: BearerAuth }

# optional, overrides the OpenAPI spec's top-level security key, required if it isn't present
security:
- BearerAuth: []

For more complicated authentication schemes, or should the Stainless Generator fail to configure yours correctly, see the examples below.

The Stainless generator uses the top-level security as the security configuration supported by the SDKs, but you may want to specify a different combination of security than the one used in your OpenAPI spec. The security and security_schemes in the Stainless config overrides the values in the spec.

HTTP Bearer Authorization: Bearer <bearer-token>

The HTTP Bearer authentication method is configured like so:

# OpenAPI
components:
security_schemes:
MyBearerAuth:
type: http
scheme: bearer
# optional, documentation purpose only
bearerFormat: JWT

security:
- MyBearerAuth: {}
# Stainless config
client_settings:
opts:
my_bearer_token: # or `token`, `bearer_token`, `api_key`, etc.
type: string
read_env: ORG_BEARER_TOKEN
auth: { security_scheme: MyBearerAuth }
HTTP Basic Authorization: Basic <base64(username:password)>

The HTTP Basic authentication method is configured like so:

# OpenAPI
components:
security_schemes:
MyBasicAuth:
type: http
scheme: basic

security:
- MyBasicAuth: {}
# Stainless config
client_settings:
opts:
my_username:
type: string
read_env: ORG_MY_USERNAME_TOKEN
auth: { security_scheme: MyBasicAuth, role: 'username' }
my_password:
type: string
read_env: ORG_MY_PASSWORD_TOKEN
auth: { security_scheme: MyBasicAuth, role: 'password' }
API Key <Header>: <API Key>

An API key in a header authentication method is configured like so:

# OpenAPI
components:
security_schemes:
MyApiKeyAuth:
type: apiKey
name: My-Api-Key
in: header

security:
- MyApiKeyAuth: {}
# Stainless config
client_settings:
opts:
my_api_key: # or `token`, `auth_token`, etc.
type: string
read_env: ORG_API_KEY_TOKEN
auth: { security_scheme: MyApiKeyAuth }
Optional Auth

To specify that you accept no authentication, declare a security configuration with no properties like so:

security:
- BearerAuth: {}
- {}
OAuth2

Stainless doesn't generate code to handle initial OAuth authorization or token management for you. To handle authorization and managing tokens, you typically want to point your users to one of the existing OAuth SDKs here. We expect to offer out-of-the-box codegen support for OAuth in the future. Please reach out if this is of interest.

Stainless does support endpoints that use OAuth-generated access tokens for authorization. In most cases, you just need to update your Stainless configuration to specify that authentication is happening via Bearer Authentication.

# openapi.yml

securitySchemes:
OAuth2:
type: oauth2
description: OAuth2 security scheme
flows:
authorizationCode:
authorizationUrl: https://my_auth_url.com/
tokenUrl: https://my_token_url.com/
scopes:
read_only: read only
write_only: write only
# Stainless config

client_settings:
opts:
access_token:
type: string
auth:
security_scheme: BearerAuth
read_env: MY_TEAM_ACCESS_TOKEN

security:
- BearerAuth: []

security_schemes:
BearerAuth:
type: http
scheme: bearer

Please reach out if you need help supporting your configuration.

README.md configuration

[reference]

We currently support configuration of the various code snippets in the README.md of a generated SDK.

readme:
example_requests:
default:
type: request
endpoint: post /cards
params:
type: SINGLE_USE
headline:
type: request
endpoint: put /cards
params:
type: SINGLE_USE
account_id: 123
pagination:
type: request
endpoint: list /cards
params:
limit: 30

The headline example is the first usage of the API that your users see in the README.md, so it should be the most 'important' endpoint in your API.

The pagination example is required if you have configured pagination, and should point to a paginated endpoint.

The default example is inherited by all other example requests, though you can manually override them if there is a need to do so. Because of this, we suggest that you choose the most 'standard' endpoint in your API.

Pagination

[reference]

Configuring pagination with Stainless SDKs generates a helper to fetch the next page and an auto-iterator to easily loop through items in your API. The helper makes it easy to manually paginate when needed and the auto-iterator makes consuming a list as natural as a for loop, with the iterator automatically fetching the next page when needed:

const iter: OffsetPage<Account> = await client.accounts.list()

for await (const account in iter) {
if (account.name === 'Michael') {
console.log(account.id)
}
}

Pagination Scheme

To configure pagination, you need to first define a pagination scheme. A pagination scheme is closely related to the pagination class that is generated in the SDKs, and is made up of:

pagination:
- name: <name of page class>
type: <offset, cursor, cursor_id, hypermedia, etc.>
request:
<name of request parameter>: <schema of request parameter>
response:
<name of response field>: <schema of response field>
  1. the type of pagination (offset, cursor, cursor_id, etc...),
  2. the parameters in the request,
  3. and the fields in the response that you expect to see on every paginated method of this type.

The request and response sections are used to generate the pagination classes which depending on the language is represented as a generic class, interface, or object. They should respectively contain keys of the request parameters and response JSON in your API, for example in the header, query, or the body.

You should include request parameters that are necessary for pagination, such as page_number, page_size, after, limit, and properties that should always be present in the response, like data or items (which contains the list of elements you have).

You may also include parameters that aren't strictly necessary for pagination to work. For example, it might be appropriate to add a request parameter like sort_by, which might be an enum of the ways you can sort across your API.

Pagination Type

The first step in configuring pagination is to define the type of pagination your API is using. The types of pagination Stainless supports are:

  • offset: A simple pagination scheme that uses an offset (the number of entries to skip) and limit (number of elements to fetch) to paginate through a list of items.
  • page_number: Similar to offset pagination, but instead of an offset, which indexes on the items, page_number indexes on chunks of elements to skip.
  • cursor: A pagination scheme that uses a cursor (a string or a number to indicate the element to start from) to paginate through a list of items.
  • cursor_id: Similar to cursor pagination, but the cursor comes from an id property of the response items.
note

If you haven't yet built pagination for your API and are unsure which type of pagination to use, we recommend using cursor_id pagination as it is typically more robust and performant than offset or page number pagination.

Cursor ID Pagination
pagination:
- name: my_cursor_id_page
type: cursor_id
request:
starting_after:
type: string
x-stainless-pagination-property:
purpose: next_cursor_id_param
ending_before:
type: string
x-stainless-pagination-property:
purpose: previous_cursor_id_param
limit:
type: integer
response:
my_data:
type: array
items:
type: object
properties:
id:
type: string
x-stainless-pagination-property:
purpose: cursor_item_id
required:
- id
Example OpenAPI Specification
openapi: 3.1.0
paths:
/accounts:
get:
parameters:
- name: starting_after
in: query
schema:
type: string
- name: ending_before
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
responses:
'200':
description: 'success!'
content:
application/json:
schema:
type: object
properties:
my_data:
type: array
items:
type: object
properties:
id:
type: string
Cursor Pagination
pagination:
- name: my_cursor_page
type: cursor
request:
next:
type: string
x-stainless-pagination-property:
purpose: next_cursor_param
limit:
type: integer
response:
my_data:
type: array
items:
type: object
next:
type: string
x-stainless-pagination-property:
purpose: next_cursor_field
Example OpenAPI Specification
openapi: 3.1.0
paths:
/accounts:
get:
parameters:
- name: next
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
responses:
'200':
description: 'success!'
content:
application/json:
schema:
type: object
properties:
my_data:
type: array
items:
type: object
next:
type: string
Offset Pagination
info

Note: While creating APIs with offset pagination is often simpler to implement, they can often end up being a source of problems for both you and your users. Consider:

  • If new resources are simultaneously being created or deleted that fall in the middle of the list based on the sort, you run the risk of missing or double scanning objects as you paginate through results.
  • Many database engines have poor performance for large offset values, since they often have to scan through the entire table to get to the offset.

If you can, we'd recommend cursor-id-based pagination, which is more robust and performant.

pagination:
- name: my_offset_page
type: offset
request:
my_offset:
type: integer
description: The number of elements to skip.
# this tells us to modify this param when getting the next page
x-stainless-pagination-property:
purpose: offset_count_param
my_limit:
type: integer
description: The maximum number of elements to fetch.
response:
my_data:
type: array
items:
type: object
my_total:
type: integer
x-stainless-pagination-property:
# total number of elements in the list
purpose: offset_total_count_field
my_count:
type: integer
x-stainless-pagination-property:
# where to start the next page
purpose: offset_count_start_field
Example OpenAPI Specification
openapi: 3.1.0
paths:
/accounts:
get:
parameters:
- name: my_offset
in: query
schema:
type: integer
- name: my_limit
in: query
schema:
type: integer
responses:
'200':
content:
application/json:
schema:
type: object
properties:
my_data:
type: array
items:
type: object
my_total:
type: integer
my_count:
type: integer
Page Number Pagination
note

Note: While creating APIs with page-number pagination is often simpler to implement, they can often end up being a source of problems for both you and your users. Consider:

  • If new resources are simultaneously being created or deleted that fall in the middle of the list based on the sort, you run the risk of missing or double scanning objects as you paginate through results.
  • Many database engines have poor performance for large offset values, since they often have to scan through the entire table to get to the offset.

If you can, we'd recommend cursor-id-based pagination, which is more robust and performant.

pagination:
- name: my_page_number_page
type: page_number
request:
page:
type: integer
x-stainless-pagination-property:
purpose: page_number_param
page_size:
type: integer
response:
data:
type: array
items:
type: object
page:
type: integer
x-stainless-pagination-property:
purpose: current_page_number_field
last_page:
type: integer
x-stainless-pagination-property:
purpose: total_page_count_field
Example OpenAPI Specification
openapi: 3.1.0
paths:
/accounts:
get:
description: Example case for page_number pagination
parameters:
- name: page
in: query
schema:
type: integer
- name: page_size
in: query
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/MyModel'
last_page:
type: integer
description: The last page number
page:
type: integer
description: The page number

Multiple pagination schemes

The pagination section accepts multiple pagination schemes. We match each endpoint against the defined pagination schemes by making sure the relevant request parameters/response fields exist for that endpoint and have the correct type.

In cases where it's ambiguous or you want to explicitly assert that a method matches my_offset_page, you can provide paginated: my_offset_page and the generator reports an error if it doesn't match that specific page.

See the config reference for pagination for various examples and edge cases.

Other Pagination Properties

Retrieving pagination properties from headers

If your API returns pagination values in headers, you can use the from_header property to specify the header to read the value from.

pagination:
- name: page_cursor_from_headers
type: cursor
request: ...
response:
my_cursor:
type: string
nullable: true
x-stainless-pagination-property:
purpose: next_cursor_field
from_header: 'X-My-Cursor'

Retrieving pagination properties from nested items

If your API returns pagination values nested within the response, you can configure Stainless to read them from the nested items.

Example response:

{
"data": {
"items": [{}, {}]
},
"pagination_object": {
"next_page": "next_page_cursor",
"previous_page": "previous_page_cursor"
}
}
pagination:
- name: page_cursor_nested_items
type: cursor_id
request: ...
response:
data:
type: object
properties:
items:
x-stainless-pagination-property:
purpose: items
type: array
items: {}
pagination_object:
type: object
properties:
next_page:
type: string
x-stainless-pagination-property:
purpose: next_cursor_id_param
previous_page:
type: string
x-stainless-pagination-property:
purpose: previous_cursor_id_param

Top level arrays in response

You can use the is_top_level_array property to indicate that the response is a top-level array.

Example response:

{
{
"id": 1,
"name": "Alice",
},
{
"id": 2,
"name": "Bob",
},
}
pagination:
- name: top_level_array
type: offset
request: ...
response:
items:
type: array
items: {}
x-stainless-pagination-property:
is_top_level_array: true

Miscellaneous

Extra client arguments

You can define extra client arguments, which generally appears as an extra argument on the client constructor of each SDK (in Go, it appears as an extra RequestOption). These are generally used for supplying values for authentication methods, but can be also used for extra headers and more.

client_settings:
opts:
pet_store_version:
type: string # can be a string, boolean, or a number
nullable: true # makes this an optional argument
default: v3 # the default pet store version to use

read_env: PETSTORE_VERSION
send_in_header: 'X-Petstore-Version'
const client = new Petstore({
apiKey: '...',
petStoreVersion: 'v2', // sends 'X-Petstore-Version: v2'
});

Default headers

Default headers are headers we add to every request made by the SDK. We send platform headers so that you can collect metrics on the languages and platforms your users use. For all SDKs, we send the following

HeadersDescription
X-Stainless-LangThe language, such as go, node, java, kotlin, python.
X-Stainless-Package-VersionThe package version such as v2.3.1.
X-Stainless-OSThe OS, such as Android, MacOS, Windows, FreeBSD, OpenBSD, Linux, Other:xxx.
X-Stainless-ArchThe architecture, such as x32, x64, arm, aarch64, other:xxx.

We also send some extra headers for each language:

Node HeadersDescription
X-Stainless-RuntimeWhich JS runtime was used.
X-Stainless-Runtime-VersionThe version that was used to make the request.
Python HeadersDescription
X-Stainless-AsyncWhether or not the `AsyncClient` was used by the user.
X-Stainless-RuntimeWhich python runtime was used.
X-Stainless-Runtime-VersionThe python version that was used to make the request.
Java/Kotlin HeadersDescription
X-Stainless-Runtime-VersionThe Java version that was used to make the request.
Go HeadersDescription
X-Stainless-RuntimeWhich Go runtime was used.
X-Stainless-Runtime-VersionThe version of Go that was used to make the request.

Retries

Our clients retry connection errors (for example, a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors.

By default, our clients retry 2 times (so a total of 3 requests) using the exponential backoff strategy with an initial delay of 0.5s and a max delay of 8s. We also add a jitter of 25% to spread out requests.

This can be configured by you for your users:

client_settings:
default_retries:
# 5 retries are made, with the interval [1, 2, 4, 8, 10 (capped by the max)]
# not accounting for jitter
max_retries: 5
initial_delay_seconds: 1
max_delay_seconds: 10

Or it can be changed by the user in each language's SDK:

client = new Petstore({
maxRetry: 0, // Doesn't retry at all
});

Our SDKs also respect the Retry-After header sent by the API, which defines in integers how many seconds we should wait before making another request. We also support the Retry-After-Ms header which is less standard but gives more fine-grained control over timings in milliseconds.

Timeouts

In addition to retries, our clients also have a default timeout of 60 seconds, which can be configured.

client_settings:
default_timeout: PT60S # ISO8601 or number of milliseconds. This is 60 seconds

Idempotency key

Idempotency Keys can prevent errors where multiple retried requests are interpreted as separate requests. Our clients retry connection errors and certain status codes by default to create robust integrations, so we recommend that your API supports idempotency keys, especially for critical endpoints.

You can configure idempotency keys by specifying client_settings.idempotency like so:

client_settings:
idempotency:
header: 'Idempotency-Key' # or a header you prefer, like 'X-Request-Id'

We send the configured header with a value in the format stainless-retry-{random_uuid} on all non-GET requests. This header is also possible to override in every SDK that we generate.

Enterprise

OpenAPI transformations

If your OpenAPI is not perfect and/or hard to modify because it's generated, we can work with you to identify problems in your OpenAPI spec and fix issues on our end with OpenAPI transforms. Contact our sales.

SSE streaming

Configuring SSE streaming is supported by Stainless for enterprise customers. When configured, we will generate ergonomics bindings for your users to consume streams ergonomically and conveniently, for example in for await loops and more. Contact our sales.

We can also help write and design helpers to make your streamed APIs more powerful for your users.