# Quickstart

> Make your first By Dawn API call in about five minutes, in TypeScript, Python, Java, C#, Go or cURL.

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](#run-it-in-your-browser).

### 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](https://docs.bydawn.com/api-keys/) walks through it.

### Set up your language

Each recipe is one file that uses only your language's standard library, so there's almost
nothing to install.

  **TypeScript**

  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:

  ```sh
  npx openapi-typescript https://docs.bydawn.com/openapi/public.openapi.yaml -o saw-api.d.ts
  ```

  Regenerate them when the [changelog](https://docs.bydawn.com/changelog/) records a change. `npx tsx` and `bun` run
  the file too.

  **Python**

  Python 3.9 or later. Nothing to install: the script uses `urllib` and `json`.

  **Java**

  JDK 17 or later and [JBang](https://www.jbang.dev/download/), which runs a single `.java` file
  and fetches its one dependency, Jackson, for JSON. In a Maven or Gradle project, add
  `com.fasterxml.jackson.core:jackson-databind` and drop the `//` lines at the top.

  **C#**

  The [.NET 10 SDK](https://dotnet.microsoft.com/download), which runs a single `.cs` file. No
  NuGet packages. The `#:property PublishAot=false` line turns off Native AOT, which `dotnet run`
  uses by default and which can't read JSON this way. In a console project, leave that line out
  and paste the rest into `Program.cs`.

  **Go**

  Go 1.21 or later. Standard library only, so no module setup.

  **cURL**

  `bash`, `curl` 7.66 or later, and [`jq`](https://jqlang.org/download/) 1.6 or later, which
  builds the request bodies and reads the answers.

Save the recipe under its file name, set `SAW_API_KEY` to your key, and run the command that
comes with it.

### 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`.

### 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`.

### Call the API

Now send the token in an `Authorization: Bearer …` header and ask for your projects. Every answer
has the same shape: `ok` says whether it worked, and the projects are in `data`. That shape is
called the [envelope](https://docs.bydawn.com/developers/responses-and-errors/), 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.

The whole recipe:

**TypeScript** (`quickstart.ts`):

```ts
// Quickstart: exchange an org API key for an access token, then make a first call.
// Built-in `fetch` only. The types come from `saw-api.d.ts`, which you generate from the public
// spec (see the Quickstart); without TypeScript, drop the type annotations.
//
//   SAW_API_KEY=saw_live_… node quickstart.ts
import type { components } from './saw-api';

type Schemas = components['schemas'];

const apiKey = process.env.SAW_API_KEY;
if (!apiKey) throw new Error('Set SAW_API_KEY in the environment.');

const ACCOUNTS_URL = process.env.SAW_ACCOUNTS_URL ?? 'https://accounts.dev.sawrun.com/api/v1';
const WORKSPACE_URL = process.env.SAW_WORKSPACE_URL ?? 'https://workspace.dev.sawrun.com/api/v1';

// 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.`);

// 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}`);

// 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})`);
```

Run it:

```sh
SAW_API_KEY=saw_live_… node --experimental-strip-types quickstart.ts
```

**Python** (`quickstart.py`):

```python
# Quickstart: exchange an org API key for an access token, then make a first call.
# Standard library only, Python 3.9 or later.
#
#   SAW_API_KEY=saw_live_… python3 quickstart.py
import base64
import json
import os
import sys
import urllib.error
import urllib.request

api_key = os.environ.get("SAW_API_KEY") or sys.exit("Set SAW_API_KEY in the environment.")

ACCOUNTS_URL = os.environ.get("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1")
WORKSPACE_URL = os.environ.get("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1")

def call(url, body=None, token=None):
    """Send a request and return its envelope: {"ok": true, "data": …} or {"ok": false, "error": …}."""
    headers = {"Content-Type": "application/json"} if body is not None else {}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    data = json.dumps(body).encode() if body is not None else None
    try:
        with urllib.request.urlopen(urllib.request.Request(url, data=data, headers=headers)) as response:
            return json.load(response)
    except urllib.error.HTTPError as failure:
        # A failed call answers with the same envelope, carrying a stable `error` slug.
        return json.load(failure)

# 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.")

# 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}")

# 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']})")
```

Run it:

```sh
SAW_API_KEY=saw_live_… python3 quickstart.py
```

**Java** (`Quickstart.java`):

```java
// Quickstart: exchange an org API key for an access token, then make a first call.
// JBang runs this single file and fetches its one dependency, Jackson, for JSON. In a Maven
// or Gradle project, add com.fasterxml.jackson.core:jackson-databind and drop the `//`
// directives below.
//
//   SAW_API_KEY=saw_live_… jbang Quickstart.java
//JAVA 17+
//COMPILE_OPTIONS -encoding UTF-8
//DEPS com.fasterxml.jackson.core:jackson-databind:2.19.0

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.Map;

public class Quickstart {
  static final HttpClient http = HttpClient.newHttpClient();
  static final ObjectMapper json = new ObjectMapper();

  public static void main(String[] args) throws Exception {
    String apiKey = System.getenv("SAW_API_KEY");
    if (apiKey == null || apiKey.isEmpty()) throw new IllegalStateException("Set SAW_API_KEY in the environment.");
    String accountsUrl = env("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1");
    String workspaceUrl = env("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1");

    // 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.");

    // 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);

    // 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() + ")");
    }
  }

  /** Sends a request and returns its envelope: {"ok": true, "data": …} or {"ok": false, "error": …}. */
  static JsonNode send(HttpRequest request) throws Exception {
    // A failed call answers with the same envelope, so the status code is not checked here.
    return json.readTree(http.send(request, HttpResponse.BodyHandlers.ofString()).body());
  }

  static String env(String name, String fallback) {
    String value = System.getenv(name);
    return value == null || value.isEmpty() ? fallback : value;
  }
}
```

Run it:

```sh
SAW_API_KEY=saw_live_… jbang Quickstart.java
```

**C#** (`quickstart.cs`):

```csharp
// Quickstart: exchange an org API key for an access token, then make a first call.
// Plain HttpClient and System.Net.Http.Json, both part of .NET: no NuGet packages.
//
//   SAW_API_KEY=saw_live_… dotnet run quickstart.cs
//
// `dotnet run <file>.cs` builds for Native AOT by default, which turns off reflection-based JSON;
// the `#:` line below turns AOT off. In a console project, leave that line out and paste the rest
// into Program.cs.
#:property PublishAot=false
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var apiKey = Environment.GetEnvironmentVariable("SAW_API_KEY");
if (string.IsNullOrEmpty(apiKey)) throw new InvalidOperationException("Set SAW_API_KEY in the environment.");
var accountsUrl = Environment.GetEnvironmentVariable("SAW_ACCOUNTS_URL") ?? "https://accounts.dev.sawrun.com/api/v1";
var workspaceUrl = Environment.GetEnvironmentVariable("SAW_WORKSPACE_URL") ?? "https://workspace.dev.sawrun.com/api/v1";

using var http = new HttpClient();

// 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.");

// 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}");

// 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})");

// Helpers: the functions and types the steps above use.
record Envelope<T>(bool Ok, T? Data, string? Error, string? Message);
record AccessTokenData(string AccessToken, int ExpiresInSeconds);
record Project(string Id, string Name);
```

Run it:

```sh
SAW_API_KEY=saw_live_… dotnet run quickstart.cs
```

**Go** (`quickstart.go`):

```go
// Quickstart: exchange an org API key for an access token, then make a first call.
// Standard library only.
//
//	SAW_API_KEY=saw_live_… go run quickstart.go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)

// envelope is the shape of every SAW API response: {"ok": true, "data": …} or
// {"ok": false, "error": "<slug>"}.
type envelope[T any] struct {
	OK    bool   `json:"ok"`
	Data  T      `json:"data"`
	Error string `json:"error"`
}

type accessToken struct {
	AccessToken      string `json:"accessToken"`
	ExpiresInSeconds int    `json:"expiresInSeconds"`
}

type project struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func main() {
	apiKey := os.Getenv("SAW_API_KEY")
	if apiKey == "" {
		fail("Set SAW_API_KEY in the environment.")
	}
	accountsURL := env("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1")
	workspaceURL := env("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1")

	// 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)

	// 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)

	// 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)
	}
}

// send makes the request and decodes its envelope into out. A failed call answers with the
// same envelope, so the status code is not checked here: the caller branches on `ok`.
func send(req *http.Request, out any) {
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		fail(err.Error())
	}
	defer res.Body.Close()
	if err := json.NewDecoder(res.Body).Decode(out); err != nil {
		fail(fmt.Sprintf("%s %s answered %s with no JSON envelope", req.Method, req.URL, res.Status))
	}
}

func env(name, fallback string) string {
	if value := os.Getenv(name); value != "" {
		return value
	}
	return fallback
}

func fail(message string) {
	fmt.Fprintln(os.Stderr, message)
	os.Exit(1)
}
```

Run it:

```sh
SAW_API_KEY=saw_live_… go run quickstart.go
```

**cURL** (`quickstart.sh`):

```bash
#!/usr/bin/env bash
# Quickstart: exchange an org API key for an access token, then make a first call.
# Needs curl and jq.
#
#   SAW_API_KEY=saw_live_… bash quickstart.sh
set -euo pipefail

: "${SAW_API_KEY:?Set SAW_API_KEY in the environment.}"
ACCOUNTS_URL="${SAW_ACCOUNTS_URL:-https://accounts.dev.sawrun.com/api/v1}"
WORKSPACE_URL="${SAW_WORKSPACE_URL:-https://workspace.dev.sawrun.com/api/v1}"

# 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 ]; then
  echo "Token exchange failed: $(jq -r .error <<<"$token")" >&2
  exit 1
fi
access_token=$(jq -r .data.accessToken <<<"$token")
echo "Access token issued, valid for $(jq -r .data.expiresInSeconds <<<"$token") seconds."

# 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"

# 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 ]; then
  echo "Listing projects failed: $(jq -r .error <<<"$projects")" >&2
  exit 1
fi

echo "Your organization has $(jq '.data | length' <<<"$projects") project(s):"
jq -r '.data[] | "  \(.name)  (\(.id))"' <<<"$projects"
```

Run it:

```sh
SAW_API_KEY=saw_live_… bash quickstart.sh
```

What you'll see (from our test organization; your names and IDs will differ):

```text
Access token issued, valid for 900 seconds.
Organization ID: org-7f3c9a1e
Your organization has 2 project(s):
  Web app  (project-1)
  Mobile  (project-2)
```

## 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](https://docs.bydawn.com/developers/try-it/) explains what the page does with your key.

> A runner for this recipe, with your own credential, is on the [web version of this page](https://docs.bydawn.com/developers/quickstart/).

## Next

- Work with tickets: [list them](https://docs.bydawn.com/developers/recipes/list-tickets/), [create one](https://docs.bydawn.com/developers/recipes/create-ticket/),
  [update it](https://docs.bydawn.com/developers/recipes/update-ticket/) and [comment on it](https://docs.bydawn.com/developers/recipes/comments/).
- [Authentication](https://docs.bydawn.com/developers/authentication/) explains tokens, scopes and what a key can't do.
- The [API reference](https://docs.bydawn.com/reference/) lists every operation, and you call each one the way this
  script calls `GET /projects`.

Source: https://docs.bydawn.com/developers/quickstart/
