Onboarding
Set up Competitor Tracker & Co.: signup, completing your profile, inviting your team and claiming your application domain so the pack tracks the right site.
The first hour with Competitor Tracker & Co. is mostly housekeeping — your account, your team and the domain you call your own. Our lead detective C. T. Lucky walks the moves you'll make to get the org ready for work.
Sign up
Anyone can sign up. Walk up to the front door, enter your email and you're in. Two paths to an account:
- Open signup. Sign in with a fresh email and no invitation. We create a new organization with you as the owner and seed it with twenty-five coins.
- Team invitation. An existing organization admin invites you to their organization. Accepting attaches you to that org as an admin or member at the role the admin picked. You don't get a separate organization of your own through this path.
Both flows go through the same OAuth machinery and the same email-bound link. The only difference is what happens when the link is followed — a brand-new org of your own, or attachment to an existing one.
Emails that already exist in the user database (returning users whose previous organization was deleted) sign in the same way — they're not new signups.
Register interest
POST https://auth.competitortracker.io/request-invite
The authorization server hosts an open, unauthenticated endpoint that records a request. The marketing site uses it to capture interest. The endpoint accepts both JSON and form-encoded posts, so a plain HTML form or a JavaScript fetch both work.
curl -X POST "https://auth.competitortracker.io/request-invite" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"source": "marketing-site",
"note": "Looking for a team plan."
}'Returns 200 OK:
{ "ok": true, "email": "alice@example.com", "alreadyRequested": false }Form posts (the auth-server-rendered page or any plain <form method="post">) receive an HTML confirmation page instead of JSON. Resubmitting the same email is idempotent — the row's note and source refresh, the original request timestamp stays put, and alreadyRequested flips to true on the response.
The endpoint is rate-limited per email and per IP. Once the team mints an invitation for the address, the request is marked invited and stays on file for a couple of weeks in case the email needs resending; after that window it is cleaned up.
Complete your signup
POST /v1/users/me/complete-signup
On your first sign-in the access token resolves but your profile is empty: no first name, no last name, no signup_completed_at stamp. The signup form sends a single request to fill all of that in.
curl -X POST "$CT_API/users/me/complete-signup" \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Alice",
"lastName": "Example",
"orgName": "Vacation Tracker",
"domain": "vacationtracker.io",
"referralSource": "google_search",
"referralDetails": { "query": "competitor tracking" }
}'Returns 200 OK:
{
"id": "01HC...",
"firstName": "Alice",
"lastName": "Example",
"domain": "vacationtracker.io",
"backfilledCompetitorCount": 0,
"signupCompletedAt": "2026-05-16T12:00:00.000Z"
}What each field does
firstName,lastName— required, 1 to 120 characters each. Together they replace the legacy single-fieldnameon your user record.orgName— optional, up to 240 characters. Sets the name of your new organization. Only the owner can use it, so team-invitees fill out the same form without it and don't rename the org they joined. Leave it off to keep the default name, and rename later withPATCH /v1/orgif you change your mind.domain— optional. Only the owner of the organization can claim a domain, and only when the org's own-app domain isn't already set. Team-invitees fill out the same form without this field. When supplied the same verifier as Claim your own application domain runs.referralSource— optional. One ofgoogle_search,asked_ai,saw_video,app_store,social_media,recommendation,other. Slugs match the frontend survey.referralDetails— optional. Free-form JSON object for the survey's follow-up answers (search query, site name, who recommended). The backend stores it verbatim.
One-shot semantics
The endpoint is one-shot. A second call returns 409 SignupAlreadyComplete — use PATCH /v1/users/me to update your name after signup. The signup_completed_at timestamp is the gate.
Errors you might see
| Code | HTTP | When |
|---|---|---|
SignupAlreadyComplete | 409 | The user has already completed signup. |
OwnAppDomainEmailMismatch | 403 | The submitted domain doesn't match the verified email-domain on your account. |
PersonalEmailDomainNotAllowed | 422 | The submitted domain is a personal-mail or disposable-mail provider. |
OwnAppDomainImmutable | 409 | The organization's own-app domain is already set. Contact support if it really needs to change. |
Invite teammates
POST /v1/org/invites
Admin-only. The invitee joins your organization at the role you choose. To bring someone into the product as a separate customer (their own organization), use Refer a friend instead.
curl -X POST "$CT_API/org/invites" \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "newhire@vacationtracker.io", "role": "member" }'Returns 201 Created:
{
"id": "01HF...",
"email": "newhire@vacationtracker.io",
"role": "member",
"expiresAt": "2026-05-30T00:00:00.000Z"
}The token itself never appears in the response. It travels only in the email link, so an intercepted API response can't be turned into an account takeover.
Roles accepted in the body are admin or member. Owner is not assignable through invites — see Transfer ownership.
Resending
Calling POST /v1/org/invites again for the same email supersedes the prior invitation. The earlier token is revoked, a fresh email is sent and only the new row counts against the outstanding cap. This is the resend mechanism — there's no separate /resend endpoint.
The outstanding-invite cap
An organization can hold at most ten pending team invitations at a time. Accepted invites and revoked ones don't count against the cap. Referrals don't count either (each spawns a new org). The eleventh team invite returns 409 InviteCapReached — revoke a stale one or wait for it to be accepted.
Rate limit
Five invitation emails per recipient address per ten-minute window, and five per inviter IP per ten-minute window. The sixth attempt returns 429 LoginRateLimited. Referrals run on a separate rate-limit bucket.
Refer a friend
POST /v1/org/referrals
Send a friend a sign-in link that bootstraps their own organization on accept. Your organization is credited three coins the first time they purchase coins. Any member-tier caller can refer; no admin role required.
curl -X POST "$CT_API/org/referrals" \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "friend@example.com" }'Returns 201 Created:
{
"id": "01HF...",
"email": "friend@example.com",
"expiresAt": "2026-05-30T00:00:00.000Z"
}How the bonus works
When the friend accepts the referral, a pending bonus row is recorded linking your organization to theirs. The bonus is credited atomically the first time their account submits a purchase coin grant — three coins land in your ledger as a referral_bonus transaction. If the friend never purchases coins, no bonus is paid. The first-purchase rule applies once per referral pair, so subsequent purchases by the same friend don't double-credit you.
Rules
- The friend has to start a brand-new organization for the bonus to apply. If their email is already associated with an organization, accepting the link signs them into their existing organization and no bonus is recorded. The friend has no way to tell whether the bonus was paid; the inviter sees the invite as accepted either way.
- If the friend's email is in the user database but they have no active organization (their previous one was deleted), accepting the link bootstraps a fresh organization for them. They're treated as a returning user, not a referral — no bonus is paid.
- Disposable-mailbox addresses are blocked at send time with
422 DisposableEmailNotAllowed. Free-mail providers like gmail or outlook are fine. - Five referrals per recipient and five per inviter IP per ten-minute window; the sixth returns
429 LoginRateLimited. - Calling
POST /v1/org/referralsagain for the same email supersedes the earlier referral (same resend semantic as team invites).
List pending invites
GET /v1/org/invites
curl "$CT_API/org/invites" \
-H "Authorization: Bearer $CT_TOKEN"{
"items": [
{
"id": "01HF...",
"email": "newhire@vacationtracker.io",
"role": "member",
"createdAt": "2026-05-14T10:23:00.000Z",
"expiresAt": "2026-05-28T10:23:00.000Z"
}
]
}Newest-first. Accepted, revoked and expired rows are filtered out. The list is bounded by the outstanding-invite cap, so there's no pagination.
Revoke an invitation
DELETE /v1/org/invites/{inviteId}
Cancels a pending invitation. Admin-only. Idempotent on already-revoked rows in the sense that the second call returns 404 InviteInvalid — there's nothing left to revoke.
curl -X DELETE "$CT_API/org/invites/01HF..." \
-H "Authorization: Bearer $CT_TOKEN"Returns 200 OK with { "ok": true }. Accepted invitations can't be revoked through this endpoint by design — once a teammate has joined, you remove them through the membership endpoints.
Edit your profile after signup
GET /v1/users/me and PATCH /v1/users/me
curl "$CT_API/users/me" \
-H "Authorization: Bearer $CT_TOKEN"{
"id": "01HC...",
"email": "alice@vacationtracker.io",
"name": "Alice Example",
"createdAt": "2026-04-29T10:18:02.000Z"
}PATCH accepts { name: string | null }. Pass null to clear, omit for no-op. Use this to rename yourself after signup is complete.
Claim your own application domain
POST /v1/org/own-app-domain
This is the standalone version of the same domain claim that complete-signup runs. Useful when you skipped the domain field at signup and want to claim it later. Owner-only. Same email-domain match rules apply.
curl -X POST "$CT_API/org/own-app-domain" \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "domain": "vacationtracker.io" }'Returns 200 OK:
{
"orgId": "01HC...",
"domain": "vacationtracker.io",
"backfilledCompetitorCount": 2
}backfilledCompetitorCount reports the number of existing subscriptions in your org whose host matched the newly-claimed domain. Those subscriptions flip to the own-app rate going forward — the next weekly pass no longer charges them. The partial month you already paid is sunk: no refund, no credit. The claim is immutable, so plan accordingly.
Errors you might see
| Code | HTTP | When |
|---|---|---|
OwnAppDomainEmailMismatch | 403 | The submitted domain doesn't match the verified email-domain on your account. |
PersonalEmailDomainNotAllowed | 422 | The submitted domain is a personal-mail or disposable-mail provider. |
OwnAppDomainImmutable | 409 | The organization's own-app domain is already set. |
InsufficientRole | 403 | The caller is not the owner. |
Race against subscribe
A subscription added in the brief window between your claim attempt and the response can land with isOwnApp: false. The backfill batch reads its match set just before the write, so a row inserted after that read won't be flipped. If you spot a stray paid row right after a claim, unsubscribe and re-subscribe — the second attempt reads the now-set own-app domain at subscribe time and classifies correctly.
Organization
Manage your Competitor Tracker & Co. organization: rename it, list members, change a role, invite or remove people, leave, transfer ownership or close it.
Onboarding checklist
A checklist of starter tasks for new Competitor Tracker & Co. organizations, with the one-time coin reward for finishing four and getting your first dossier.