Create a ticket
Use this to file work in By Dawn from another tool: an alert that fires, a form someone fills in, an email to a support address. By the end you’ll have a ticket in a project’s backlog, and its ID to update or comment on it later.
Before you start: you need a Board — read & write key (see
Scopes). To pick the project, set SAW_PROJECT_ID to its ID.
Without it, the recipe uses your organization’s first project.
Read down the steps. The code on the right follows along and highlights the lines each one explains.
Swap your key for an access token
Section titled “Swap your key for an access token”Every recipe starts this way, as the Quickstart explains.
// 1. Exchange the API key for a short-lived access token. Every call after this goes to your// organization's URL with the token as a Bearer header.const exchange: Schemas['ApiKeyExchange'] = { apiKey };const { accessToken: token } = await call<Schemas['AccessTokenData']['data']>(`${ACCOUNTS_URL}/auth/token/api-key`, { method: 'POST', body: exchange });const api = `${WORKSPACE_URL}/orgs/${orgIdOf(token)}`;# 1. Exchange the API key for a short-lived access token. Every call after this goes to your# organization's URL with the token as a Bearer header.token = call(f"{ACCOUNTS_URL}/auth/token/api-key", "POST", {"apiKey": API_KEY})["accessToken"]api = f"{WORKSPACE_URL}/orgs/{org_id_of(token)}"// 1. Exchange the API key for a short-lived access token. Every call after this goes to your// organization's URL with the token as a Bearer header.String token = call("POST", accountsUrl + "/auth/token/api-key", null, Map.of("apiKey", apiKey), new TypeReference<Envelope<AccessToken>>() {}).accessToken();String api = workspaceUrl + "/orgs/" + orgIdOf(token);// 1. Exchange the API key for a short-lived access token.using var accounts = new HttpClient();var token = await ReadAsync<AccessTokenData>(await accounts.PostAsJsonAsync($"{accountsUrl}/auth/token/api-key", new { apiKey }));// Every call goes to your organization's URL with the token as a Bearer header.using var http = new HttpClient { BaseAddress = new Uri($"{workspaceUrl}/orgs/{OrgIdOf(token.AccessToken)}/") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);// 1. Exchange the API key for a short-lived access token. Every call after this goes to your// organization's URL with the token as a Bearer header.exchange, err := call[accessToken](http.MethodPost, accountsURL+"/auth/token/api-key", "", map[string]string{"apiKey": apiKey})check(err)token := exchange.AccessTokenapi := workspaceURL + "/orgs/" + orgIDOf(token)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 @- |jq -r "$DATA | .accessToken")# The token is a JWT; its `org_id` claim is the organization the key belongs to. Every call# after this goes to that organization's URL with the token as a Bearer header.org_id=$(jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | . + ("=" * ((4 - length % 4) % 4))| @base64d | fromjson | .org_id' <<<"$token")api="$WORKSPACE_URL/orgs/$org_id"Choose the project
Section titled “Choose the project”Tickets always belong to a project, so the recipe needs one. In your own integration you’ll usually know it already, for example one project per team or per alert source.
GET /projectslists them if you need to look one up.// 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.const projectId = process.env.SAW_PROJECT_ID ?? (await call<Schemas['Project'][]>(`${api}/projects`, { token }))[0]?.id;if (!projectId) throw new Error('This organization has no projects yet.');# 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.project_id = os.environ.get("SAW_PROJECT_ID")if not project_id:projects = call(f"{api}/projects", token=token)project_id = projects[0]["id"] if projects else sys.exit("This organization has no projects yet.")// 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.String projectId = System.getenv("SAW_PROJECT_ID");if (projectId == null || projectId.isEmpty()) {List<Project> projects = call("GET", api + "/projects", token, null, new TypeReference<Envelope<List<Project>>>() {});if (projects.isEmpty()) throw new IllegalStateException("This organization has no projects yet.");projectId = projects.get(0).id();}// 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.var projectId = Environment.GetEnvironmentVariable("SAW_PROJECT_ID")?? (await ReadAsync<List<Project>>(await http.GetAsync("projects"))).FirstOrDefault()?.Id?? throw new InvalidOperationException("This organization has no projects yet.");// 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.projectID := os.Getenv("SAW_PROJECT_ID")if projectID == "" {projects, err := call[[]project](http.MethodGet, api+"/projects", token, nil)check(err)if len(projects) == 0 {fail("This organization has no projects yet.")}projectID = projects[0].ID}Terminal window # 2. Choose the project: SAW_PROJECT_ID if you set it, or else your organization's first one.project_id=${SAW_PROJECT_ID:-}if [ -z "$project_id" ]; thenproject_id=$(curl -sS "$api/projects" -H "Authorization: Bearer $token" | jq -r "$DATA | .[0].id // empty")fiif [ -z "$project_id" ]; thenecho 'This organization has no projects yet.' >&2exit 1fiCreate the ticket
Section titled “Create the ticket”POST /ticketsneeds only aprojectIdand atitle. Leave out the column and the ticket lands in the project’s backlog, where people triage new work.The answer is the new ticket. The recipe prints its identifier, such as
WEB-5, for people, then itsidfor code: that’s what you pass to update or comment on it.// 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.const create: Schemas['TicketCreate'] = {projectId,title: 'Created by the create-ticket recipe',description: 'This ticket was created through the SAW API.',priority: 'LOW',};const ticket = await call<Schemas['Ticket']>(`${api}/tickets`, { method: 'POST', body: create, token });console.log(`Created ${ticket.identifier}: ${ticket.title}`);// The id is what every other ticket operation takes, e.g. `node update-ticket.ts <id>`.console.log(ticket.id);# 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.ticket = call(f"{api}/tickets","POST",{"projectId": project_id,"title": "Created by the create-ticket recipe","description": "This ticket was created through the SAW API.","priority": "LOW",},token,)print(f"Created {ticket['identifier']}: {ticket['title']}")# The id is what every other ticket operation takes, e.g. `python3 update_ticket.py <id>`.print(ticket["id"])// 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.Ticket ticket = call("POST", api + "/tickets", token, Map.of("projectId", projectId,"title", "Created by the create-ticket recipe","description", "This ticket was created through the SAW API.","priority", "LOW"), new TypeReference<Envelope<Ticket>>() {});System.out.println("Created " + ticket.identifier() + ": " + ticket.title());// The id is what every other ticket operation takes, e.g. `jbang UpdateTicket.java <id>`.System.out.println(ticket.id());// 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.var ticket = await ReadAsync<Ticket>(await http.PostAsJsonAsync("tickets", new{projectId,title = "Created by the create-ticket recipe",description = "This ticket was created through the SAW API.",priority = "LOW",}));Console.WriteLine($"Created {ticket.Identifier}: {ticket.Title}");// The id is what every other ticket operation takes, e.g. `dotnet run update-ticket.cs -- <id>`.Console.WriteLine(ticket.Id);// 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.created, err := call[ticket](http.MethodPost, api+"/tickets", token, map[string]string{"projectId": projectID,"title": "Created by the create-ticket recipe","description": "This ticket was created through the SAW API.","priority": "LOW",})check(err)fmt.Printf("Created %s: %s\n", created.Identifier, created.Title)// The id is what every other ticket operation takes, e.g. `go run update_ticket.go <id>`.fmt.Println(created.ID)Terminal window # 3. Create the ticket. Only projectId and title are required, and it lands in the backlog column.ticket=$(jq -n --arg projectId "$project_id" '{projectId: $projectId,title: "Created by the create-ticket recipe",description: "This ticket was created through the SAW API.",priority: "LOW"}' |curl -sS -X POST "$api/tickets" -H "Authorization: Bearer $token" -H 'Content-Type: application/json' --data @- |jq "$DATA")jq -r '"Created \(.identifier): \(.title)"' <<<"$ticket"# The id is what every other ticket operation takes, e.g. `bash update-ticket.sh <id>`.jq -r .id <<<"$ticket"
Run it in your browser
Section titled “Run it in your browser”Run the TypeScript recipe from this page. It creates a real ticket in your organization, so use a test organization. The new ticket’s ID then fills in the update and comment recipes for you.
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.
create-ticket.tsThe same code CI runs, in your browsercreate-ticket.ts…Other fields you can set
Section titled “Other fields you can set”Anything you can set now, you can also change later:
- What it is:
description, andpriority(NO_PRIORITY,URGENT,HIGH,MEDIUMorLOW). - Where it sits:
statusId, a column fromGET /statuses?projectId=…, andlabelIds. - Who and when:
assigneeIdanddueDate. - Planning:
cycleId,epicId,milestoneIdandestimate.GET /epics?projectId=…lists the project’s epics (see Group work into epics). - Structure:
parentTicketId, which makes the ticket a sub-ticket.
An ID from another project, or a value the project doesn’t accept, gets 422 with a slug such as
invalid_status or validation_error, and message says which field. See
Responses and errors.