Fav Quotes

FavQs API v2

Access our API at:

https://favqs.com/api/

For example, get the Quote of the Day:

https://favqs.com/api/qotd

All data is sent and received as JSON.


Headers

All data is sent and received as JSON.

Content-Type: application/json

This must be set in the request headers when the request body contains content.

There are three HTTP status codes that you need to check for: 200, 404, and 500.

Success

A successful request returns a 200 response and no error_code.

Validation Errors

A request containing validation errors will return a 200 response (breaking from 4xx convention) as well as an error_code and message containing the reason (in English) for the validation error. A full list of error codes is available in the last section of this page if you would like to provide translations within your app.

Summary

To recap, check for a 200 to see if the server received and understood your request. Then check for the presence of an error_code to make sure there were no validation errors on your request. If there were, a message will be present and can be shown to the user.

An app token is required to access our API (except for the Quote of the Day).

For authorized API calls, pass your access token in via the header:

Authorization: Token token="YOUR_APP_TOKEN"

For example,

$ curl -H 'Authorization: Token token="YOUR_APP_TOKEN"'

Note:  This format will not work:

Authorization: "YOUR_APP_TOKEN"

For calls that require a user session, pass the user token in via the header:

User-Token: "USER_SESSION_TOKEN"

The double quotes are optional.

You can specify the version of the API in the header of each request.

$ curl -H 'Accept: application/vnd.favqs.v2+json;'

Note:  The API will default to the latest version if no version is specified.

It is a best practice to include the version of the API in each request, as the default version could change and break your app.

Previous Versions

Prior versions of the API include Version 1.

All responses include an ETag (Entity Tag) in the header. The ETag identifies the specific version of a returned resource. Use this value to check for changes to a resource by repeating the request and passing the ETag value in the If-None-Match header. If the resource has not changed, a 304 Not Modified status will be returned with an empty body. If the resource has changed, the request will return the resource.

Response Header
HTTP/1.1 200 OK 
...
Etag: "558f0b9bea2e6910b1f93de7f4c0d47c"
Request
$ curl -I -H 'If-None-Match: "558f0b9bea2e6910b1f93de7f4c0d47c"'
HTTP/1.1 304 Not Modified

Access to the API is rate limited. Each session is allowed 30 requests in a 20 second interval.

The remaining number of requests is returned in the HTTP header 'Rate-Limit-Remaining'.

Response Header
HTTP/1.1 200 OK 
...
Rate-Limit-Remaining: 24

Session

A user can only have one session per app token. If you have more than one app, please request another app token.

To create a session for the user, inside your application:

Request
POST /api/session
Request Body

Note: A user can login with their username or email address.

{ 
  "user": {
    "login": "login_or_email_here",
    "password": "user_password_here"
  }
}
Response Body
{ 
  "User-Token": "i4dwXAUNfSUEaTP90K5RWOWLap5cvc9mtjfpvA+c0uufP5GYnpYZUO4k5pTEpwsJHVHNhY5lbLn0cDRpiwsOKA==",
  "login": "user_login",
  "email": "user_email"
}

The "User-Token" should be added to the header for requests that require a user session.

Errors

An invalid login or password will return an error:

{ 
  "error_code": 21,
  "message": "Invalid login or password."
}

An account that has been deactivated will return an error:

{ 
  "error_code": 22,
  "message": "Login is not active. Contact support@favqs.com."
}

A request with missing data will return an error:

{ 
  "error_code": 23,
  "message": "User login or password is missing."
}

To destroy a user session (logout):

Request
DELETE /api/session
Response Body
{ 
  "message": "User logged out."
}
Error

If the user revokes account access to your app from their FavQs.com account page, you will see:

{ 
  "error_code": 20,
  "message": "No user session found."
}

Users

To create a new user:

