Introduction
Welcome to the TONE3000 API. This RESTful API provides programmatic access to TONE3000 accounts, tones and models. To use the API, you or your users will need to authenticate via their TONE3000 account.
Note: This is version 1 of our API. Endpoints and data structures may change as we improve the platform.
Integration Options
All integration options use OAuth 2.0 with PKCE and issue the same access token, giving you full access to tone data, model files, and user libraries. Choose the flow that fits your product:
Select
Your users browse and pick a tone directly within TONE3000's interface. Zero auth UI or tone browser to build. Ideal for plugins, DAW integrations, and native apps.
Load Tone
Your app specifies a tone_id; TONE3000 authenticates the user and verifies access. If the tone is unavailable, the user can browse for a replacement. Ideal for applications where the TONE3000 tone ID is known in advance.
Full API Access
Authenticate via OAuth and use the access token to build any experience: custom tone browsers, library sync, in-app model management.
Explore all integration options in the example app repository.
Rate Limit
100 requests per minute by default. For production applications please email [email protected].
Support & Feedback
Questions, issues, or feedback? Contact [email protected] - we'd love to hear from you.
Authentication
API Keys
Generate your keys in settings. Your account has two types of keys:
Publishable Key
client_idIdentifies your application in OAuth flows. Safe to include in client-side code, mobile apps, and browser environments. Used as the client_id parameter on every authorization and token request.
OAuth Authorization Flow
Use the OAuth flow when your integration is user-facing. Your app redirects the user to TONE3000, they authorize and complete the flow, and TONE3000 redirects back with an authorization code. The prompt parameter controls which flow runs:
- prompt omitted — standard authorization only
- prompt=select_tone — user browses and selects a tone (see Select)
- prompt=load_tone — verifies access to a specific tone_id (see Load Tone)
1. Redirect the user to TONE3000
Generate a PKCE code_verifier and derive the code_challenge (SHA-256, base64url-encoded). Store the code_verifier and state — you'll need them to complete the token exchange.
GET https://www.tone3000.com/api/v1/oauth/authorize
TONE3000 responds with a redirect — there is no response body to read. If the user isn't signed in, they'll be prompted to log in first. Once authenticated, TONE3000 completes the flow and redirects back to your redirect_uri.
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your publishable key |
redirect_uri | Yes | Where to return the user after the flow. If you've registered redirect URIs in settings, only those will be accepted |
response_type | Yes | Must be code |
code_challenge | Yes | Base64url-encoded SHA-256 hash of your code_verifier |
code_challenge_method | Yes | Must be S256 |
state | Yes | A random value you generate; returned in the callback to verify the response is legitimate |
prompt | No | Controls the flow type. See values above. Omit for standard authorization. |
tone_id | Conditional | Required when prompt=load_tone |
gears | No | Restrict catalog by gear type. Applies to select_tone and load_tone flows. Separate multiple values with _ (e.g. amp_full-rig). See Gear |
platform | No | Restrict catalog to a specific model format. Applies to select_tone and load_tone flows. See Platform |
const codeVerifier = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '');const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');const state = crypto.randomUUID();sessionStorage.setItem('t3k_code_verifier', codeVerifier);sessionStorage.setItem('t3k_state', state);const params = new URLSearchParams({client_id: 'YOUR_PUBLISHABLE_KEY',redirect_uri: 'https://your-app.com/callback',response_type: 'code',code_challenge: codeChallenge,code_challenge_method: 'S256',state,// prompt: 'select_tone' | 'load_tone'});window.location.href = `https://www.tone3000.com/api/v1/oauth/authorize?${params}`;
2. Handle the callback
After the user completes the flow, TONE3000 redirects to your redirect_uri. Always verify state before proceeding — this protects against CSRF attacks.
Callback parameters
| Parameter | Description |
|---|---|
code | Short-lived authorization code to exchange for tokens |
state | The value you sent in Step 1 — verify this matches before proceeding |
tone_id | Present on success for prompt=select_tone and prompt=load_tone flows |
error | Present on failure |
const params = new URLSearchParams(window.location.search);const code = params.get('code');const state = params.get('state');const error = params.get('error');if (state !== sessionStorage.getItem('t3k_state')) {throw new Error('State mismatch. Possible CSRF attack.');}if (error) {// Handle error — e.g. error === 'access_denied'return;}
Token Exchange
Exchange the authorization code for an access token. Include the access token as a Bearer token on every subsequent API request.
POST https://www.tone3000.com/api/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
Request body
| Field | Description |
|---|---|
grant_type | Must be authorization_code |
code | The authorization code from the callback |
code_verifier | The PKCE verifier you generated in Step 1 |
redirect_uri | Must match the redirect_uri used in Step 1 |
client_id | Your publishable key |
const response = await fetch('https://www.tone3000.com/api/v1/oauth/token', {method: 'POST',headers: { 'Content-Type': 'application/x-www-form-urlencoded' },body: new URLSearchParams({grant_type: 'authorization_code',code,code_verifier: sessionStorage.getItem('t3k_code_verifier')!,redirect_uri: 'https://your-app.com/callback',client_id: 'YOUR_PUBLISHABLE_KEY',}),});const { access_token, refresh_token, expires_in } = await response.json();
Response
| Field | Type | Description |
|---|---|---|
access_token | string | Bearer token for API requests |
refresh_token | string | Use to get a new access token without re-authorization |
token_type | string | Always bearer |
expires_in | number | Seconds until the access token expires |
scope | string | The scope granted, as passed in the authorization request |
Session Management
1. Store tokens
Store the access token, refresh token, and expiration time securely.
sessionStorage.setItem('t3k_access_token', access_token);sessionStorage.setItem('t3k_refresh_token', refresh_token);sessionStorage.setItem('t3k_expires_at', String(Date.now() + expires_in * 1000));
2. Make authenticated requests
Include the access token as a Bearer token in the Authorization header. Check expiration before each request and refresh proactively.
const expiresAt = parseInt(sessionStorage.getItem('t3k_expires_at') || '0');if (Date.now() > expiresAt) await refreshTokens();const response = await fetch('https://www.tone3000.com/api/v1/user', {headers: { Authorization: `Bearer ${sessionStorage.getItem('t3k_access_token')}` },});
3. Refresh the access token
When the access token expires, POST to the same token endpoint with grant_type=refresh_token.
Request body
| Field | Description |
|---|---|
grant_type | Must be refresh_token |
refresh_token | The refresh token from the previous token response |
client_id | Your publishable key |
const response = await fetch('https://www.tone3000.com/api/v1/oauth/token', {method: 'POST',headers: { 'Content-Type': 'application/x-www-form-urlencoded' },body: new URLSearchParams({grant_type: 'refresh_token',refresh_token: sessionStorage.getItem('t3k_refresh_token')!,client_id: 'YOUR_PUBLISHABLE_KEY',}),});const { access_token, refresh_token, expires_in } = await response.json();sessionStorage.setItem('t3k_access_token', access_token);sessionStorage.setItem('t3k_refresh_token', refresh_token);sessionStorage.setItem('t3k_expires_at', String(Date.now() + expires_in * 1000));
The response shape is the same as the initial token exchange.
4. Handle refresh failure
A 400 response with error: invalid_grant means the refresh token has expired. Clear stored tokens and restart the authorization flow. Refresh tokens are long-lived, but handle this gracefully so users aren't left in a broken state.
sessionStorage.removeItem('t3k_access_token');sessionStorage.removeItem('t3k_refresh_token');sessionStorage.removeItem('t3k_expires_at');startAuthorization(); // restart from Step 1
Select
Select lets your users browse the TONE3000 catalog and pick a tone without you building any auth UI or tone browser. The user is redirected to TONE3000, signs in, browses, and selects a tone — then lands back in your app with an access token and the selected tone_id ready to use.
Step 1: Send the User to TONE3000
Build the authorization URL with prompt=select_tone. You can optionally restrict the catalog by gear type or platform so users only see tones relevant to your product. See Authentication for PKCE generation details.
Native apps: Use a deep link (e.g. yourapp://callback) as your redirect_uri and open the authorization URL in an in-app browser (SFSafariViewController on iOS, Chrome Custom Tabs on Android).
// PKCE generation: see Authentication sectionconst params = new URLSearchParams({client_id: 'YOUR_CLIENT_ID',redirect_uri: 'https://your-app.com/callback',response_type: 'code',code_challenge: codeChallenge,code_challenge_method: 'S256',state,prompt: 'select_tone',// gears: 'full-rig', // optional: amp, full-rig, pedal, outboard, ir// gears: 'amp_full-rig', // optional: multiple values separated by _// platform: 'nam', // optional: nam, aida-x, aa-snapshot, proteus, ir// menubar: 'true', // optional: show navigation bar with back/forward/refresh/close});window.location.href = `https://www.tone3000.com/api/v1/oauth/authorize?${params}`;
| Parameter | Description |
|---|---|
prompt | Must be select_tone (required) |
gears | Restrict the catalog to one or more gear types. Separate multiple values with _ (e.g. amp_full-rig). The gear filter will be locked to your selection (optional, see Gear) |
platform | Restrict the catalog to a specific model format. Only compatible tones will be shown (optional, see Platform) |
menubar | Set to true to show a navigation bar at the top of the TONE3000 experience with back, forward, refresh, and close buttons. Recommended for in-app browsers and popup windows (optional) |
Step 2: The User Browses and Selects
TONE3000 handles sign-in, browsing, and selection. The user sees the full public catalog and their own private tones, filtered by any gear or platform constraints you specified. Once they tap a tone, TONE3000 redirects them back to your app.
Step 3: Handle the Callback
TONE3000 redirects to your redirect_uri with a code, state, and the tone_id the user selected. Verify state before proceeding.
If the menubar is enabled and the user clicks the close button, the redirect will include canceled=true instead of a tone_id. If the user had already signed in, a code is still included and can be exchanged for tokens. If the user closed before signing in, no code is present.
const params = new URLSearchParams(window.location.search);const code = params.get('code');const state = params.get('state');const toneId = params.get('tone_id');const canceled = params.get('canceled') === 'true';if (state !== sessionStorage.getItem('t3k_state')) {throw new Error('State mismatch. Possible CSRF attack.');}if (canceled) {// User exited without selecting a tone.// If code is present, you can still exchange it for tokens.// If code is absent, the user closed before signing in.return;}
Step 4: Exchange the Code and Fetch the Tone
Exchange the authorization code for an access token (see Token Exchange), then use the tone_id from the callback to fetch tone metadata and model download URLs.
// Token exchange: see Authentication sectionconst { access_token } = await exchangeCode(code);// Fetch tone metadataconst tone = await fetch(`https://www.tone3000.com/api/v1/tones/${toneId}`, {headers: { Authorization: `Bearer ${access_token}` },}).then(r => r.json());// Fetch models — each has a model_url for downloadingconst { data: models } = await fetch(`https://www.tone3000.com/api/v1/models?tone_id=${toneId}`,{ headers: { Authorization: `Bearer ${access_token}` } },).then(r => r.json());
Load Tone
Load Tone is for apps that already know which tone they want. Your app passes a tone_id and TONE3000 handles authentication and access verification. If the tone is accessible, the user is redirected straight back to your app. If it's private or deleted, the user can browse for a replacement — your app receives the result either way. You can optionally pass gears and platform filters to scope the replacement browse view.
Step 1: Send the User to TONE3000
Build the authorization URL with prompt=load_tone and the tone_id you want to load. Optionally pass gears and platform to filter the replacement browse view if the tone is inaccessible. See Authentication for PKCE generation details.
// PKCE generation: see Authentication sectionconst params = new URLSearchParams({client_id: 'YOUR_CLIENT_ID',redirect_uri: 'https://your-app.com/callback',response_type: 'code',code_challenge: codeChallenge,code_challenge_method: 'S256',state,prompt: 'load_tone',tone_id: '42',// gears: 'amp', // optional: filter replacement browse by gear type// platform: 'nam', // optional: filter replacement browse by platform// menubar: 'true', // optional: show navigation bar with back/forward/refresh/close});window.location.href = `https://www.tone3000.com/api/v1/oauth/authorize?${params}`;
| Parameter | Description |
|---|---|
prompt | Must be load_tone (required) |
tone_id | The ID of the tone to load (required) |
gears | Restrict the replacement browse view to one or more gear types. Separate multiple values with _ (e.g. amp_full-rig). Only applied when the user needs to browse for a replacement (optional, see Gear) |
platform | Restrict the replacement browse view to a specific model format. Only applied when the user needs to browse for a replacement (optional, see Platform) |
menubar | Set to true to show a navigation bar at the top of the TONE3000 experience with back, forward, refresh, and close buttons. Recommended for in-app browsers and popup windows (optional) |
Step 2: TONE3000 Verifies Access
After sign-in, TONE3000 checks whether the user can access the requested tone: public tones, tones they own, and tones they've favorited all proceed immediately. If the tone is private or deleted, TONE3000 shows a friendly error page with the option to browse the catalog and pick a replacement. Any gears or platform filters you passed are applied to that replacement browse view.
When a user selects a replacement tone, the callback is the same as a successful load. The tone_id in the callback will be the newly selected tone, not the one you originally requested.
Step 3: Handle the Callback
TONE3000 redirects to your redirect_uri with a code, state, and the resolved tone_id. Always verify state before proceeding. Note that tone_id may differ from your original request if the user selected a replacement.
If the menubar is enabled and the user clicks the close button, the redirect will include canceled=true instead of a tone_id. If the user had already signed in, a code is still included and can be exchanged for tokens. If the user closed before signing in, no code is present.
const params = new URLSearchParams(window.location.search);const code = params.get('code');const state = params.get('state');const toneId = params.get('tone_id'); // may differ from your original requestconst canceled = params.get('canceled') === 'true';if (state !== sessionStorage.getItem('t3k_state')) {throw new Error('State mismatch. Possible CSRF attack.');}if (canceled) {// User exited without loading a tone.// If code is present, you can still exchange it for tokens.// If code is absent, the user closed before signing in.return;}
Step 4: Exchange the Code and Fetch the Tone
Exchange the authorization code for an access token (see Token Exchange), then use the tone_id from the callback to fetch tone metadata and model download URLs.
// Token exchange: see Authentication sectionconst { access_token } = await exchangeCode(code);// Fetch tone metadataconst tone = await fetch(`https://www.tone3000.com/api/v1/tones/${toneId}`, {headers: { Authorization: `Bearer ${access_token}` },}).then(r => r.json());// Fetch models — each has a model_url for downloadingconst { data: models } = await fetch(`https://www.tone3000.com/api/v1/models?tone_id=${toneId}`,{ headers: { Authorization: `Bearer ${access_token}` } },).then(r => r.json());
User
Get information about the currently authenticated user.
const response = await fetch('https://www.tone3000.com/api/v1/user', {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const user = await response.json();
- Response type: User
Users
Get a list of users with public content, sorted by various metrics.
// Get top users by tones countconst response = await fetch('https://www.tone3000.com/api/v1/users?sort=tones&page=1&page_size=10', {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const result = await response.json();// Search for users with username containing "john"const searchResponse = await fetch('https://www.tone3000.com/api/v1/users?query=john', {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const searchResult = await searchResponse.json();
- Response type: PaginatedResponse<PublicUser[]>
- Query parameters:
Name Type Description sortUsersSort Sort users by most stat (default: 'tones', optional) pagenumber Page number for pagination (default: 1, optional) page_sizenumber Number of users per page (default: 10, max: 10, optional) querystring Text search query to filter users by username (optional)
Tones
Created Tones
Get a list of tones created by the currently authenticated user.
const response = await fetch(`https://www.tone3000.com/api/v1/tones/created?page=${page}&page_size=${pageSize}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const tones = await response.json();
- Response type: PaginatedResponse<Tones[]>
- Query parameters:
Name Type Description pagenumber Page number for pagination (default: 1, optional) page_sizenumber Number of items per page (default: 10, max: 100, optional)
Favorited Tones
Get a list of tones favorited by the currently authenticated user.
const response = await fetch(`https://www.tone3000.com/api/v1/tones/favorited?page=${page}&page_size=${pageSize}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const tones = await response.json();
- Response type: PaginatedResponse<Tones[]>
- Query parameters:
Name Type Description pagenumber Page number for pagination (default: 1, optional) page_sizenumber Number of items per page (default: 10, max: 100, optional)
Get Tone
Get a single tone by ID. Public tones are accessible to any authenticated user. Private tones are only accessible to the owner or users who have favorited the tone.
const response = await fetch(`https://www.tone3000.com/api/v1/tones/${toneId}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const tone = await response.json();
- Response type: Tone
- To get download URLs for a tone's models, use the List Models endpoint.
- Path parameters:
Name Type Description idnumber ID of the tone to retrieve
Search Tones
Search and filter tones with various filters and sorting options.
const response = await fetch(`https://www.tone3000.com/api/v1/tones/search?query=${query}&page=${page}&page_size=${pageSize}&sort=${sort}&gears=${gears}&sizes=${sizes}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const tones = await response.json();
- Response type: PaginatedResponse<Tones[]>
- Query parameters:
Name Type Description querystring Search query term (optional, default: empty string) pagenumber Page number for pagination (default: 1, optional) page_sizenumber Number of items per page (default: 10, max: 25, optional) sortTonesSort Sort order (default: 'best-match' if query provided, 'trending' otherwise) gearsGear[] Filter by gear type. Underscore-separated for multiple values (e.g. amp_pedal_ir) (optional)sizesSize[] Filter by model sizes. Underscore-separated for multiple values (e.g. standard_lite_feather) (optional)
Models
Get Model
Get a single model by ID. Accessible if the parent tone is public, owned by the authenticated user, or favorited by the authenticated user.
const response = await fetch(`https://www.tone3000.com/api/v1/models/${modelId}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const model = await response.json();
- Response type: Model
- The model_url field is a pre-built download URL. Pass your access token as a Bearer token when fetching it.
- Path parameters:
Name Type Description idnumber ID of the model to retrieve
List Models
Get a list of models for a specific tone accessible by the current authenticated user.
const response = await fetch(`https://www.tone3000.com/api/v1/models?tone_id=${toneId}&page=${page}&page_size=${pageSize}`, {headers: {'Authorization': `Bearer ${accessToken}`,'Content-Type': 'application/json'}});const models = await response.json();
- Response type: PaginatedResponse<Model[]>
- Query parameters:
Name Type Description tone_idnumber ID of the tone to get models for pagenumber Page number for pagination (default: 1, optional) page_sizenumber Number of items per page (default: 10, max: 100, optional)
The model_url field can be used to download the model. It is only valid for tones that are accessible by the current authenticated user.
// Download model in browserconst downloadModel = async (modelUrl: string) => {const response = await fetch(modelUrl, {headers: {'Authorization': `Bearer ${accessToken}`}});if (!response.ok) {throw new Error('Failed to download model');}const blob = await response.blob();const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = modelUrl.split('/').pop() || 'model';document.body.appendChild(a);a.click();window.URL.revokeObjectURL(url);document.body.removeChild(a);};// Usageawait downloadModel(model.model_url);
Enums
Gear
In the search API, use underscore-separated values for multiple gear types (e.g. amp_pedal_ir). Comma-separated is deprecated.
enum Gear {Amp = 'amp',FullRig = 'full-rig',Pedal = 'pedal',Outboard = 'outboard',Ir = 'ir'}
Platforms
enum Platform {Nam = 'nam',Ir = 'ir',AidaX = 'aida-x',AaSnapshot = 'aa-snapshot',Proteus = 'proteus'}
Licenses
enum License {T3k = 't3k',CcBy = 'cc-by',CcBySa = 'cc-by-sa',CcByNc = 'cc-by-nc',CcByNcSa = 'cc-by-nc-sa',CcByNd = 'cc-by-nd',CcByNcNd = 'cc-by-nc-nd',Cco = 'cco'}
Sizes
In the search API, use hyphen-separated values for multiple sizes (e.g. standard-lite-feather). Comma-separated is deprecated.
enum Size {Standard = 'standard',Lite = 'lite',Feather = 'feather',Nano = 'nano',Custom = 'custom'}
UsersSort
enum UsersSort {Tones = 'tones',Downloads = 'downloads',Favorites = 'favorites',Models = 'models'}
TonesSort
enum TonesSort {BestMatch = 'best-match',Newest = 'newest',Oldest = 'oldest',Trending = 'trending',DownloadsAllTime = 'downloads-all-time'}
Types
Session
The response type for both session creation and refresh endpoints.
interface Session {access_token: str;refresh_token: str;expires_in: number; // seconds until token expirestoken_type: 'bearer';}
EmbeddedUser
interface EmbeddedUser {id: int;username: str;avatar_url: str | null;url: str;}
User
interface User extends EmbeddedUser {bio: str | null;links: str[] | null;created_at: str;updated_at: str;}
PublicUser
Public user information with content counts, returned by the users endpoint.
interface PublicUser {id: int;username: str;bio: str | null;links: str[] | null;avatar_url: str | null;downloads_count: number;favorites_count: number;models_count: number;tones_count: number;url: str;}
Make
interface Make {id: number;name: str;}
Tag
interface Tag {id: number;name: str;}
Paginated Response
interface PaginatedResponse<T> {data: T[];page: number;page_size: number;total: number;total_pages: number;}
Tone
interface Tone {id: number;user_id: int;user: EmbeddedUser;created_at: str;updated_at: str;title: str;description: str | null;gear: Gear;images: str[] | null;is_public: boolean | null;links: str[] | null;platform: Platform;license: License;sizes: Size[];makes: Make[];tags: Tag[];models_count: number;downloads_count: number;favorites_count: number;url: str;}
Model
interface Model {id: number;created_at: str;updated_at: str;user_id: int;model_url: str;name: str;size: Size;tone_id: number;}
Example App
The example repository contains self-contained demo apps — one for each integration flow — along with tone3000-client.ts, a zero-dependency helper that covers all OAuth flows and API endpoints.
Acme Inc — Select Flow
User browses the TONE3000 catalog and picks a tone. The app receives the selected tone_id and fetches tone metadata and model download URLs.
Beacon Inc — Load Tone Flow
App stores TONE3000 tone IDs in presets and loads them on demand. TONE3000 handles auth and access — if a tone is unavailable, the user can pick a replacement.
Chord Inc — Full API Integration
Reference implementation covering every documented endpoint: search, tone detail, user profile, favorites, model listings, and file downloads.
tone3000-client.ts
src/tone3000-client.ts is a zero-dependency integration helper included as inspiration for your own integration. It covers PKCE generation, OAuth flows, automatic token refresh, and authenticated requests via T3KClient.
import { startSelectFlow, handleOAuthCallback, T3KClient } from './tone3000-client';// Start a flowawait startSelectFlow(PUBLISHABLE_KEY, REDIRECT_URI);// Handle the callbackconst result = await handleOAuthCallback(PUBLISHABLE_KEY, REDIRECT_URI);if (result.ok) {client.setTokens(result.tokens);const { toneId } = result;}// Make authenticated API requestsconst client = new T3KClient(PUBLISHABLE_KEY, () => {startSelectFlow(PUBLISHABLE_KEY, REDIRECT_URI); // called when re-auth is needed});const tone = await client.getTone(toneId);const { data: models } = await client.listModels(toneId);await client.downloadModel(models[0].model_url, models[0].name);