Integration examples: one call, four platforms

Squiddy's data plane is one procedure, /squiddy.v1.ConfigService/GetRemoteConfiguration, spoken over Connect — which means an ordinary JSON POST. There is nothing to install: the examples below are the same call in four languages, sending the same body and reading the same answer with nothing but what each platform ships with.

There is no SDK yet, and that is why these exist. When one arrives it will not make them wrong — it will cover some of these platforms, and the rest will still call the service exactly like this.

If you do not have an organization, an API key and a published configuration yet, start from the quickstart: it takes you from signing up to your first answer. This page is that same call, in your language.

The call

WhatValue
MethodPOST
Path/squiddy.v1.ConfigService/GetRemoteConfiguration
CredentialX-API-Key: <your key>
BodyJSON (application/json), the six fields below

The key is what addresses the configuration. It belongs to exactly one environment, and an environment to exactly one project, so presenting it tells the server which organization, project and environment you mean: none of the three is in the body. Note the header — X-API-Key, not an Authorization: Bearer, which this plane does not read.

The values you supply

Every example takes these three from the environment. The key is a credential, so it is read from the environment rather than passed as an argument: an argument is in your shell history and in every process listing on the machine.

VariableWhat to put in it
SQUIDDY_HOSTThe origin your Squiddy is served on, with no trailing slash: the scheme and host in your browser's address bar while you are signed in to the dashboard (http://localhost:7071 for a local run).
SQUIDDY_API_KEYAn API key secret. It is shown once, when the key is created.
SQUIDDY_APP_IDThe id of the app you published. Nothing published yet? Any id — say my-app — still runs, and is answered not_found, which already proves your key, your endpoint and your coordinates.

The request body

All six fields are required. appId names the configuration you want; the other five describe the app asking for it, and are what resolution is decided on — which is why they are in every example rather than hidden behind a default.

FieldWhat it is
appIdThe app whose configuration you are asking for.
appBundleIdYour app's bundle identifier. Put your own in.
appVersionThe running version, in YYYY.MM.DD. It is compared numerically against the rules a configuration carries, so 1.0.0 would sort below every rule written for a real release.
appTypeThe platform: a rule can answer one thing to ios and another to android.
appModeThe build: likewise, release and debug can be answered differently.
appLanguageThe language the dictionary in the answer is for.

The fields the examples read

The answer carries more than this — every field of it is in the ConfigService reference. Each example reads these 3, one of them nested, because reading a nested field is the part every language spells differently.

FieldWhat it tells you
configVersionThe version of the configuration that answered. It changes when you publish.
serverApi.baseUrlA field of the payload itself: the backend coordinates your app reads out of it.
configFingerprintSend it back as the request's configFingerprint next time and an unchanged configuration answers "notModified": true with the heavy blocks left out.

What each one needs

Every command below expects the three variables above in the environment.

ExampleNeedsRun
curlcurl and jq, plus a POSIX shellsh docs/examples/first-call.sh
TypeScriptNode 20+, Deno or Bun — the built-in fetch, nothing installednpx tsx docs/examples/first-call.ts
KotlinJDK 11+ and a JSON reader — org.json, which Android ships and the JVM gets from org.json:json. On Android put OkHttp where java.net.http is: the request is unchangedkotlinc FirstCall.kt -cp json.jar -d app.jar && java -cp app.jar:json.jar FirstCallKt
SwiftSwift 5.7+ and Foundation — nothing to install, on macOS or on Linuxswift docs/examples/FirstCall.swift

curl

The file: docs/examples/first-call.sh.

#!/usr/bin/env sh
#
# Squiddy — the first data-plane call, in curl. No SDK, and no dependency on Squiddy: plain
# HTTP/JSON on the public contract.
#
# It POSTs the six-field body below to /squiddy.v1.ConfigService/GetRemoteConfiguration,
# presenting the API key in the X-API-Key header. A key is bound to one environment, so the server
# derives the organization, the project and the environment from it — there is nothing to
# address by hand.
#
# Needs: `curl` and `jq`, plus a POSIX shell.
# Run:   sh docs/examples/first-call.sh
#        with SQUIDDY_HOST, SQUIDDY_API_KEY and SQUIDDY_APP_ID in the environment.
set -eu

# `:?` refuses to run with an input missing, instead of sending an empty one and leaving you to
# read the refusal as a problem with the product.
: "${SQUIDDY_HOST:?is not set: see docs/examples.md for what goes in it}"
: "${SQUIDDY_API_KEY:?is not set: see docs/examples.md for what goes in it}"
: "${SQUIDDY_APP_ID:?is not set: see docs/examples.md for what goes in it}"

