Getting Started with the UserVoice API

The UserVoice API provides a fast and easy way of working with your feedback data that allows you to build client applications and custom integrations.

Authentication

Any interaction with the UserVoice API requires a trusted API client. You can create one from your UserVoice Admin Console in Settings → Integrations → UserVoice API keys.

Enter a name for the API Client. Make sure the “Trusted” checkbox is checked:

When you create your client you will see a “key” and a “secret”: these are the credentials you will use to access the API.

Beware: You should never store trusted client credentials in an insecure environment (for example: in your client-side JavaScript or a public source code repository). Trusted clients have full access to perform the same actions admins do, including deleting content.

Note: The UserVoice API requires all calls to be made over SSL. Please use HTTPS for all requests to ensure the security and privacy of your data.

To make an API request, you will need an API token. You can request a token for your UserVoice account owner with the API key and secret. Here’s how it would be done with curl:

$ curl https://SUBDOMAIN.uservoice.com/api/v2/oauth/token \
       --data "grant_type=client_credentials;client_id=KEY;client_secret=SECRET"
Example response:
{"access_token": "096e8ae9c6a3c039"}

Now you can authenticate your API requests by passing your access_token in the Authorization header. Here’s how you would do that with curl:

  $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/users/current \
       -H "Authorization: Bearer 096e8ae9c6a3c039"

All requests to the API will need to use this Authorization header for access. Store the token in your client application. For instance, you might save it in a session store, connection object, or variable, as convenient on your platform.

If the token expires or is invalidated, the API will return a 401 Unauthorized error that looks like this:

  {"errors":{"error":"not_authorized"}}

At this point, your application should request a new access_token and retry the request with the new token.

Data Format

The UserVoice API will respond to all requests with a JSON data structure. Bodies for POST and PUT requests can be passed as either JSON or url-form-encoded data.

JSON Request:
  $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/suggestions \
       -X "POST" \
       -H "Authorization: Bearer 096e8ae9c6a3c039" \
       -H "Content-type: application/json" \
       -d "{
           'title': 'Test Suggestion',
           'links': {
              'forum': 1
            }
          }"
Form Request:
  $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/suggestions \
       -X "POST" \
       -H "Authorization: Bearer 096e8ae9c6a3c039" \
       -H "Content-type: application/x-www-form-urlencoded" \
       -d "title=Test+Suggestion&links.forum=1"
Response:
  {
    "links": {
      "suggestions.category": {
        "type": "categories"
      },
      "suggestions.created_by": {
        "type": "users"
      },
      "suggestions.forum": {
        "type": "forums"
      },
      "suggestions.labels": {
        "type": "labels"
      },
      "suggestions.last_status_update": {
        "type": "status_updates"
      },
      "suggestions.parent_suggestion": {
        "type": "suggestions"
      },
      "suggestions.status": {
        "type": "statuses"
      },
      "suggestions.ticket": {
        "type": "tickets"
      }
    },
    "suggestions": [
      {
        "id": 184102,
        "title": "Test Suggestion",
        "body": "",
        "body_mime_type": "",
        "portal_url": "http://SUBDOMAIN.uservoice.com/forums/1/suggestions/184102",
        "admin_url": "https://SUBDOMAIN.uservoice.com/admin/v3/suggestions/184102",
        "created_at": "2017-02-24T16:27:36Z",
        "creator_user_agent": null,
        "creator_referrer": null,
        "creator_browser": null,
        "creator_browser_version": null,
        "creator_os": null,
        "creator_mobile": false,
        "state": "approved",
        "inappropriate_flags_count": 0,
        "approved_at": null,
        "updated_at": "2017-02-24T16:27:36Z",
        "closed_at": null,
        "comments_count": 0,
        "notes_count": 0,
        "requests_count": 0,
        "votes_count": 0,
        "supporters_count": 0,
        "supporting_accounts_count": 0,
        "supporter_mrr": 0,
        "supporter_satisfaction_score": 0,
        "satisfaction_detractor_count": 0,
        "satisfaction_promoter_count": 0,
        "satisfaction_neutral_count": 0,
        "average_engagement": 0,
        "recent_engagement": 1,
        "engagement_trend": 0,
        "first_support_at": null,
        "links": {
          "forum": 1,
          "category": null,
          "labels": null,
          "status": null,
          "parent_suggestion": null,
          "ticket": null,
          "last_status_update": null,
          "created_by": 9182
        }
      }
    ]
  }

