Quickstart
In about five minutes you’ll make your first call to the By Dawn API: you’ll swap an API key for an access token, then list your organization’s projects. Pick your language on any code tab, and every page on this site follows. Rather not install anything? Run it in your browser.
Read down the steps. The code on the right follows along and highlights the lines each one explains.
Create an API key
Section titled “Create an API key”In By Dawn, open Settings › Organization › Security and find API keys. Name the key, and choose Board — read only for now. You can make a read & write key later, when your code needs to change things. Copy the key straight away: it starts with
saw_live_, and By Dawn shows it only once.Only organization owners and admins can create keys. If that isn’t you, ask an admin for a read-only key. API keys walks through it.
Set up your language
Section titled “Set up your language”Each recipe is one file that uses only your language’s standard library, so there’s almost nothing to install.
Node.js 22.6 or later. The script has no dependencies, but it’s typed from our OpenAPI document, so generate the types next to it:
Terminal window npx openapi-typescript https://docs.bydawn.com/openapi/public.openapi.yaml -o saw-api.d.tsRegenerate them when the changelog records a change.
npx tsxandbunrun the file too.Python 3.9 or later. Nothing to install: the script uses
urllibandjson.JDK 17 or later and JBang, which runs a single
.javafile and fetches its one dependency, Jackson, for JSON. In a Maven or Gradle project, addcom.fasterxml.jackson.core:jackson-databindand drop the//lines at the top.The .NET 10 SDK, which runs a single
.csfile. No NuGet packages. The#:property PublishAot=falseline turns off Native AOT, whichdotnet runuses by default and which can’t read JSON this way. In a console project, leave that line out and paste the rest intoProgram.cs.Go 1.21 or later. Standard library only, so no module setup.
bash,curl7.66 or later, andjq1.6 or later, which builds the request bodies and reads the answers.Save the recipe under its file name, set
SAW_API_KEYto your key, and run the command that comes with it.Swap the key for an access token
Section titled “Swap the key for an access token”Your key never travels with ordinary requests. Instead, the script sends it once to the accounts host and gets back an access token that lasts about 15 minutes. If a token leaks into a log, it expires on its own.
A wrong or revoked key stops the script here with
Token exchange failed: invalid_api_key.// 1. Exchange the API key for a short-lived access token.const exchange: Schemas['ApiKeyExchange'] = { apiKey };const tokenResponse = await fetch(`${ACCOUNTS_URL}/auth/token/api-key`, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(exchange),});const token = (await tokenResponse.json()) as Schemas['AccessTokenData'] | Schemas['AccountsApiError'];if (!token.ok) throw new Error(`Token exchange failed: ${token.error}`);console.log(`Access token issued, valid for ${token.data.expiresInSeconds} seconds.`);# 1. Exchange the API key for a short-lived access token.token = call(f"{ACCOUNTS_URL}/auth/token/api-key", body={"apiKey": api_key})if not token["ok"]:sys.exit(f"Token exchange failed: {token['error']}")access_token = token["data"]["accessToken"]print(f"Access token issued, valid for {token['data']['expiresInSeconds']} seconds.")// 1. Exchange the API key for a short-lived access token.JsonNode token = send(HttpRequest.newBuilder(URI.create(accountsUrl + "/auth/token/api-key")).header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(Map.of("apiKey", apiKey)))).build());if (!token.path("ok").asBoolean()) throw new IllegalStateException("Token exchange failed: " + token.path("error").asText());String accessToken = token.path("data").path("accessToken").asText();System.out.println("Access token issued, valid for " + token.path("data").path("expiresInSeconds").asInt() + " seconds.");// 1. Exchange the API key for a short-lived access token.// Every response is an envelope: {"ok": true, "data": …} or {"ok": false, "error": "<slug>"}.var exchange = await http.PostAsJsonAsync($"{accountsUrl}/auth/token/api-key", new { apiKey });var token = await exchange.Content.ReadFromJsonAsync<Envelope<AccessTokenData>>();if (token is not { Ok: true, Data: { } data }) throw new InvalidOperationException($"Token exchange failed: {token?.Error}");Console.WriteLine($"Access token issued, valid for {data.ExpiresInSeconds} seconds.");// 1. Exchange the API key for a short-lived access token.exchange, _ := json.Marshal(map[string]string{"apiKey": apiKey})req, _ := http.NewRequest(http.MethodPost, accountsURL+"/auth/token/api-key", bytes.NewReader(exchange))req.Header.Set("Content-Type", "application/json")var token envelope[accessToken]send(req, &token)if !token.OK {fail("Token exchange failed: " + token.Error)}fmt.Printf("Access token issued, valid for %d seconds.\n", token.Data.ExpiresInSeconds)Terminal window # 1. Exchange the API key for a short-lived access token. jq reads the key from the# environment, so it never appears on a command line.token=$(jq -n '{apiKey: env.SAW_API_KEY}' |curl -sS "$ACCOUNTS_URL/auth/token/api-key" -H 'Content-Type: application/json' --data @-)if [ "$(jq -r .ok <<<"$token")" != true ]; thenecho "Token exchange failed: $(jq -r .error <<<"$token")" >&2exit 1fiaccess_token=$(jq -r .data.accessToken <<<"$token")echo "Access token issued, valid for $(jq -r .data.expiresInSeconds <<<"$token") seconds."Read your organization ID
Section titled “Read your organization ID”Every API path starts with your organization’s ID, and you don’t need to look it up: the token carries it. A token is a JWT, three parts separated by dots, and the middle part holds its claims, including
org_id.// 2. Read your organization ID from the token.// The token is a JWT; its `org_id` claim is the organization the key belongs to.const claims = JSON.parse(atob(token.data.accessToken.split('.')[1]!.replace(/-/g, '+').replace(/_/g, '/')));const orgId: string = claims.org_id;console.log(`Organization ID: ${orgId}`);# 2. Read your organization ID from the token.# The token is a JWT; its `org_id` claim is the organization the key belongs to.payload = access_token.split(".")[1]claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))org_id = claims["org_id"]print(f"Organization ID: {org_id}")// 2. Read your organization ID from the token.// The token is a JWT; its `org_id` claim is the organization the key belongs to.JsonNode claims = json.readTree(Base64.getUrlDecoder().decode(accessToken.split("\\.")[1]));String orgId = claims.path("org_id").asText();System.out.println("Organization ID: " + orgId);// 2. Read your organization ID from the token.// The token is a JWT; its `org_id` claim is the organization the key belongs to.var payload = data.AccessToken.Split('.')[1].Replace('-', '+').Replace('_', '/');var claims = Convert.FromBase64String(payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='));var orgId = JsonDocument.Parse(claims).RootElement.GetProperty("org_id").GetString();Console.WriteLine($"Organization ID: {orgId}");// 2. Read your organization ID from the token.// The token is a JWT; its `org_id` claim is the organization the key belongs to.payload, err := base64.RawURLEncoding.DecodeString(strings.Split(token.Data.AccessToken, ".")[1])if err != nil {fail("The access token is not a JWT: " + err.Error())}var claims struct {OrgID string `json:"org_id"`}if err := json.Unmarshal(payload, &claims); err != nil {fail("The access token is not a JWT: " + err.Error())}fmt.Printf("Organization ID: %s\n", claims.OrgID)Terminal window # 2. Read your organization ID from the token.# The token is a JWT; its `org_id` claim is the organization the key belongs to.org_id=$(jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | . + ("=" * ((4 - length % 4) % 4))| @base64d | fromjson | .org_id' <<<"$access_token")echo "Organization ID: $org_id"Call the API
Section titled “Call the API”Now send the token in an
Authorization: Bearer …header and ask for your projects. Every answer has the same shape:oksays whether it worked, and the projects are indata. That shape is called the envelope, and every call on this site reads it the same way.The script prints the token’s lifetime, your organization ID and one line per project.
// 3. Call the API with it: list the organization's projects.const projectsResponse = await fetch(`${WORKSPACE_URL}/orgs/${orgId}/projects`, {headers: { Authorization: `Bearer ${token.data.accessToken}` },});const projects = (await projectsResponse.json()) as Schemas['ProjectListData'] | Schemas['ApiError'];if (!projects.ok) throw new Error(`Listing projects failed: ${projects.error}`);console.log(`Your organization has ${projects.data.length} project(s):`);for (const project of projects.data) console.log(` ${project.name} (${project.id})`);# 3. Call the API with it: list the organization's projects.projects = call(f"{WORKSPACE_URL}/orgs/{org_id}/projects", token=access_token)if not projects["ok"]:sys.exit(f"Listing projects failed: {projects['error']}")print(f"Your organization has {len(projects['data'])} project(s):")for project in projects["data"]:print(f" {project['name']} ({project['id']})")// 3. Call the API with it: list the organization's projects.JsonNode projects = send(HttpRequest.newBuilder(URI.create(workspaceUrl + "/orgs/" + orgId + "/projects")).header("Authorization", "Bearer " + accessToken).build());if (!projects.path("ok").asBoolean()) throw new IllegalStateException("Listing projects failed: " + projects.path("error").asText());System.out.println("Your organization has " + projects.path("data").size() + " project(s):");for (JsonNode project : projects.path("data")) {System.out.println(" " + project.path("name").asText() + " (" + project.path("id").asText() + ")");}// 3. Call the API with it: list the organization's projects.using var request = new HttpRequestMessage(HttpMethod.Get, $"{workspaceUrl}/orgs/{orgId}/projects");request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", data.AccessToken);var response = await http.SendAsync(request);var projects = await response.Content.ReadFromJsonAsync<Envelope<List<Project>>>();if (projects is not { Ok: true, Data: { } list }) throw new InvalidOperationException($"Listing projects failed: {projects?.Error}");Console.WriteLine($"Your organization has {list.Count} project(s):");foreach (var project in list) Console.WriteLine($" {project.Name} ({project.Id})");// 3. Call the API with it: list the organization's projects.req, _ = http.NewRequest(http.MethodGet, workspaceURL+"/orgs/"+claims.OrgID+"/projects", nil)req.Header.Set("Authorization", "Bearer "+token.Data.AccessToken)var projects envelope[[]project]send(req, &projects)if !projects.OK {fail("Listing projects failed: " + projects.Error)}fmt.Printf("Your organization has %d project(s):\n", len(projects.Data))for _, p := range projects.Data {fmt.Printf(" %s (%s)\n", p.Name, p.ID)}Terminal window # 3. Call the API with it: list the organization's projects.projects=$(curl -sS "$WORKSPACE_URL/orgs/$org_id/projects" -H "Authorization: Bearer $access_token")if [ "$(jq -r .ok <<<"$projects")" != true ]; thenecho "Listing projects failed: $(jq -r .error <<<"$projects")" >&2exit 1fiecho "Your organization has $(jq '.data | length' <<<"$projects") project(s):"jq -r '.data[] | " \(.name) (\(.id))"' <<<"$projects"
Run it in your browser
Section titled “Run it in your browser”You can make the same first call without leaving this page. Paste your key and run the TypeScript Quickstart: your browser swaps the key and lists your projects, with the same code our CI runs. Try it in your browser explains what the page does with your key.
Try it with your organization
How this worksConnect once and run this recipe live against the By Dawn API.
Your key stays in this tab and goes only to By Dawn. Use a read-only key in a test organization.
quickstart.tsThe same code CI runs, in your browserquickstart.ts…- Work with tickets: list them, create one, update it and comment on it.
- Authentication explains tokens, scopes and what a key can’t do.
- The API reference lists every operation, and you call each one the way this
script calls
GET /projects.