# The key travels in a header, never in the URL and never in the body.
response=$(curl -sS -X POST "$SQUIDDY_HOST/squiddy.v1.ConfigService/GetRemoteConfiguration" \
  -H 'Content-Type: application/json' \
  -H "X-API-Key: $SQUIDDY_API_KEY" \
  --data-binary @- <<JSON
{
  "appId": "$SQUIDDY_APP_ID",
  "appBundleId": "com.example.app",
  "appVersion": "2026.01.01",
  "appType": "ios",
  "appMode": "release",
  "appLanguage": "en"
}
JSON
)

# A refusal answers with a Connect error object — `code` and `message` — where a configuration
# would be. Reading the fields off it anyway would report an empty configuration as a real one.
if printf '%s' "$response" | jq -e 'has("code")' >/dev/null; then
  printf 'the call was refused: %s\n' "$response" >&2
  exit 1
fi

# The fields this example reads. The whole answer is on stdout above; `jq` is only reading it.
printf '%s' "$response" | jq -r '
  "configVersion       " + .configVersion,
  "serverApi.baseUrl   " + .serverApi.baseUrl,
  "configFingerprint   " + .configFingerprint'

TypeScript

The file: docs/examples/first-call.ts.

//
// Squiddy — the first data-plane call, in TypeScript. No SDK, and no dependency on Squiddy:
// plain HTTP/JSON on the public contract.
//
// It POSTs the six-field body below to /squiddy.v1.ConfigService/GetRemoteConfiguration,
// presenting the API key in the X-API-Key header. A key is bound to one environment, so the
// server derives the organization, the project and the environment from it — there is nothing
// to address by hand.
//
// Needs: Node 20+, Deno or Bun — the built-in `fetch`, nothing installed.
// Run:   npx tsx docs/examples/first-call.ts
//        with SQUIDDY_HOST, SQUIDDY_API_KEY and SQUIDDY_APP_ID in the environment.

/** requireEnv reads one required input, refusing to run rather than sending an empty value. */
function requireEnv(name: string): string {
  const value = process.env[name];
  if (value === undefined || value === "") {
    throw new Error(`${name} is not set: see docs/examples.md for what goes in it`);
  }
  return value;
}

const host = requireEnv("SQUIDDY_HOST");
const apiKey = requireEnv("SQUIDDY_API_KEY");
const appId = requireEnv("SQUIDDY_APP_ID");

// The body is written as JSON text, not built from an object, so what is on the wire is what you
// see here.
const body = `
{
  "appId": "${appId}",
  "appBundleId": "com.example.app",
  "appVersion": "2026.01.01",
  "appType": "ios",
  "appMode": "release",
  "appLanguage": "en"
}
`;

const response = await fetch(`${host}/squiddy.v1.ConfigService/GetRemoteConfiguration`, {
  method: "POST",
  // The key travels in a header, never in the URL and never in the body.
  headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
  body,
});
if (!response.ok) {
  throw new Error(`the call was refused with HTTP ${response.status}: ${await response.text()}`);
}

/** The fields this example reads. The reference documents every other one. */
interface RemoteConfiguration {
  readonly configVersion: string;
  readonly serverApi: { readonly baseUrl: string };
  readonly configFingerprint: string;
}

const payload = (await response.json()) as RemoteConfiguration;
console.log("configVersion       " + payload.configVersion);
console.log("serverApi.baseUrl   " + payload.serverApi.baseUrl);
console.log("configFingerprint   " + payload.configFingerprint);

Kotlin

The file: docs/examples/FirstCall.kt.

//
// Squiddy — the first data-plane call, in Kotlin. No SDK, and no dependency on Squiddy: plain
// HTTP/JSON on the public contract.
//
// It POSTs the six-field body below to /squiddy.v1.ConfigService/GetRemoteConfiguration,
// presenting the API key in the X-API-Key header. A key is bound to one environment, so the
// server derives the organization, the project and the environment from it — there is nothing
// to address by hand.
//
// Needs: JDK 11+ and a JSON reader — `org.json`, which Android ships and the JVM gets from
//        `org.json:json`. On Android put OkHttp where `java.net.http` is: the request is
//        unchanged.
// Run:   kotlinc FirstCall.kt -cp json.jar -d app.jar && java -cp app.jar:json.jar FirstCallKt
//        with SQUIDDY_HOST, SQUIDDY_API_KEY and SQUIDDY_APP_ID in the environment.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import org.json.JSONObject