Associations & Side-loading

The response JSON object contains a key for each collection you request. The value of each key is an array of objects. Using the “includes” param, you can easily side-load associated records. This allows you to request additional data in a single query and gives you control over the weight of the response. The “links” object in a response provides a list of available associations.

In this example, we’re requesting a list of suggestions and side-loading the associated forums and users:

  $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/suggestions?includes=forums,users \
       -H "Authorization: Bearer 096e8ae9c6a3c039"
(Simplified) Response:
  {
    "forums": [
      {
        "id": 2002,
        "name": "Product Ideas",
        "open_suggestions_count": 1,
        "suggestions_count": 1,
        "is_public": true,
        "created_at": "2017-01-20T14:24:45Z",
        "updated_at": "2017-01-20T14:24:45Z",
        "links": {
          "updated_by": null
        }
      }
    ],
    "links": {
      "forums.updated_by": {
        "type": "users"
      },
      "suggestions.category": {
        "type": "categories"
      },
      "suggestions.created_by": {
        "type": "users"
      },
      "suggestions.forum": {
        "type": "forums"
      },
      "suggestions.labels": {
        "type": "labels"
      },
      "suggestions.last_status_update": {
        "type": "status_updates"
      },
      "suggestions.parent_suggestion": {
        "type": "suggestions"
      },
      "suggestions.status": {
        "type": "statuses"
      },
      "suggestions.ticket": {
        "type": "tickets"
      },
      "users.current_nps_rating": {
        "type": "nps_ratings"
      },
      "users.external_users": {
        "type": "external_users"
      },
      "users.previous_nps_rating": {
        "type": "nps_ratings"
      },
      "users.teams": {
        "type": "teams"
      }
    },
    "list_meta": null,
    "pagination": {
      "page": 1,
      "per_page": 20,
      "total_pages": 1,
      "total_records": 1
    },
    "suggestions": [
      {
        "id": 303,
        "title": "Make the logo bigger",
        "body": "I love your logo but I can barely see it!",
        "created_at": "2012-07-19T14:23:16Z",
        "state": "published",
        "updated_at": "2017-01-20T14:24:47Z",
        "comments_count": 1,
        "links": {
          "forum": 2002,
          "category": null,
          "labels": null,
          "status": null,
          "parent_suggestion": null,
          "ticket": null,
          "last_status_update": null,
          "created_by": 1001
        }
      }
    ],
    "users": [
      {
        "id": 1001,
        "name": "Logo Fan",
        "email_address": "logofan@example.com",
        "avatar_url": "https://cdn.uservoice.com/pkg/admin/icons/user_80-fc2452956c9d66d679a429bd81acd3f3.png",
        "created_at": "2017-01-20T14:24:47Z",
        "updated_at": "2017-01-20T14:24:47Z",
      }
    ]
  }

Using the “links” property of the suggestion object, we can map the “forum” and “created_by” ids to the forum our suggestion belongs to and the user that created it.

Pagination

There are two methods of pagination: cursor-based and page-based. Cursor-based pagination is not available for every sort order on every collection, but it should be preferred whenever it is available because it offers a more reliable ordering of results and more consistent API performance. There is also a Link header provided which will make this decision for you.

In either case the number of records you receive is controlled by the per_page parameter. The default per_page is 20 and the maximum is 100. There will be a pagination object in the response with metadata about the current page:

  "pagination": {
    "page": 1,
    "per_page": 100,
    "total_pages": 3,
    "total_records": 272
  }

Cursor-based pagination

When you get the first page of results, if cursor-based pagination is supported, there will be a cursor value in the pagination part of the response. Pass this cursor value as a parameter to the same endpoint to retrieve the next page of results along with a new cursor value.

