Introduction

Updated 11 Oct, 2026 08:40 PM in API

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.

Every resource carries links to itself and to what you can do next, in fields ending in _href:

  • href is the resource itself in the API.
  • html_href is the same thing as a page on your site, for people.
  • Other *_href fields lead to related resources and actions, such as a discussion's comments_href or resolve_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 for Retry-After before the next request.

More from the knowledge base