Comment on a ticket
Use this to keep a ticket’s discussion in one place: post a build result or a customer reply to the ticket, or copy its comments somewhere your team already reads. By the end you’ll have posted a comment and read back the ticket’s whole thread.
Before you start: posting needs a Board — read & write key (see
Scopes), and you need the ticket’s id, for example from
Create a ticket. Pass the ID as the recipe’s argument.
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"Post a comment
Section titled “Post a comment”POST /tickets/{ticketId}/commentswith abodyposts the comment. It shows your key as its author, so people can tell it came from your integration rather than a colleague.// 2. Post a comment on the ticket you named.const ticketId = process.argv[2];if (!ticketId) throw new Error('Usage: node comment.ts <ticketId>');const url = `${api}/tickets/${encodeURIComponent(ticketId)}/comments`;const create: Schemas['CommentCreate'] = { body: 'Posted by the comment recipe through the SAW API.' };const comment = await call<Schemas['Comment']>(url, { method: 'POST', body: create, token });console.log(`Posted comment ${comment.id}.`);# 2. Post a comment on the ticket you named.if len(sys.argv) < 2:sys.exit("Usage: python3 comment.py <ticketId>")url = f"{api}/tickets/{urllib.parse.quote(sys.argv[1], safe='')}/comments"comment = call(url, "POST", {"body": "Posted by the comment recipe through the SAW API."}, token)print(f"Posted comment {comment['id']}.")// 2. Post a comment on the ticket you named.String url = api + "/tickets/" + URLEncoder.encode(args[0], StandardCharsets.UTF_8) + "/comments";TicketComment comment = call("POST", url, token, Map.of("body", "Posted by the comment recipe through the SAW API."),new TypeReference<Envelope<TicketComment>>() {});System.out.println("Posted comment " + comment.id() + ".");// 2. Post a comment on the ticket you named.var comment = await ReadAsync<Comment>(await http.PostAsJsonAsync($"tickets/{ticketId}/comments",new { body = "Posted by the comment recipe through the SAW API." }));Console.WriteLine($"Posted comment {comment.Id}.");// 2. Post a comment on the ticket you named.if len(os.Args) < 2 {fail("Usage: go run comment.go <ticketId>")}commentsURL := api + "/tickets/" + url.PathEscape(os.Args[1]) + "/comments"posted, err := call[comment](http.MethodPost, commentsURL, token, map[string]string{"body": "Posted by the comment recipe through the SAW API.",})check(err)fmt.Printf("Posted comment %s.\n", posted.ID)Terminal window # 2. Post a comment on the ticket you named.url="$api/tickets/$(jq -rn --arg id "$ticket_id" '$id | @uri')/comments"comment=$(jq -n '{body: "Posted by the comment recipe through the SAW API."}' |curl -sS -X POST "$url" -H "Authorization: Bearer $token" -H 'Content-Type: application/json' --data @- |jq "$DATA")echo "Posted comment $(jq -r .id <<<"$comment")."Read the comments back
Section titled “Read the comments back”GETon the same path returns the ticket’s comments, oldest first, so they read like a conversation. The recipe checks that its own comment is there.// 3. Read the ticket's comments back, oldest first.const comments = await call<Schemas['Comment'][]>(url, { token });console.log(`The ticket has ${comments.length} comment(s):`);for (const c of comments) console.log(` ${c.createdAt} ${c.body}`);if (!comments.some((c) => c.id === comment.id)) throw new Error('The new comment is missing from the list.');# 3. Read the ticket's comments back, oldest first.comments = call(url, token=token)print(f"The ticket has {len(comments)} comment(s):")for c in comments:print(f" {c['createdAt']} {c['body']}")if not any(c["id"] == comment["id"] for c in comments):sys.exit("The new comment is missing from the list.")// 3. Read the ticket's comments back, oldest first.List<TicketComment> comments = call("GET", url, token, null, new TypeReference<Envelope<List<TicketComment>>>() {});System.out.println("The ticket has " + comments.size() + " comment(s):");for (TicketComment c : comments) System.out.println(" " + c.createdAt() + " " + c.body());if (comments.stream().noneMatch(c -> c.id().equals(comment.id()))) throw new IllegalStateException("The new comment is missing from the list.");// 3. Read the ticket's comments back, oldest first.var comments = await ReadAsync<List<Comment>>(await http.GetAsync($"tickets/{ticketId}/comments"));Console.WriteLine($"The ticket has {comments.Count} comment(s):");// `createdAt` is printed as the API sends it, an ISO 8601 string.foreach (var c in comments) Console.WriteLine($" {c.CreatedAt} {c.Body}");if (!comments.Any(c => c.Id == comment.Id)) throw new InvalidOperationException("The new comment is missing from the list.");// 3. Read the ticket's comments back, oldest first.comments, err := call[[]comment](http.MethodGet, commentsURL, token, nil)check(err)fmt.Printf("The ticket has %d comment(s):\n", len(comments))found := falsefor _, c := range comments {fmt.Printf(" %s %s\n", c.CreatedAt, c.Body)found = found || c.ID == posted.ID}if !found {fail("The new comment is missing from the list.")}Terminal window # 3. Read the ticket's comments back, oldest first.comments=$(curl -sS "$url" -H "Authorization: Bearer $token" | jq "$DATA")echo "The ticket has $(jq length <<<"$comments") comment(s):"jq -r '.[] | " \(.createdAt) \(.body)"' <<<"$comments"if [ "$(jq --arg id "$(jq -r .id <<<"$comment")" 'any(.id == $id)' <<<"$comments")" != true ]; thenecho 'The new comment is missing from the list.' >&2exit 1fi
Run it in your browser
Section titled “Run it in your browser”Run the TypeScript recipe from this page. It posts a comment on the ticket, then lists the ticket’s comments. A ticket you created here is filled in for you. See Try it in your browser.
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.
comment.tsThe same code CI runs, in your browsercomment.ts…Things to know
Section titled “Things to know”- Replies. Set
parentCommentIdto reply to a comment. Replies go one level deep. - Only what’s new. To pick up new comments as they come, pass the newest
createdAtyou’ve seen assince(ISO 8601). You get only the comments created after it. - Your own comments. A key can edit (
PATCH) and delete (DELETE …/comments/{commentId}) the comments it posted, and no one else’s.