Make your first request
Read one product from your store, check the result, and choose what to build next.
This request reads your catalog. It does not create a product or charge anyone.
You need a SellApp store and a server-side API key with the listing ability.
Create the key in your store's Developer settings. Keep it out of browser code
and source control. See authentication for OAuth and permissions.
1. Set your credentials
Replace replace-me with your API key and launch-lab with your store slug.
Run these commands in a terminal with cURL installed:
export SELLAPP_API_KEY='replace-me'
export SELLAPP_STORE='launch-lab'
export SELLAPP_API_BASE_URL='https://sell.app/api'The cURL and SDK programs below pass SELLAPP_API_BASE_URL explicitly.
Setting it alone does not configure every SDK.
2. List one product
curl --silent --show-error --fail-with-body \
--url "${SELLAPP_API_BASE_URL}/v2/products?limit=1" \
--header "Authorization: Bearer ${SELLAPP_API_KEY}" \
--header "X-STORE: ${SELLAPP_STORE}" \
--header 'Accept: application/json'A successful request returns HTTP 200. Read the data array in the JSON
response: it contains at most one product. Product IDs come from your store;
use the returned ID in later requests instead of copying an illustrative ID.
An empty store returns an empty data array. That is a successful request,
not an authentication problem. Create your first product
when you are ready.
3. If the request fails
| Response | What to do |
|---|---|
401 | Check that your key is present, valid, and has not been revoked. |
403 | Check the key's listing ability and your permission to access this store. |
400 with OAuth | Supply X-STORE with the store you selected. |
429 | Wait for the response's retry guidance before sending another request. |
Keep the X-Request-ID response header when asking for help. Never share your
API key. See errors for the complete response format.
Use an SDK or the CLI
The official SDKs cover TypeScript, Python, PHP, Go, .NET, Kotlin, Ruby, Rust, and Elixir. Their first-request examples use the same environment credentials. See CLI setup for installing the command-line tool.
Install the SDK for your language using its source installation instructions,
then run the matching complete program below. Each example makes the same catalog
read. The CLI reads SELLAPP_API_BASE_URL; --base-url overrides it.
SDK programs print the product or a successful empty-store message. The CLI
returns a JSON array when its output is redirected; [] means the read succeeded.
import { SellApp } from 'sellapp';// Explicit endpoint selection keeps examples from accidentally calling a live store.const baseUrl = process.env.SELLAPP_API_BASE_URL;if (!baseUrl) throw new Error('Set SELLAPP_API_BASE_URL before running this example');const client = new SellApp({ baseUrl }); // Reads SELLAPP_API_KEY and SELLAPP_STORE.const page = await client.products.list({ limit: 1 });for (const product of page.data) { console.log(product.id, product.title);}if (page.data.length === 0) console.log('No products yet. The request worked!');import osfrom sellapp_sdk import SellAppClientbase_url = os.environ["SELLAPP_API_BASE_URL"]if not base_url.strip(): raise ValueError("Set a nonempty SELLAPP_API_BASE_URL before running this example")with SellAppClient(base_url=base_url) as client: page = client.products.list(limit=1) for product in page.data: print(product.id, product.title) if not page.data: print("No products yet. Your connection is ready.")<?phpdeclare(strict_types=1);require __DIR__ . '/../vendor/autoload.php';use SellApp\Client;$baseUrl = getenv('SELLAPP_API_BASE_URL');if (!$baseUrl) { throw new RuntimeException('Set SELLAPP_API_BASE_URL before running this example');}$client = new Client(baseUrl: $baseUrl); // Reads SELLAPP_API_KEY and SELLAPP_STORE.$page = $client->products()->list(limit: 1);foreach ($page->data as $product) { echo $product->id . ' ' . $product->title . PHP_EOL;}if ($page->data === []) { echo 'No products yet. The request worked!' . PHP_EOL;}package mainimport ( "context" "errors" "fmt" "io" "os" "time" "github.com/sellapp/sellapp-go")func firstRequest(ctx context.Context, client *sellapp.Client, out io.Writer) error { limit := 1 products := client.Products().List(ctx, &sellapp.ProductsListParams{Limit: &limit}) if products.Next() { product := products.Current() fmt.Fprintf(out, "%d: %s\n", product.ID, product.Title) } else if products.Err() == nil { fmt.Fprintln(out, "No products yet. Your connection is ready.") } return products.Err()}func paginate(ctx context.Context, client *sellapp.Client, out io.Writer) error { limit := 15 products := client.Products().List(ctx, &sellapp.ProductsListParams{Limit: &limit}) // Keep this example bounded: inspect at most 30 products. for count := 0; count < 30 && products.Next(); count++ { fmt.Fprintf(out, "%d: %s\n", products.Current().ID, products.Current().Title) } return products.Err()}func reportError(err error, out io.Writer) { var authentication *sellapp.AuthenticationError var rateLimit *sellapp.RateLimitExceededError var timeout *sellapp.TimeoutError switch { case errors.As(err, &authentication): fmt.Fprintf(out, "Check the API key and store: %s (request %s)\n", authentication.Message, authentication.RequestID) case errors.As(err, &rateLimit): fmt.Fprintf(out, "Rate limited: %s (request %s)\n", rateLimit.Message, rateLimit.RequestID) case errors.As(err, &timeout): fmt.Fprintln(out, "Request timed out:", timeout) default: fmt.Fprintln(out, "Request failed:", err) }}func run(out io.Writer) error { // An explicit URL keeps accidental example runs from reaching production. baseURL := os.Getenv("SELLAPP_API_BASE_URL") if baseURL == "" || os.Getenv("SELLAPP_API_KEY") == "" || os.Getenv("SELLAPP_STORE") == "" { return fmt.Errorf("set SELLAPP_API_BASE_URL, SELLAPP_API_KEY, and SELLAPP_STORE") } client := sellapp.NewClient("", "", sellapp.WithBaseURL(baseURL), sellapp.WithMaxRetries(0)) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if os.Getenv("SELLAPP_EXAMPLE_MODE") == "pagination" { return paginate(ctx, client, out) } return firstRequest(ctx, client, out)}func main() { if err := run(os.Stdout); err != nil { reportError(err, os.Stderr) os.Exit(1) }}using SellApp;using Newtonsoft.Json.Linq;namespace SellAppExamples;public static class Onboarding{ public static async Task FirstRequestAsync(SellAppClient client, TextWriter output, CancellationToken ct) { var page = await client.Products.ListAsync(new ProductsListOptions { Limit = 1 }, cancellationToken: ct); foreach (var product in page.Data) await output.WriteLineAsync($"{product.Id}: {product.Title}"); if (page.Data.Count == 0) await output.WriteLineAsync("No products yet. Your connection is ready."); } public static async Task PaginateAsync(SellAppClient client, TextWriter output, CancellationToken ct) { // Bound this example to three pages; ask for each page explicitly. for (var number = 1; number <= 3; number++) { var page = await client.Products.ListAsync( new ProductsListOptions { Limit = 15, Page = number }, cancellationToken: ct); foreach (var product in page.Data) await output.WriteLineAsync($"{product.Id}: {product.Title}"); if (page.Meta?["current_page"]?.Value<int>() >= page.Meta?["last_page"]?.Value<int>()) break; } } public static async Task<long> CatalogWorkflowAsync(SellAppClient client, CancellationToken ct) { // Creates and updates real catalog data when used outside the fixture tests. var created = await client.Products.CreateAsync(new ProductsCreateOptions { Title = "Design kit", Description = "Templates for your next project.", Visibility = new CatalogVisibility("HIDDEN") }, cancellationToken: ct); var product = await client.Products.GetAsync(created.Data.Id.ToString(), cancellationToken: ct); var updated = await client.Products.UpdateAsync(product.Data.Id.ToString(), new ProductsUpdateOptions { Title = "Design kit revised" }, cancellationToken: ct); return updated.Data.Id; } public static async Task<long> CheckoutAsync(SellAppClient client, string orderId, CancellationToken ct) { // Starts a real payment-provider checkout. Inspect current order state before retrying. var order = await client.Orders.GetAsync(orderId, cancellationToken: ct); var checkout = await client.Orders.CreateCheckoutAsync(order.Data.Id.ToString(), new OrdersCreateCheckoutOptions {}, cancellationToken: ct); return checkout.Data.Id; } public static async Task<long> UploadAsync(SellAppClient client, string productId, string variantId, byte[] file, CancellationToken ct) { var uploaded = await client.VariantDeliverableFiles.UploadAsync(productId, variantId, new VariantDeliverableFilesUploadOptions { File = file }, cancellationToken: ct); var saved = await client.VariantDeliverableFiles.GetAsync(productId, variantId, uploaded.Data.Id.ToString(), cancellationToken: ct); return saved.Data.Id; } public static string DescribeError(Exception error) => error switch { AuthenticationException e => $"Check your API key and store: {e.Message}", ApiException e => $"API status {e.Status}: {e.Message} (request {e.RequestId ?? "unavailable"})", SellAppTimeoutException e => $"Request timed out: {e.Message}", OperationCanceledException => "Request canceled.", _ => $"Request failed: {error.Message}", }; public static async Task<int> Main(string[] args) { try { string Required(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new InvalidOperationException($"Set {name} before running this example."); using var client = new SellAppClient(new SellAppOptions { ApiKey = Required("SELLAPP_API_KEY"), Store = Required("SELLAPP_STORE"), BaseUrl = Required("SELLAPP_API_BASE_URL"), MaxRetries = 0, }); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30)); if (args.Contains("pagination")) await PaginateAsync(client, Console.Out, cancellation.Token); else await FirstRequestAsync(client, Console.Out, cancellation.Token); return 0; } catch (Exception exception) { Console.Error.WriteLine(DescribeError(exception)); return 1; } }}package sellapp.examplesimport app.sell.sellapp.SellAppimport app.sell.sellapp.common.exceptions.SellAppApiExceptionimport app.sell.sellapp.common.exceptions.SellAppSerializationExceptionimport app.sell.sellapp.common.exceptions.SellAppTimeoutExceptionimport kotlinx.coroutines.runBlockingimport okhttp3.OkHttpClientimport app.sell.sellapp.types.CatalogVisibilityimport app.sell.sellapp.common.http.PatchFieldfun firstRequest(client: SellApp): String { val product = client.products.list(limit = 1).data.firstOrNull() ?: return "No products yet. Your connection is ready." return "${product.id}: ${product.title}"}suspend fun firstRequestSuspend(client: SellApp): String { val product = client.products.listSuspend(limit = 1).data.firstOrNull() ?: return "No products yet. Your connection is ready." return "${product.id}: ${product.title}"}fun inspectProducts(client: SellApp): String { return try { client.products.list(limit = 1).take(30).joinToString("\n") { "${it.id}: ${it.title}" }.ifEmpty { "No products yet." } } catch (error: SellAppSerializationException) { "Product pages could not be decoded safely: ${error.message}." }}fun catalogWorkflow(client: SellApp): Long { // This changes real catalog data outside the local fixture test. val created = client.products.create(title = "Design kit", description = "Templates for your next project.", visibility = CatalogVisibility.Hidden) val product = client.products.get(created.data.id.toString()) return client.products.update(product.data.id.toString(), title = PatchField.Present("Design kit revised")).data.id}suspend fun catalogWorkflowSuspend(client: SellApp): Long { val created = client.products.createSuspend(title = "Design kit", description = "Templates for your next project.", visibility = CatalogVisibility.Hidden) val product = client.products.getSuspend(created.data.id.toString()) return client.products.updateSuspend(product.data.id.toString(), title = PatchField.Present("Design kit revised")).data.id}fun checkout(client: SellApp, orderId: String): Long { // Creates a provider checkout; inspect the order before retrying a lost response. val order = client.orders.get(orderId) return client.orders.createCheckout(order.data.id.toString()).data.id}fun upload(client: SellApp, productId: String, variantId: String, file: ByteArray): Long { val uploaded = client.variantDeliverableFiles.upload(productId, variantId, file) return client.variantDeliverableFiles.get(productId, variantId, uploaded.data.id.toString()).data.id}suspend fun checkoutSuspend(client: SellApp, orderId: String): Long { val order = client.orders.getSuspend(orderId) return client.orders.createCheckoutSuspend(order.data.id.toString()).data.id}suspend fun uploadSuspend(client: SellApp, productId: String, variantId: String, file: ByteArray): Long { val uploaded = client.variantDeliverableFiles.uploadSuspend(productId, variantId, file) return client.variantDeliverableFiles.getSuspend(productId, variantId, uploaded.data.id.toString()).data.id}fun describeError(error: Exception): String = when (error) { is SellAppApiException -> "API status ${error.status}: ${error.message} (request ${error.requestId ?: "unavailable"})" is SellAppTimeoutException -> "Request timed out: ${error.message}" else -> "Request failed: ${error.message}"}fun main(args: Array<String>) { fun required(name: String): String = System.getenv(name)?.takeIf { it.isNotBlank() } ?: error("Set $name before running this example.") val http = OkHttpClient() try { val client = SellApp( apiKey = required("SELLAPP_API_KEY"), store = required("SELLAPP_STORE"), baseUrl = required("SELLAPP_API_BASE_URL"), maxRetries = 0, httpClient = http, ) val result = when (args.firstOrNull()) { "suspend" -> runBlocking { firstRequestSuspend(client) } "pagination" -> inspectProducts(client) else -> firstRequest(client) } println(result) } catch (error: Exception) { System.err.println(describeError(error)) throw error } finally { http.dispatcher.executorService.shutdown() http.connectionPool.evictAll() http.cache?.close() }}# frozen_string_literal: truerequire "sellapp"base_url = ENV.fetch("SELLAPP_API_BASE_URL")raise "Set SELLAPP_API_BASE_URL before running this example" if base_url.empty?client = SellApp::Client.new(base_url: base_url) # Reads SELLAPP_API_KEY and SELLAPP_STORE.page = client.products.list(limit: 1)page.data.each { |product| puts "#{product.id} #{product.title}" }puts "No products yet. The request worked!" if page.data.empty?use sellapp::{Client, Error};use sellapp::resources::products::ListParams;pub async fn first_request(client: &Client) -> Result<Vec<(i64, String)>, Error> { let page = client.products().list(ListParams { limit: Some(1), ..Default::default() }).await?; let products: Vec<_> = page.data.into_iter().map(|p| (p.id, p.title)).collect(); for (id, title) in &products { println!("{id}: {title}"); } if products.is_empty() { println!("No products yet. Your connection is ready."); } Ok(products)}#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Require a deliberate destination for this runnable example. let base_url = std::env::var("SELLAPP_API_BASE_URL")?; let client = Client::from_env()?.with_base_url(base_url).with_max_retries(0); first_request(&client).await?; Ok(())}base_url = System.fetch_env!("SELLAPP_API_BASE_URL")if base_url == "", do: raise("Set SELLAPP_API_BASE_URL before running this example")client = SellApp.client(base_url: base_url)# Credentials come from SELLAPP_API_KEY and SELLAPP_STORE.{:ok, page} = SellApp.Products.list(client, %{limit: 1})Enum.each(page.data, fn product -> IO.puts("#{product.id} #{product.title}") end)if page.data == [], do: IO.puts("No products yet. The request worked!")sellapp products list --limit 1What next?
- Create a product and variant.
- Create an order checkout.
- Receive order events.
- Advanced subscription checkout, including provider setup and webhooks.