Browse help topics

TrackMyVendor API reference

In this article: Every endpoint, the OAuth flow, the events we send and the error codes, for developers.

Applies to Pro plan

Everything the TrackMyVendor API offers, for anyone writing code against it: how to authenticate, every endpoint with what it takes and returns, the events we send, and what an error looks like. To get a key and keep it safe, see Use your API key.

Requests go to https://trackmyvendor.com/api/v1. Bodies are JSON: send Content-Type: application/json, and every response is JSON too.

Authentication

Every request carries one credential in the Authorization header, after the word Bearer. A credential anywhere else — the URL, another header — is refused. There are two kinds, and both act in exactly one workspace.

CredentialLooks likeFor
API keytmv_…Your own scripts. Generated by an owner on Settings → Integrations → API Key; one per workspace.
OAuth access tokenanything elseApps that people connect to their account, such as Zapier. Issued to a person, for the workspace they choose.
curl https://trackmyvendor.com/api/v1/me \
  -H "Authorization: Bearer tmv_your_key_here"

OAuth 2.0

Apps use the authorization-code flow. OAuth clients are registered by us, not self-serve — contact support with your app's name and redirect URI.

  1. Send the person to sign in

    Send the person to GET https://trackmyvendor.com/oauth/authorize with client_id, redirect_uri, response_type=code, scope=api and a state of your own. PKCE is supported with code_challenge_method=S256.

  2. They approve one workspace

    They sign in, choose which workspace to connect, and approve. Only workspaces whose plan includes integrations are offered. We redirect back to your redirect_uri with code and your state.

  3. Exchange the code for tokens

    POST https://trackmyvendor.com/oauth/token with grant_type=authorization_code, the code, the same redirect_uri, client_id and client_secret (and code_verifier if you used PKCE). The answer carries access_token, refresh_token and expires_in.

  4. Refresh before it lapses

    An access token lasts two hours. Renew it with grant_type=refresh_token and the refresh_token. Each refresh issues a new refresh token; the old one stops working once the new access token is first used, so a refresh you retry after a dropped connection still succeeds.

A token stops working the moment its person loses access to the workspace, or disconnects your app on Settings → Integrations → Connected Apps. To sign out from your side, POST https://trackmyvendor.com/oauth/revoke with the token, client_id and client_secret.

Limits and plan

  • Plan. Requests are answered only while the workspace's plan includes integrations; otherwise every one is refused with plan_required. Nothing is deleted, so it all works again when the plan does.
  • Rate. 120 requests a minute per workspace. Over that, 429 with rate_limited.
  • The same rules as the screens. The contractor limit on your plan, unique contractor names, and a 48-hour pause between emails to one contractor all apply. What the API changes shows in the contractor's activity, marked with where it came from.

Errors

Every error has the same shape, and its message is written to be shown to a person as it is:

{ "error": { "code": "limit_reached", "message": "Contractor limit reached (25). Upgrade your plan to add more." } }
StatusCodeMeans
401unauthorizedNo credential, or one that is wrong, expired, revoked or regenerated. For OAuth, refresh and try again.
403plan_requiredThe workspace's plan does not include integrations.
403limit_reachedThe workspace is at its contractor limit.
403checkout_requiredThe trial has ended and checkout was never completed.
403read_onlyA read-only demo account tried to change something.
404not_foundNo such record in this workspace, or no such endpoint.
409cooldownThe contractor was emailed in the last 48 hours. Send again with force=true to email them anyway.
400, 422invalid_requestSomething in the request is missing or not allowed; the message says what.
422no_emailThe contractor has no email address to send to.
429rate_limitedToo many requests this minute.

Records from another workspace answer 404, never 403, so the API does not confirm they exist.

The contractor

Every endpoint and every event returns a contractor in one shape:

{
  "id": 412,
  "name": "Ridgeline Electric",
  "email": "office@ridgeline.example",
  "primary_contact_name": "Dana Ortiz",
  "phone": "555-0142",
  "active": true,
  "auto_reminders": true,
  "compliance_status": "not_compliant",
  "compliance_reasons": ["The certificate from Travelers expired Sep 1, 2026"],
  "url": "https://trackmyvendor.com/app/vendors/412",
  "created_at": "2026-03-09T15:41:33Z",
  "updated_at": "2026-09-28T09:12:04Z"
}

