Tracy at Qode API for Zapier
The sign-in and the API calls the Tracy at Qode app in Zapier makes, and the call that creates an interview: what each one is called with and what it answers. Looking for the steps to connect it? Read the Zapier guide.
Base URL and conventions
Every address on this page starts with the address of this site:
https://tracyinterview.ai- Calls to the API and their answers are JSON. The token call is the one exception: it sends a form.
- The Zapier app adds the header
X-Tracy-Client: zapierto every request, so a host can see which of their calls came from Zapier. - Times are ISO 8601 in UTC, such as
2026-10-13T09:30:00.000Z.
Sign in
Tracy at Qode signs a host in with the OAuth 2.0 authorization code flow and PKCE. Zapier opens the authorize address in a window, the host signs in to Tracy at Qode and clicks Allow, and Zapier trades the code it is sent back for tokens at the token address.
- PKCE is required, and the one method accepted is
S256. - The Zapier app is a client that Tracy at Qode registers by hand, with a client ID, a client secret and the redirect address Zapier shows for the app. It sends the ID and the secret to the token address.
- An access token lasts 1 hour (
expires_inis 3600). - A refresh token lasts 60 days and rotates: every refresh answers with a new refresh token, which starts its own 60 days, and the one that was sent cannot be used again. Sending a used refresh token a second time ends the connection, and the host signs in again.
- The one scope is
tracy. Every connection gets it, so the app asks for none.
Authorize
Send the host's browser here. Tracy at Qode asks them to sign in if they are signed out, shows what Zapier asks for, and sends the browser on to the redirect address when they choose.
GET https://tracyinterview.ai/oauth/authorizeQuery parameters
client_id: the client ID Tracy at Qode gave Zapier.redirect_uri: the callback address Zapier shows for the app. It must match one registered for the client exactly.response_type: always code.state: a value Zapier chooses. It comes back unchanged on the redirect.code_challenge: the unpadded base64url SHA-256 of the code verifier.code_challenge_method: always S256.
Answer: a redirect
There is no JSON answer. The browser is sent to redirect_uri with code, state and iss (this site's address) added to it. The code works once and for 10 minutes. If the host chooses Deny, or the request is refused after the redirect address is known, the redirect carries error and error_description instead of a code.
REDIRECT_URI?code=CODE&state=STATE&iss=https://tracyinterview.aiGet tokens
Trades the code from the redirect for an access token and a refresh token.
POST https://tracyinterview.ai/oauth/tokenHeaders
Content-Type: application/x-www-form-urlencoded. A JSON body is refused.
Form fields
grant_type: authorization_code.code: the code from the redirect.redirect_uri: the same address the authorize request used.code_verifier: the secret the code_challenge was made from: 43 to 128 characters of A-Z, a-z, 0-9, -, ., _ and ~.client_id: the client ID.client_secret: the client secret.
Answer: 200
The access token goes in the Authorization header of every call below. Keep the refresh token: it is the only way to get the next access token without a new sign-in.
{
"access_token": "the access token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "the refresh token",
"scope": "tracy"
}Refresh the access token
The same address as above, with a different grant. Zapier calls it when an access token has expired, and runs the step again with the new one.
POST https://tracyinterview.ai/oauth/tokenHeaders
Content-Type: application/x-www-form-urlencoded.
Form fields
grant_type: refresh_token.refresh_token: the newest refresh token.client_id: the client ID.client_secret: the client secret.
Answer: 200
The same shape as Get tokens. Store the new refresh_token in place of the one you sent.
{
"access_token": "the access token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "the refresh token",
"scope": "tracy"
}Calls with the access token
The calls below act for the host who signed in, and only on their own account. Send the access token in a header on each one:
Authorization: Bearer followed by the access token.X-Tracy-Client: zapier, as in the conventions above.
An expired, revoked or missing token is answered with 401 and the error unauthorized: refresh the access token and send the call again.
List events
What the Zapier triggers poll: the account's newest interview events, newest first. Each trigger asks for one type.
GET https://tracyinterview.ai/api/eventsQuery parameters
type: optional. One ofinvite.opened,interview.started,interview.completed,interview.incompleteandresult.ready. Left out, events of every type are listed.limit: optional. A whole number from 1 to 100, 50 when left out. The triggers ask for 100.
Answer: 200
An object with an events list, which is empty when there is nothing yet. Each event has a stable id. resultUrl is null for events with no results page, and completionState is set only on result.ready. A type or limit that is not allowed is answered with 400 and the error invalid_query.
{
"events": [
{
"id": "evSample0000000001",
"type": "interview.completed",
"label": "Interview completed",
"createdAt": "2026-10-13T09:30:00.000Z",
"interview": {
"id": "inSample0000000001",
"title": "Customer discovery call"
},
"participant": {
"id": "paSample0000000001",
"name": "Ada Lovelace",
"email": "ada@example.com",
"externalRef": "crm-1042"
},
"resultId": "reSample0000000001",
"interviewUrl": "https://tracyinterview.ai/interviews/inSample0000000001",
"resultUrl": "https://tracyinterview.ai/participants/paSample0000000001/results",
"completionState": null
}
]
}List interviews
Fills the interview dropdown of the Invite Participant action: the account's interviews, most recently updated first. It takes no parameters.
GET https://tracyinterview.ai/api/interviewsAnswer: 200
A list of interviews, empty when the account has none. The action uses id and title.
[
{
"id": "inSample0000000001",
"title": "Customer discovery call",
"purpose": "Learn how customers pick a supplier.",
"templateName": "Customer discovery",
"mode": "video",
"language": "en",
"state": "Live",
"participants": 12,
"completed": 7,
"openLinkOn": false,
"questionCount": 8,
"notSent": false,
"openLink": {
"enabled": false,
"joins": 0,
"joinCap": 100,
"state": "Off",
"expiresAt": "2026-10-27T09:30:00.000Z"
},
"updatedAt": "2026-10-13T09:30:00.000Z"
}
]Create an interview
Makes an interview from who the host is interviewing and what they want to find out. Tracy drafts its title, questions and result fields, unless the call brings a setup of its own.
POST https://tracyinterview.ai/api/interviewsHeaders
Content-Type: application/json.Idempotency-Key: optional. A call repeated with the same key is not run twice: the first answer is kept for 24 hours and sent again.
JSON body
participants: required. Who is being interviewed, up to 500 characters.goal: required. What the host wants to find out, up to 2000 characters.context: optional. Anything else Tracy should know, such as a job description, up to 20000 characters.ref: optional. Your own reference for the interview, up to 200 characters. It comes back as externalRef.mode:videooraudio: how the participant joins. Left out: video.language: the language the interview is held in:en,de,es,fr,id,ja,ko,nlorvi. Left out: en.cheatingOn: optional, true or false: whether the interview starts with its integrity signals on. Left out: true. Only a video interview is checked, whatever this says. Anything that is not true or false is answered with 400 andinvalid_request. The host can change it later.setup: optional. A setup the host has already reviewed:title,questionsandresultFields, and optionallytemplate,introandoutro. The interview is made from it as it is, and Tracy does not draft one.
Example body
{
"participants": "Candidates for a Customer Support Lead role",
"goal": "Whether they have led a small team and stay calm with upset customers",
"ref": "ats-42",
"mode": "video",
"language": "en",
"cheatingOn": false
}Answer: 201
The new interview, which starts at version 1. The example shows the fields named above; the answer also carries the questions, the result fields and the open link.
{
"id": "inSample0000000002",
"title": "Customer Support Lead screen",
"purpose": {
"participants": "Candidates for a Customer Support Lead role",
"goal": "Whether they have led a small team and stay calm with upset customers",
"context": null
},
"version": 1,
"mode": "video",
"language": "en",
"emailsOn": true,
"cheatingOn": false,
"externalRef": "ats-42"
}Invite a participant
Invites one person to an interview and has Tracy at Qode email them their personal link. Replace :id with the id of an interview from the list above.
POST https://tracyinterview.ai/api/interviews/:id/participantsHeaders
Content-Type: application/json.Idempotency-Key: optional, and the Zapier app does not send one. A call repeated with the same key is not run twice: the first answer is kept for 24 hours and sent again.
JSON body
people: required. One email address, orName <email>. Without a name, Tracy at Qode uses the start of the address. Further lines invite further people, up to 200 in a call; the Zapier app sends one.ref: optional. Your own reference for the person, up to 200 characters. It is kept only when the call invites one person. It comes back as externalRef on the person, and on every event for them.
Example body
{
"people": "Ada Lovelace <ada@example.com>",
"ref": "crm-1042"
}Answer: 201
participants has the person that was invited. When the interview is paused or the credits are used up, held is true and heldReason says which (paused or credits): the person is saved, and no email has gone out yet. emailed is true when the invite email was accepted for sending. An interview that is not yours, or does not exist, is answered with 404 and interview_not_found; one that has not been given a title yet, with 409 and title_not_set.
{
"participants": [
{
"id": "paSample0000000001",
"name": "Ada Lovelace",
"email": "ada@example.com",
"via": "invite",
"link": "https://tracyinterview.ai/i/customer-discovery-k7m2xq?candidateId=paSample0000000001",
"status": "invited",
"statusLabel": "Invited",
"paused": false,
"expiredWhilePaused": false,
"completionState": null,
"sessions": 0,
"emailStatus": "sent",
"expiresAt": "2026-10-27T09:30:00.000Z",
"version": 1,
"externalRef": "crm-1042",
"source": "zapier",
"sessionMinutes": null
}
],
"held": false,
"emailed": true
}Read the host's name
The one thing the token can tell about who signed in. The Zapier app reads it after connecting and shows it as the connection's name.
GET https://tracyinterview.ai/api/me/nameAnswer: 200
name is null when the host has not set one.
{
"name": "Ada Lovelace"
}Errors
A call to the API that fails is answered with a status of 400 or more and this JSON, where code is stable and safe to test for, and message is a sentence for the person at the keyboard:
{
"error": {
"code": "unauthorized",
"message": "Sign in, or send a valid API key, to use this."
}
}400: the call is malformed:invalid_query,invalid_json,invalid_requestorinvalid_people.401:unauthorized: the access token is missing, expired or revoked.404:interview_not_found.409:title_not_set.500:internal_error: something went wrong on the Tracy at Qode side. Try the call again.
The sign-in addresses are the exception. They answer in the shape OAuth 2.0 defines (RFC 6749), with an error status, so a client that already speaks OAuth can read it. invalid_grant means the code or the refresh token is expired, already used or revoked, and only a new sign-in helps.
{
"error": "invalid_grant",
"error_description": "The grant is invalid, expired or already used."
}