The Tender API lets your own scripts and integrations work with your Tender site: read and answer discussions, manage users and companies, and keep your knowledge base in sync with other systems. It's a JSON API over HTTPS.
Only your site's support staff (supporters and the site owner) can use it. Each request is made as one supporter, with that person's access.
In this section
| Article | What it covers |
|---|---|
| Authentication | Personal API tokens: making one and sending it |
| OAuth for applications | Building an app that acts for other supporters |
| Connecting an AI assistant (MCP) | Letting Claude and other assistants work with your site |
| Sites | Your site and your own profile |
| Discussions | Listing, creating, resolving and moving discussions |
| Comments | Replies and internal notes |
| Categories | Discussion categories |
| Queues | Queues and the discussions in them |
| Users | Customers and staff |
| Companies | Companies and their users |
| Sections | Knowledge base sections |
| KB articles | Knowledge base articles |
| KB article ratings | "Was this helpful?" ratings |
| Webhooks | Getting told when things change |
The base URL
Every request goes to:
https://api.tenderapp.com/yoursite
"yoursite" is your site's permalink: the first part of yoursite.tenderapp.com. Sites with their own domain (such as support.example.com) still use api.tenderapp.com and their permalink.
Format
The API speaks JSON only. Send this header with every request:
Accept: application/vnd.tender-v1+json
Accept: application/json works too. Any other Accept header gets 406 Not Acceptable:
Invalid Accepts Header: text/html, should be: application/vnd.tender-v1+json and application/json
Responses come back as application/vnd.tender-v1+json. When you send data (POST, PUT), send it as JSON with Content-Type: application/json.
A first request
Make a personal API token first (see Authentication) and put it in $TENDER_TOKEN. Then ask for your site:
curl -H "Accept: application/vnd.tender-v1+json" \
-H "Authorization: Bearer $TENDER_TOKEN" \
https://api.tenderapp.com/yoursite
{
"name": "Juniper",
"permalink": "juniper",
"href": "https://api.tenderapp.com/yoursite",
"categories_href": "https://api.tenderapp.com/yoursite/categories{?page}",
"discussions_href": "https://api.tenderapp.com/yoursite/discussions{/state}{?page,user_email}",
"profile_href": "https://api.tenderapp.com/yoursite/profile",
"users_href": "https://api.tenderapp.com/yoursite/users{?page,email}",
"html_href": "https://yoursite.tenderapp.com"
}
That's the starting point for everything else. See Sites for the whole response.
Links and URI templates
Every resource carries links to itself and to what you can do next, in fields ending in _href:
hrefis the resource itself in the API.html_hrefis the same thing as a page on your site, for people.- Other
*_hreffields lead to related resources and actions, such as a discussion'scomments_hreforresolve_href.
Some links are URI templates (RFC 6570): the parts in braces are optional values you fill in. For example, from
https://api.tenderapp.com/yoursite/discussions{/state}{?page,user_email}
you can make any of these:
https://api.tenderapp.com/yoursite/discussions
https://api.tenderapp.com/yoursite/discussions/open
https://api.tenderapp.com/yoursite/discussions/open?page=2
https://api.tenderapp.com/yoursite/discussions?user_email=sam%40example.com
Most languages have a URI template library (Ruby's addressable, Python's uritemplate, JavaScript's url-template). If you'd rather not use one, removing everything in braces gives you a working URL.
Following links rather than building URLs yourself keeps your code working if an address ever changes.
Pagination
Lists come back 30 at a time, wrapped in an object that says where you are:
{
"offset": 0,
"total": 14,
"per_page": 30,
"discussions": [
{
"title": "Export to CSV leaves out custom fields",
"state": "open",
"href": "https://api.tenderapp.com/yoursite/discussions/21",
"html_href": "https://yoursite.tenderapp.com/discussions/problems/10"
}
]
}
Add ?page=2, ?page=3 and so on for the next ones. Past the last page, the list is empty:
{"discussions":[],"offset":30,"total":14,"per_page":30}
Errors
Successful requests answer 200 OK (or 201 Created for some creates). When something goes wrong, the status code says what:
| Status | Meaning |
|---|---|
401 Unauthorized |
No token, or a token that's wrong, revoked or expired. The WWW-Authenticate header says which (see Authentication). |
403 Forbidden |
The token's owner isn't on your support staff, or a read-only token tried to change something. |
404 Not Found |
There's no such thing, or no such site. |
406 Not Acceptable |
The Accept header is missing or wrong. |
422 Unprocessable Entity |
The data you sent isn't valid. The body lists the problems, by field. |
429 Too Many Requests |
Slow down: see below. |
A 422 looks like this:
{"name":["can't be blank"]}
The bodies of 401, 403 and 406 responses are a short plain-text message meant for people.
Rate limits
The REST API doesn't set a fixed number of requests per minute, but requests can be limited when there are too many, and some parts of Tender have limits of their own (the OAuth endpoints and the MCP server).
A limited request gets 429 Too Many Requests with a Retry-After header: the number of seconds to wait before trying again. Honour it, and keep your integration gentle:
- Don't poll: use webhooks to hear about changes.
- Make one request at a time rather than many in parallel.
- When you get a
429, wait forRetry-Afterbefore the next request.