Example:

GET /api/v2/admin/suggestions?sort=-supporters_count
{
    "suggestions": [ … ],
    "pagination": {
        "total_records": 200,
        "total_pages": 10,
        "page_size": 20,
        "page": 1,
        "cursor": "eyJzb3J0IjoiIiwidmFsIjoyNjMsImxhc3RfaWQiOjIzMTUyMjJ9"
    }
}

GET /api/v2/suggestions?cursor=eyJzb3J0IjoiIiwidmFsIjoyNjMsImxhc3RfaWQiOjIzMTUyMjJ9&sort=-supporters_count
{
    "suggestions": [ … ],
    "pagination": {
        "total_records": 200,
        "total_pages": 10,
        "page_size": 20,
        "page": 1,
        "cursor": "eyJzb3J0IjoiIiwidmFsIjoxNzgsImxhc3RfaWQiOjIwODc3OX0="
    }
}

Note that when using cursor-based pagination the page value in the response will always be 1. You will know that you are on the last page of results when there is no longer a cursor value in the response.

With cursor-based pagination you must always start at the beginning of a list and work your way forward. It is not possible to move backward or to jump to certain offset the way you can with page-based pagination. A cursor token can only be used to request records in the same sort order as the request that provided it. If you want the records in a different order, you must start at the beginning without a cursor parameter.

The cursor represents a fixed point in the ordered set of results. If you change the per_page parameter when using a cursor you will get a different number of results but they will start with the same record.

Cursor-based pagination is ideal for scenarios where you want to pull a long list of records from the API. Its performance should be basically constant from the first page of results to the last, whereas page-based pagination will degrade in performance the deeper you get into the list.

Another benefit of cursors is that your position in the list will not be shifted if objects are created or deleted while you are paging. With page-based pagination those events could result in a record being skipped, or a record being returned twice.

Page-based pagination

With page-based pagination, the records you get are determined by multiplying the page number by the per_page parameter. To get the next page of results, you simply increment the page parameter.

Example:

GET /api/v2/admin/suggestions?sort=-cf_my_field
{
    "suggestions": [ … ],
    "pagination": {
        "total_records": 200,
        "total_pages": 10,
        "page_size": 20,
        "page": 1
    }
}

GET /api/v2/suggestions?sort=-cf_my_field&page=2
{
    "suggestions": [ … ],
    "pagination": {
        "total_records": 200,
        "total_pages": 10,
        "page_size": 20,
        "page": 2
    }
}

If you change the per_page parameter when requesting a page other that 1, you will get completely different results since the offset is computed by (page - 1) * per_page.

Page-based pagination is inferior to cursor-based pagination in both consistency and performance and should only be used with the latter is unavailable.

A link header is provided along with every API response when another page of results is available. If you are on the last page of results, there will not be a link header.

Cursor example:

Link: </api/v2/admin/suggestions?cursor=eyJzb3J0IjoiIiwidmFsIjoxNzgsImxhc3RfaWQiOjIwODc3OX0%3D&sort=-supporters_count>; rel=next

Page example:

Link: </api/v2/admin/suggestions?page=2&sort=-cf_my_field>; rel=next

The link header will automatically switch between page-based and cursor-based pagination depending on whether the latter is available for the requested sort order.

Rate Limiting

API requests have per-minute rate limiting. Each request counts toward your limit. Not all requests are created equal: if a request takes longer than 1 second to compute then another “request” is counted for every whole second after the first. This helps us account for expensive endpoints (like complex searches) that use more resources.

Once you have exceeded your limit for the current minute, the UserVoice API will return a 429 HTTP error response for subsequent requests.

Every request returns the following HTTP headers:

  • X-Rate-Limit-Limit: The number of requests available every minute
  • X-Rate-Limit-Remaining: The number of requests remaining this period
  • X-Rate-Limit-Reset: The unix epoch in seconds when the limit resets

Once rate limited, every request returns the following HTTP header:

  • Retry-After: The unix epoch in seconds until the rate limit expires