compliance_status is one of compliant, not_compliant or no_documents — the badge on the contractor's page, with your required documents and project requirements counted. compliance_reasons is the list under its Why?, empty when there is nothing to say.

Endpoints

GET /me

Who the credential belongs to. user is null for an API key.

{
  "organization": { "id": 7, "name": "Ridgeline Construction", "plan": "Pro", "url": "https://trackmyvendor.com" },
  "user": { "id": 21, "email": "ops@ridgeline.example", "name": "Dana Ortiz" }
}

GET /contractors

The workspace's contractors, newest first.

Parameter
emailThe whole address, ignoring capitals.
nameThe whole name, ignoring capitals.
queryAny part of the name.
activetrue or false.
sort-created_at (the default), created_at, -updated_at, updated_at, name or -name.
page, per_pageFrom page 1, 25 a page, at most 100.
{ "data": [ contractor, … ], "meta": { "page": 1, "per_page": 25, "total": 112, "has_more": true } }

GET /contractors/:id

One contractor, as { "data": contractor }.

POST /contractors

Adds a contractor. name is required; email, phone, primary_contact_name, notes, active and auto_reminders are optional. Add request_documents: true, and optionally a message, to email them their upload link straight away — that needs an email.

{ "data": contractor, "document_request": { "id": 88, "email": "office@ridgeline.example", "expires_at": "…", "sent_at": "…" }, "warnings": [] }

document_request is null when none was asked for. If one was asked for and could not be sent, the contractor is still created and warnings says so.

PATCH /contractors/:id

Changes only the fields you send, from the same list as above. Returns { "data": contractor }.

POST /contractors/:id/document_requests

Emails the contractor their upload link — the same email as Request Documents on their page, listing everything they owe. Optional message. A contractor emailed in the last 48 hours is not emailed again unless you send force: true; without it the answer is 409 cooldown.

{ "data": { "id": 88, "email": "office@ridgeline.example", "expires_at": "2026-10-29T15:00:00Z", "sent_at": "2026-09-29T15:00:00Z" } }

GET /events?type=…

Up to three example events of one type, built from your own contractors, in exactly the shape a delivery arrives in — for setting up an integration without waiting for something to happen. They are examples, not a history: their ids start with sample_, and a status change has no from. Answers { "data": [ event, … ] }, empty when there is nothing to show.

POST /hooks and DELETE /hooks/:id

Subscribe an HTTPS url to one event, and unsubscribe it again. A subscription is an ordinary webhook endpoint — signed, logged, and listed on Settings → Integrations.

POST /hooks   { "url": "https://example.com/hooks/tmv", "event": "contractor.coi_expiring" }
→ 201 { "data": { "id": 31, "url": "https://example.com/hooks/tmv", "event": "contractor.coi_expiring", "created_at": "…" } }

DELETE /hooks/31
→ 204
  • An address on a private network is refused.
  • Only subscriptions made through the API can be removed through it.
  • One made with an OAuth token stops receiving when its person loses access or disconnects the app; one made with the API key is removed when the key is regenerated or revoked.

Events

Every event — delivered to a subscription, or returned by GET /events — is:

{
  "id": "evt_4f1c9a2b7d3e8f0a1b2c3d4e",
  "event": "contractor.coi_expiring",
  "created_at": "2026-09-29T11:00:04Z",
  "data": { "contractor": contractor, … }
}
EventFires whenAlso in data
contractor.createdA contractor is added, any way.—
contractor.compliance_changedTheir status badge changes; checked each morning.from, to
contractor.license_expiringA licence enters one of your alert windows.expiration_date, days_left, window, license (id, number, holder, state, type, category)
contractor.coi_expiringA certificate, or one policy on it, enters one of your alert windows.expiration_date, days_left, window, coverage (the policy, or null for the certificate itself), certificate (id, carrier, policy_number, insured_name, expiration_date)
contractor.w9_missingA contractor who had a W-9 on file no longer has one.—

A delivery is a POST with the headers X-TMV-Event, X-TMV-Event-Id (the event's id), X-TMV-Delivery and X-TMV-Signature, which you can verify. Answer with a 2xx quickly. Anything else is retried, three attempts in all, with the same id — use it to ignore a repeat. A 410 Gone switches the subscription off instead.

Related articles

Didn't find what you were looking for?

Send us the question — a person answers, usually the same working day.

Contact support