Request
POST /api/users
Request Body
{ 
  "user": {
    "login": "user_login_here",
    "email": "user_email_here",
    "password": "user_password_here"
  }
}
Validations
Login
  • Can only contain letters (a-z), numbers (0-9) and the underscore (_)
  • Length must be between 1 and 20 characters
  • Email
  • Must be a valid email address
  • Password
  • Length must be between 5 and 120 characters
  • Response Body

    Successful creation of a user will return a session token for that user.

    { 
      "User-Token": "i4dwXAUNfSUEaTP90K5RWOWLap5cvc9mtjfpvA+c0uufP5GYnpYZUO4k5pTEpwsJHVHNhY5lbLn0cDRpiwsOKA==",
      "login": "user_login"
    }

    The "User-Token" should be added to the header for requests that require a user session.

    Errors

    If a user session already exists, you will get an error:

    { 
      "error_code": 31,
      "message": "User session present."
    }

    If any of the fields contain validation errors, message will contain a list of validation errors separated by a semi-colon.

    {
      "error_code": 32,
      "message": "Email is not a valid email; Password is too short (minimum is 5 characters)"
    }

    To get a user:

    Request
    GET /api/users/:login
    Response

    A user session may query any user.

    {
      "login": "gose",
      "pic_url": "https://pbs.twimg.com/profile_images/2160924471/Screen_Shot_2012-04-23_at_9.23.44_PM_.png",
      "public_favorites_count": 520,
      "followers": 12,
      "following": 23,
      "pro": true
    }
    Response

    Note:  User session required

    If you request the user of the current user session, you will also get "account_details".

    {
      "login": "gose",
      "pic_url": "https://pbs.twimg.com/profile_images/2160924471/Screen_Shot_2012-04-23_at_9.23.44_PM_.png",
      "public_favorites_count": 520,
      "followers": 12,
      "following": 23,
      "pro": true,
      "account_details": {
        "email": "gose@favqs.com",
        "private_favorites_count": 22,
        "active_theme_id": 1,
        "pro_expiration": "2015-03-13T07:19:06.133-05:00"
      }
    }

    If the user is Pro, you will also get an active_theme_id and pro_expiration .

    Note:  User session required

    To update a user:

    Request
    PUT /api/users/:login
    Request Body

    All fields are optional.

    { 
      "user": {
        "login": "user_login_here",
        "email": "user_email_here",
        "password": "user_password_here",
        "twitter_username": "twitter_username_here",
        "facebook_username": "facebook_username_here",
        "pic": "twitter",
        "profanity_filter": false,
        "public_themes": false
      }
    }
    Validations

    Additional validations are listed under 'Create User'.

    Pic
  • Either ["twitter", "facebook", "gravater", ""]
  • Profanity Filter
  • Must be either [true, false]. True means it's enabled (default).
  • Response Body
    { 
      "message": "User successfully updated."
    }

    Profile Pics

    Setting a user's profile picture involves:

    1. Setting "twitter_username", "facebook_username", or relying on the user's email having a Gravatar.
    2. Setting "pic" to either ["twitter", "facebook", "gravater", ""]
    Request Body

    For example, to set user gose's pic to their Twitter handle 'sgose':

    { 
      "user": {
        "twitter_username": "sgose",
        "pic": "twitter"
      }
    }
    Errors

    If there is a validation error, one or more validation errors will be returned:

    {
      "error_code": 32,
      "message": {
        "login": [
          "has already been taken"
        ],
        "email": [
          "is not a valid email"
        ]
      }
    }

    To request a password reset.

    Request
    POST /api/users/forgot_password
    Request Body
    { 
      "user": {
        "email": "user_email_here"
      }
    }
    Response Body
    { 
      "message": "A reset link has been emailed."
    }
    Errors

    If the user login or email is not found, a 404 will be returned.

    { 
      "error_code": 30,
      "message": "User not found."
    }

    To reset a password:

    Request
    POST /api/users/reset_password
    Request Body
    { 
      "user": {
        "email": "user_email_here",
        "reset_password_token": "user_reset_password_token_here"
      }
    }
    Response Body
    { 
      "message": "Password successfully updated. Please login."
    }
    Errors

    If the password reset token is invalid:

    { 
      "error_code": 33,
      "message": "Invalid password reset token."
    }

    To purchase a one year FavQs Pro subscription, open the following URL in a web view:

    GET /users/:login/pro

    Typeahead

    This resource provides a list of the public Authors, Tags, and Users in FavQs.

    With this data, typeahead can be performed locally giving the user a fast response.

    Request
    GET /api/typeahead
    Response
    {
      "authors": [
        {
          "count": 118,
          "permalink": "mark-twain",
          "name": "Mark Twain"
        },
        ...
      ],
      "tags": [
        {
          "count": 44,
          "permalink": "funny",
          "name": "funny"
        },
        ...
      ],
      "users": [
        {
          "count": 432,
          "permalink": "gose",
          "name": "gose"
        },
        ...
      ]
    }

    Quotes

    Get the Quote of the Day.

    Request
    GET /api/qotd
    Example
    $ curl https://favqs.com/api/qotd
    Response
    {
      "qotd_date": "2014-07-04T03:00:00.000-05:00",
      "quote": {
        "id": 17025,
        "favorites_count": 0,
        "dialogue": false,
        "favorite": false,
        "tags": [
          "equality",
          "men"
        ],
        "url": "http://localhost:3000/quotes/abraham-lincoln/17025-fourscore-and-",
        "upvotes_count": 1,
        "downvotes_count": 0,
        "author": "Abraham Lincoln",
        "author_permalink": "abraham-lincoln",
        "body": "Fourscore and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
      }
    }

    A list of quotes, paged 25 at a time.

    Request
    GET /api/quotes
    Response
    {
      "page": 1,
      "last_page": false,
      "quotes": [
        {
          "tags": [
            "linux",
            "programming",
            "code",
            "finnish-american"
          ],
          "favorite": false,
          "author_permalink": "linus-torvalds",
          "body": "Talk is cheap. Show me the code.",
          "id": 253,
          "favorites_count": 1,
          "upvotes_count": 0,
          "downvotes_count": 0,
          "dialogue": false,
          "author": "Linus Torvalds",
          "url": "https://favqs.com/quotes/linus-torvalds/253-talk-is-cheap-s-"
        },
        ...
      ]
    }

    Optional URL Parameters

    The list of quotes can be refined via these filters.

    ParameterDescription
    filterType lookup or keyword search
    type['author', 'tag', 'user']
    privateGet private quotes for the pro user session (e.g., private=true)
    hiddenGet hidden quotes for the user session (e.g., hidden=true)
    pagePage number (25 quotes per page)

    Examples

    Get a random list of quotes:

    GET https://favqs.com/api/quotes/

    Search for quotes containing the word "funny":

    GET https://favqs.com/api/quotes/?filter=funny

    Get quotes that are tagged with "funny":

    GET https://favqs.com/api/quotes/?filter=funny&type=tag

    Get quotes by Mark Twain:

    GET https://favqs.com/api/quotes/?filter=Mark+Twain&type=author

    Get the public quotes favorited by user 'gose':

    GET https://favqs.com/api/quotes/?filter=gose&type=user

    Get the private quotes of the current user (pro session required):

    GET https://favqs.com/api/quotes/?private=1

    Search for private quotes of the current user session that contain the words "little book":

    GET https://favqs.com/api/quotes/?filter=little+book&private=1

    Get quotes hidden by the user:

    GET https://favqs.com/api/quotes/?hidden=1
    Request
    GET /api/quotes/:quote_id
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
    }

    To mark a quote as a user's favorite:

    Request
    PUT /api/quotes/:quote_id/fav
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "favorite": true,
        ...
      }
    }
    Errors
    { 
      "error_code": 40,
      "message": "Quote not found."
    }
    Request

    To unmark a quote as a user's favorite:

    PUT /api/quotes/:quote_id/unfav
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "favorite": false,
        ...
      }
    }
    Errors
    { 
      "error_code": 41,
      "message": "Private quotes cannot be unfav'd."
    }

    Users can vote a quote up or down to influence that quotes popularity.

    Vote a quote up:

    Request
    PUT /api/quotes/:quote_id/upvote
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "upvote": true,
        "downvote": false,
        ...
      }
    }

    Vote a quote down:

    Request
    PUT /api/quotes/:quote_id/downvote
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "upvote": false,
        "downvote": true,
        ...
      }
    }

    To clear the vote on a quote:

    Request
    PUT /api/quotes/:quote_id/clearvote
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "upvote": false,
        "downvote": false,
        ...
      }
    }

    To add Personal Tags to a quote:

    Request
    PUT /api/quotes/:quote_id/tag

    The list of tags sent must include all of the user's personal tags for that quote.

    Request Body
    {
      "personal_tags": [
        "simplicity",
        "minimalism"
      ]
    }

    If a tag is missing from the list, that personal tag will be removed from the quote.

    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "personal_tags": [
          "simplicity",
          "minimalism"
        ]
        ...
      }
    }

    To hide a quote:

    Request
    PUT /api/quotes/:quote_id/hide
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "hidden": true,
        ...
      }
    }

    To unhide a quote:

    Request
    PUT /api/quotes/:quote_id/unhide
    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      ...
      "user_details": {
        "hidden": false,
        ...
      }
    }

    Quotes added by Pro users are private initially and can be edited.

    Quotes added by non-Pro users are public and cannot be edited.

    To add a quote:

    Request
    POST /api/quotes
    Request Body
    {
      "quote": {
        "author": "Mark Twain",
        "body": "Never let your schooling interfere with your education."
      }
    }

    To add a dialogue:

    Request
    POST /api/quotes
    Request Body
    {
      "quote": {
        "lines": [
          {
            "author": "State Trooper",
            "body": "Pullover!"
          },
          {
            "author": "Harry Dunne",
            "body": "No, it's a cardigan but thanks for noticing."
          },
          {
            "author": "Lloyd Christmas",
            "body": "Yeah, killer boots man!"
          }
        ],
        "source": "Dumb & Dumber, 1994. Film",
        "context": "While being pulled over by a State Trooper on a motorcycle.",
        "tags": "funny, movie, dumb and dumber, police"
      }
    }
    Response Body

    The response is the same as Get Quote.

    {
      "id": 123,
      "author": "Mark Twain",
      "body": "Never let your schooling interfere with your education."
      ...
    }

    Updating a quote or dialogue requires the same request format as adding a quote.

    Request
    PUT /api/quotes/:quote_id
    Response

    The response is the same as Get Quote.

    To delete a quote:

    Request
    DELETE /api/quotes/:quote_id
    Response Header
    HTTP/1.1 200 OK 

    To make a quote public:

    Request
    PUT /api/quotes/:quote_id/publicize

    Once a quote is public, it cannot be edited and others will be able to see, fav it, etc.

    Response Body
    {
      "id": 4,
      "author": "Albert Einstein",
      "body": "Make everything as simple as possible, but not simpler.",
      "private": false
      ...

    Activity

    Get the activity of a user, tag, or author, sorted by recency.

    Request
    GET /api/activities/

    If a user session is present and no parameters are given, the activites for that user will be returned.

    URL Parameters

    ParameterDescription
    filterType lookup
    type['author', 'tag', 'user']
    pagePage number (25 activities per page)
    Response Body
    {
      "activities": [
        {
          "activity_id": 427,
          "owner_type": "User",
          "owner_id": "gose",
          "owner_value": "gose",
          "action": "favorited",
          "trackable_id": 61003,
          "trackable_type": "Quote",
          "trackable_value": "Pleasure in the job puts perfection in the work. - Aristotle",
          "message": "User gose favorited Quote \"Pleasure in the job puts perfection in the work.\" - Aristotle" 
        },
        {
          "activity_id": 426,
          "owner_type": "User",
          "owner_id": "gose",
          "owner_value": "gose",
          "action": "followed",
          "trackable_id": "chicago",
          "trackable_type": "Tag",
          "trackable_value": "chicago",
          "message": "User gose followed Tag chicago"
        },
        ...
      ]
    }

    Examples

    Get the activities of user 'gose':

    GET https://favqs.com/api/activities/?type=user&filter=gose

    Get the activities of author Mark Twain:

    GET https://favqs.com/api/activities/?type=author&filter=Mark+Twain

    Get the activities of tag "funny":

    GET https://favqs.com/api/activities/?type=tag&filter=funny

    To delete an activity entry:

    Request
    DELETE /api/activities/:activity_id

    Successful requests return a 200 OK and empty body.

    To follow a user, tag, or author:

    Request
    PUT /api/activities/follow/

    To unfollow a user, tag, or author:

    Request
    PUT /api/activities/unfollow/

    Required URL Parameters

    ParameterDescription
    filterType lookup
    type['author', 'tag', 'user']

    Successful requests return a 200 OK.

    To get the followers for a user, tag, or author:

    Request
    GET /api/activities/followers/

    Required URL Parameters

    ParameterDescription
    filterType lookup
    type['author', 'tag', 'user']

    For example, get users who follow user 'gose':

    GET https://favqs.com/api/activities/followers?type=user&filter=gose
    Response Body
    {
      "page": 1,
      "last_page": true,
      "users": [
        "david",
        "michael",
        "john"
      ]
    }

    Get users who follow tag 'funny':

    GET https://favqs.com/api/activities/followers?type=tag&filter=funny

    Get users who follow author 'Mark Twain':

    GET https://favqs.com/api/activities/followers?type=author&filter=mark-twain

    To get a list of users, tags, and authors a user is following:

    Request
    GET /api/activities/following/

    Only users can follow authors, tags, and other users.

    Response Body
    {
      "page": 1,
      "last_page": true,
      "following": [
        {
          "following_type": "Author",
          "following_id": "mark-twain",
          "following_value": "Mark Twain"
        },
        {
          "following_type": "Tag",
          "following_id": "society",
          "following_value": "society"
        },
        {
          "following_type": "User",
          "following_id": "david",
          "following_value": "david"
        }
      ]
    }

    Optional URL Parameters

    ParameterDescription
    userUsername

    To see who is following the current user:

    GET https://favqs.com/api/activities/following

    To see who is following user 'gose':

    GET https://favqs.com/api/activities/following/?user=gose

    Themes

    To get the default theme:

    Request
    GET /api/themes/default
    Response
    {
      "theme": {
        "id": 1,
        "name": "Default",
        "font_name": "Helvetica Neue",
        "font_color": "#555555",
        "background_color": "#F5F5F5",
        "accent_color": "#999999",
        "button_color": "#468CC8",
        "show_author": true,
        "show_context": true,
        "show_tags": false,
        "show_source": false,
        "show_quotation_marks": false,
        "presentation_mode": false
      }
    }

    To get a list of themes:

    Request
    GET /api/themes

    URL Parameters

    ParameterDescription
    type['user', 'templates']
    loginUser themes (if public)
    Response Body
    {
      "themes": [
        {
          "id": 100,
          "name": "My Theme",
          "font_name": "Helvetica Neue",
          "font_color": "#555555",
          "background_color": "#F5F5F5",
          "accent_color": "#999999",
          "button_color": "#468CC8",
          "show_author": true,
          "show_context": true,
          "show_tags": false,
          "show_source": false,
          "show_quotation_marks": false,
          "presentation_mode": false
        },
        ...
      ]
    }

    Examples

    Get the themes for the current user:

    GET https://favqs.com/api/themes

    Get the theme templates:

    GET https://favqs.com/api/themes?type=templates

    Get the themes of user 'gose' (if public):

    GET https://favqs.com/api/themes?type=user&login=gose

    Themes are private by default, but a user may make their themes public.

    A user can make their themes public by setting 'public_themes' to true on Update User.

    A user can only apply a Theme if they own it. Otherwise the Theme must be copied first.

    To apply a Theme:

    Request
    PUT /api/themes/:id/apply

    Successful requests return a 200 OK and empty body.

    To clear the selected theme and use the default theme:

    Request
    PUT /api/themes/clear

    Successful requests return a 200 OK and empty body.

    To create a Theme

    Request
    POST /api/themes
    Request Body
    {
      "theme": {
        "name": "My Theme",
        "font_name": "Helvetica Neue",
        "font_color": "#555555",
        "background_color": "#F5F5F5",
        "accent_color": "#999999",
        "button_color": "#468CC8",
        "show_author": true,
        "show_context": true,
        "show_tags": false,
        "show_source": false,
        "show_quotation_marks": false,
        "presentation_mode": false
      }
    }

    The presentation_mode attribute can be used to prevent the display from going to sleep and auto-advancing quotes with a timer.

    Validations
    Font Color
  • Required
  • Must be in HEX format (e.g., #123456)
  • Length must be 7 characters
  • Background Color
  • Required
  • Must be in HEX format (e.g., #123456)
  • Length must be 7 characters
  • Button Color
  • Required
  • Must be in HEX format (e.g., #123456)
  • Length must be 7 characters
  • Accent Color
  • Required
  • Must be in HEX format (e.g., #123456)
  • Length must be 7 characters
  • Response Body

    Successful creation of a theme will return the theme with an id assigned to it.

    {
      "theme": {
        "id": 100,
        "name": "My Theme",
        "font_name": "Helvetica Neue",
        "font_color": "#555555",
        "background_color": "#F5F5F5",
        "accent_color": "#999999",
        "button_color": "#468CC8",
        "show_author": true,
        "show_context": true,
        "show_tags": false,
        "show_source": false,
        "show_quotation_marks": false,
        "presentation_mode": false
      }
    }

    If any of the fields contain validation errors, an error_code will be present and a message will contain a list of validation errors separated by a semi-colon.

    {
      "error_code": 83,
      "message": "Font color can't be blank; Font color is the wrong length (should be 7 characters); Font color must be in HEX format"
    }

    Note:  Only templates and public user themes can by copied.

    To copy a Theme

    Request
    POST /api/themes/:id/copy
    Response Body
    {
      "theme": {
        "name": "Theme Name",
        "font_name": "Helvetica Neue",
        "font_color": "#555555",
        "background_color": "#F5F5F5",
        "accent_color": "#999999",
        "button_color": "#468CC8",
        "show_author": true,
        "show_context": true,
        "show_tags": false,
        "show_source": false,
        "show_quotation_marks": false,
        "presentation_mode": false
      }
    }

    To update a Theme

    Request
    PUT /api/themes/:id
    Response Header

    Successful requests return a 200 OK and empty body.

    HTTP/1.1 200 OK 

    If any of the fields contain validation errors, message will contain a list of validation errors separated by a semi-colon.

    {
      "error_code": 83,
      "message": "Font color can't be blank; Font color is the wrong length (should be 7 characters); Font color must be in HEX format"
    }

    To delete a Theme

    Request
    DELETE /api/themes/:id

    Successful requests return a 200 OK and empty body.


    Errors

    Successful requests return a 200 OK response, and no error_code.

    If your request returns a 200 OK and an error_code, the request could not be completed.

    Response Body
    { 
      "error_code": 21,
      "message": "Invalid login or password."
    }

    An additional message will indicate the reason, but you can present your own message based off the error_code.

    Error Codes
    10Invalid request.
    11Permission denied.
    20User session not found.
    21Invalid login or password.
    22Login is not active. Contact support@favqs.com.
    23User login or password is missing.
    24Pro user required
    30User not found.
    31User session already present.
    32A list of 'user' validation errors.
    33Invalid password reset token.
    40Quote not found.
    41Private quotes cannot be unfav'd.
    42Could not create quote.
    50Author not found.
    60Tag not found.
    70Activity not found.
    80Theme not found.
    81Themes not public.
    82Theme must be copied first.
    83A list of 'theme' validation errors.

    Additional error scenarios include non-200 codes in the response header.

    404HTTP/1.1 404 Not Found
    500HTTP/1.1 500 Internal Server Error

    Request API Key

    By using the FavQs API, you agree to our:

    Follow these steps to request access:

    1. Sign up for an account
    2. Visit your API Keys page