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.
| Credential | Looks like | For |
|---|---|---|
| API key | tmv_… | Your own scripts. Generated by an owner on Settings → Integrations → API Key; one per workspace. |
| OAuth access token | anything else | Apps 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.
-
Send the person to sign in
Send the person to
GET https://trackmyvendor.com/oauth/authorizewithclient_id,redirect_uri,response_type=code,scope=apiand astateof your own. PKCE is supported withcode_challenge_method=S256. -
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_uriwithcodeand yourstate. -
Exchange the code for tokens
POST https://trackmyvendor.com/oauth/tokenwithgrant_type=authorization_code, thecode, the sameredirect_uri,client_idandclient_secret(andcode_verifierif you used PKCE). The answer carriesaccess_token,refresh_tokenandexpires_in. -
Refresh before it lapses
An access token lasts two hours. Renew it with
grant_type=refresh_tokenand therefresh_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,
429withrate_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." } }
| Status | Code | Means |
|---|---|---|
| 401 | unauthorized | No credential, or one that is wrong, expired, revoked or regenerated. For OAuth, refresh and try again. |
| 403 | plan_required | The workspace's plan does not include integrations. |
| 403 | limit_reached | The workspace is at its contractor limit. |
| 403 | checkout_required | The trial has ended and checkout was never completed. |
| 403 | read_only | A read-only demo account tried to change something. |
| 404 | not_found | No such record in this workspace, or no such endpoint. |
| 409 | cooldown | The contractor was emailed in the last 48 hours. Send again with force=true to email them anyway. |
| 400, 422 | invalid_request | Something in the request is missing or not allowed; the message says what. |
| 422 | no_email | The contractor has no email address to send to. |
| 429 | rate_limited | Too 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 | |
|---|---|
email | The whole address, ignoring capitals. |
name | The whole name, ignoring capitals. |
query | Any part of the name. |
active | true or false. |
sort | -created_at (the default), created_at, -updated_at, updated_at, name or -name. |
page, per_page | From 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, … }
}
| Event | Fires when | Also in data |
|---|---|---|
contractor.created | A contractor is added, any way. | — |
contractor.compliance_changed | Their status badge changes; checked each morning. | from, to |
contractor.license_expiring | A licence enters one of your alert windows. | expiration_date, days_left, window, license (id, number, holder, state, type, category) |
contractor.coi_expiring | A 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_missing | A 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.