The limit depends on your plan. See Terms of Service for details.

Take advantage of side-loading, per_page settings and bulk endpoints to consolidate your requests.

File Uploads

The UserVoice API allows you to upload files and link them to suggestions. This is done through a two-step process.

  1. Use the Attachments API to upload your file in multipart/form-data format. Take note of the token in the response, as this will allow you to link the file to a suggestion later on. Note: If you lose your access token, don’t worry. You can retrieve it later through our GET endpoint!
  2.     $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/attachments \
             -H "Authorization: Bearer <token>" \
             -F "file=@/path/to/file"
      
        {
          "attachments": [
            {
              "id": 116876320,
              "file_name": "test.jpg",
              "content_type": "image/jpeg",
              "size_in_bytes": 14919,
              "updated_at": "2017-02-16T14:06:35Z",
              "created_at": "2017-02-16T14:06:35Z",
              "url": "someUrl.com",
              "thumb_url": "someOtherUrl.com",
              "token": "1234567890ebcf3635a209ab4ozp1kd9",
              "links": {
                "suggestion": null,
                "note": null
              }
            }
          ],
          "links": {
            "attachments.note": {
              "type": "notes"
            },
            "attachments.suggestion": {
              "type": "suggestions"
            }
          }
        }
      
  3. Use the token you received in the response to link the file to a suggestion or when creating a note. Take a look at our Suggestions API for how to update a suggestion with an attachment token, or at our Notes API for how to create a note with an attachment token.
  4.     $ curl https://SUBDOMAIN.uservoice.com/api/v2/admin/suggestions/ID \
              -X "PUT" \
              -H "Authorization: Bearer <token>" \
              -d attachment_tokens=1234567890ebcf3635a209ab4ozp1kd9,someOtherToken
      
        {
          "links": {
            "suggestions.category": {
              "type": "categories"
            },
            "suggestions.created_by": {
              "type": "users"
            },
            "suggestions.forum": {
              "type": "forums"
            },
            "suggestions.labels": {
              "type": "labels"
            },
            "suggestions.last_status_update": {
              "type": "status_updates"
            },
            "suggestions.parent_suggestion": {
              "type": "suggestions"
            },
            "suggestions.status": {
              "type": "statuses"
            },
            "suggestions.ticket": {
              "type": "tickets"
            }
          },
          "suggestions": [
            {
              "id": 12345,
              "title": "Make a Getting Started Guide",
              "body": "A good starting guide would be helpful thanks!",
              "body_mime_type": "",
              "portal_url": "http://SUBDOMAIN.uservoice.com/forums/54321/suggestions/12345",
              "admin_url": "https://SUBDOMAIN.uservoice.com/admin/v3/suggestions/12345",
              "created_at": "2013-03-08T00:17:42Z",
              "creator_user_agent": null,
              "creator_referrer": null,
              "creator_browser": null,
              "creator_browser_version": null,
              "creator_os": null,
              "creator_mobile": false,
              "state": "approved",
              "inappropriate_flags_count": 0,
              "approved_at": null,
              "updated_at": "2017-02-16T14:09:34Z",
              "closed_at": null,
              "comments_count": 2,
              "notes_count": 0,
              "requests_count": 0,
              "votes_count": 1,
              "supporters_count": 0,
              "supporting_accounts_count": 0,
              "supporter_mrr": 0,
              "supporter_satisfaction_score": 0,
              "satisfaction_detractor_count": 0,
              "satisfaction_promoter_count": 0,
              "satisfaction_neutral_count": 0,
              "average_engagement": 0,
              "recent_engagement": 0,
              "engagement_trend": 0,
              "first_support_at": null,
              "links": {
                "forum": 54321,
                "category": null,
                "labels": null,
                "status": 567899,
                "parent_suggestion": null,
                "ticket": null,
                "last_status_update": 5321563,
                "created_by": 32364234
              }
            }
          ]
        }
      

Constraints

File Size: 50 MB (50000000 bytes)

A complete list of endpoints is available in our API Reference.