/** requireEnv reads one required input, refusing to run rather than sending an empty value. */
fun requireEnv(name: String): String = System.getenv(name)?.takeIf { it.isNotEmpty() }
    ?: error("$name is not set: see docs/examples.md for what goes in it")

fun main() {
    val host = requireEnv("SQUIDDY_HOST")
    val apiKey = requireEnv("SQUIDDY_API_KEY")
    val appId = requireEnv("SQUIDDY_APP_ID")

    // The body is written as JSON text, not built from a JSONObject, so what is on the wire is
    // what you see here.
    val body = """
{
  "appId": "$appId",
  "appBundleId": "com.example.app",
  "appVersion": "2026.01.01",
  "appType": "ios",
  "appMode": "release",
  "appLanguage": "en"
}
""".trimIndent()

    val url = URI.create(host + "/squiddy.v1.ConfigService/GetRemoteConfiguration")
    val request = HttpRequest.newBuilder(url)
        .header("Content-Type", "application/json")
        // The key travels in a header, never in the URL and never in the body.
        .header("X-API-Key", apiKey)
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build()
    val response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString())
    if (response.statusCode() != 200) {
        error("the call was refused with HTTP ${response.statusCode()}: ${response.body()}")
    }

    val payload = JSONObject(response.body())
    println("configVersion       " + payload.getString("configVersion"))
    println("serverApi.baseUrl   " + payload.getJSONObject("serverApi").getString("baseUrl"))
    println("configFingerprint   " + payload.getString("configFingerprint"))
}

Swift

The file: docs/examples/FirstCall.swift.

//
// Squiddy — the first data-plane call, in Swift. No SDK, and no dependency on Squiddy: plain
// HTTP/JSON on the public contract.
//
// It POSTs the six-field body below to /squiddy.v1.ConfigService/GetRemoteConfiguration,
// presenting the API key in the X-API-Key header. A key is bound to one environment, so the
// server derives the organization, the project and the environment from it — there is nothing
// to address by hand.
//
// Needs: Swift 5.7+ and Foundation — nothing to install, on macOS or on Linux.
// Run:   swift docs/examples/FirstCall.swift
//        with SQUIDDY_HOST, SQUIDDY_API_KEY and SQUIDDY_APP_ID in the environment.
import Foundation

/// requireEnv reads one required input, refusing to run rather than sending an empty value.
func requireEnv(_ name: String) -> String {
    guard let value = ProcessInfo.processInfo.environment[name], !value.isEmpty else {
        fatalError("\(name) is not set: see docs/examples.md for what goes in it")
    }
    return value
}

let host = requireEnv("SQUIDDY_HOST")
let apiKey = requireEnv("SQUIDDY_API_KEY")
let appId = requireEnv("SQUIDDY_APP_ID")

// The body is written as JSON text, not built from a dictionary, so what is on the wire is what
// you see here.
let body = """
{
  "appId": "\(appId)",
  "appBundleId": "com.example.app",
  "appVersion": "2026.01.01",
  "appType": "ios",
  "appMode": "release",
  "appLanguage": "en"
}
"""

guard let url = URL(string: host + "/squiddy.v1.ConfigService/GetRemoteConfiguration") else {
    fatalError("SQUIDDY_HOST is not a URL: \(host)")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// The key travels in a header, never in the URL and never in the body.
request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
request.httpBody = Data(body.utf8)

let (data, response) = try await URLSession.shared.data(for: request)
guard let status = (response as? HTTPURLResponse)?.statusCode, status == 200 else {
    fatalError("the call was refused: " + (String(data: data, encoding: .utf8) ?? ""))
}

let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
print("configVersion       " + (payload["configVersion"] as? String ?? ""))
print("serverApi.baseUrl   " + ((payload["serverApi"] as? [String: Any])?["baseUrl"] as? String ?? ""))
print("configFingerprint   " + (payload["configFingerprint"] as? String ?? ""))

What they print

All four, on a served configuration:

configVersion       2026.08.07
serverApi.baseUrl   https://api.example.com/v1/
configFingerprint   a36f1250…3574c1

A brand-new organization has published nothing, so the same call is answered not_found (404, application code 5003) until you publish something — see step 5 of the quickstart, which shows all three answers the call can get.

Where to go next

Caching, in one line: keep configFingerprint and send it back in the next request. An unchanged configuration is then answered "notModified": true with the payload left out, which is a few hundred bytes instead of a few kilobytes and is the difference between an app that can ask often and one that cannot.