API v1 → v2 Migration Guide

ReadMe API v2 is the best way to programmatically access your ReadMe data once you have migrated to ReadMe Refactored. We’re giving you access to the very same API that ReadMe’s developers use to build ReadMe. Let’s try it out.

Overview of Key Changes

Below is a quick summary of the major updates to transition from API v1 to v2. For a deeper dive into each change, refer to the relevant sections below.

API v1API v2
API endpointsUpdated URLs, new endpoints, and redesigned responses.
AuthenticationBasic AuthenticationBearer Authentication
Hostnamehttps://dash.readme.com/api/v1https://api.readme.com/v2
VersionsRenamed to Branches
Project VersionSupplied a x-readme-version header.Supply the branch (previously version) in your API URL.
Using Default VersionOmit the x-readme-version headerUse stable in your API URL.
Flag Valuesbooleansenums
Resource IdentifiersHexadecimal IDsSlugs and URIs
Response Body ShapescamelCasesnake_case
PaginationLink headerCollection format

💡 Make your first API v2 Request

We’re going to use cURL in this example, however any HTTP client should work too. Let’s start with a request to get details about your project.

curl -X GET https://api.readme.com/v2/projects/me --header "Authorization: Bearer {your-api-key}"

In the Authorization header you’ll need to add an API key. Learn more about ReadMe API keys here.

When you run this cURL command, you will see JSON with details about your project. If that’s the case, you’ve successfully made your first API v2 request. If not, contact support and we’ll help you figure it out!

Most aspects of this cURL command can be reused for your other API requests; here are the most important points:

  • https://api.readme.com/v2/ is the hostname for every API call. You can place it into a constant for reuse across all of your API requests.
  • The Authorization header will always be how you provide your API key. The value must include the string Bearer, then a space, then your API key.
  • The GET HTTP method returns data, but will never change data.
  • The POST HTTP method requests generally create data (such as guides or custom pages), but are also used for more complex requests like search.
  • The PATCH HTTP method is used for editing data that already exists.
  • The PUT HTTP method is used for creating data (if you have the URI) or replacing data that already exists.
  • The DELETE HTTP method is used for deleting data that already exists.

‼️ Major changes

Authentication

As mentioned previously, authentication has changed:

ReadMe API v1 previously used Basic Authentication and has now moved to Bearer Authentication. Here’s what a cURL request looked like previously in API v1:

curl -X GET https://dash.readme.com/api/v1 --user "{your-api-key}:"

That same request with API v2:

curl -X GET https://api.readme.com/v2/projects/me --header "Authorization: Bearer {your-api-key}"

Hostname

As mentioned previously, the hostname has changed:

Versions

Across API v2, we have reworked our API URIs from being centered around "versions" to "branches" as part of our support for branching within ReadMe Refactored. All branching APIs we offer within API v2 support both versions and feature branches.

Though the recommended API v2 URIs use /branches/{branch}, we still do support accessing these endpoints via /versions/{version} or /versions/{branch} for backwards compatibility reasons. If you use the latter, we emit a Warning header with a 199 code informing you that they have been deprecated.

Specifying Project Version

The project version has moved from ax-readme-version header into the URL:

  • As with API v1, you can pass the version with or without the v prefix (e.g., GET https://api.readme.com/v2/branches/1.0/custom_pages or GET https://api.readme.com/v2/branches/v1.0/custom_pages).
  • Previously, if you wanted the default version (a.k.a. stable version) you could leave the header out. With the new API, you can use the word stable (e.g., GET https://api.readme.com/v2/branches/stable/custom_pages).

Flag Values

Nearly all flags that were previously delineated with boolean values (e.g., true or false) have now been moved to enum fields (e.g., enabled or disabled). The API reference docs for a given endpoint and the fields within will contain detailed information on what the possible enum values are.

The reason for this change is that we previously ran into issues where, for instance, we needed to introduce an additional state beyond true and false. Enums provide us with additional overhead for future enhancements and more descriptive states (e.g., enabled vs. disabled, public vs. anyone_with_link, etc.), while maintaining a nearly identical API reference experience.

Resource Identifiers

Resources are identified via URIs and slugs:

We’ve heard your feedback that using hexadecimal object IDs (e.g., 5f92cbf10cf217478ba93561) in API v1 to identify resources (e.g., API definitions, categories, etc.) makes it difficult to build scalable integrations (e.g., integrations that might update many API definitions at once across several project versions)so we have migrated to using slugs and URIs as our primary resource identifiers.

For example, to retrieve all of the API schemas for a given project version, you’d make a request to GET /v2/branches/{branch}/apis and your response may look something like this:

{
  "total": 1,
  "data": [
    {
      ...
      "created_at": "2025-01-27T16:33:27.193Z",
      "type": "openapi",
      "upload": {
        "status": "done"
      },
      "uri": "/branches/1.0/apis/petstore.json",
    }
  ]
}

For this API definition resource, the URI is /branches/1.0/apis/petstore.json and the slug is petstore.json. You can use the URI and slug for this API definition to make reads and writes to that resource. For example, you can make the following API request to retrieve information about your petstore.json API definition:

curl -X GET https://api.readme.com/v2/branches/1.0/apis/petstore.json --header "Authorization: Bearer {your-api-key}"

Response Body Shapes

The response body shape has changed:

We now offer consistent snake_case field names and have structured the responses in anticipation of growing them to include more useful data over time by nesting all objects within a top-level data property. If you are retrieving a collection of results, data will be an array of objects. If you are retrieving a single resource, data will be an object.

Here’s what a response from our custom pages API formerly looked like with API v1:

[
  {
    "metadata": {
      "image": [],
      "title": "",
      "description": "",
      "keywords": ""
    },
    "mdx": {
      "altBody": "",
      "status": "rdmd"
    },
    "algolia": {
      "recordCount": 1,
      "publishPending": false
    },
    "title": "Welcome to the API",
    "slug": "welcome-to-the-api",
    "body": "We’ve got endpoints galore",
    "html": "",
    "htmlmode": false,
    "fullscreen": false,
    "hidden": true,
    "revision": 2,
    "_id": "673974422297a9aa6fc28b2b",
    "createdAt": "2025-01-16T21:12:34.267Z",
    "updatedAt": "2025-01-16T21:12:34.267Z",
    "lastUpdatedHash": "1608f5b78bac84444acc76ce134667c318bbc6ee",
    "__v": 0
  }
]

That same request under API v2:

{
  "total": 1,
  "data": [
    {
      "appearance": {
        "fullscreen": false
      },
      "content": {
        "body": "We’ve got endpoints galore",
        "type": "markdown"
      },
      "metadata": {
        "description": null,
        "image": {
          "uri": null,
          "url": null
        },
        "keywords": null,
        "title": null
      },
      "privacy": {
        "view": "public"
      },
      "slug": "welcome-to-the-api",
      "title": "Welcome to the API",
      "updated_at": "2025-01-16T21:15:53.484Z",
      "links": {
        "project": "/projects/me"
      },
      "uri": "/branches/1.0/custom_pages/welcome-to-the-api"
    }
  ]
}

Pagination

Pagination has changed:

In API v2, we no longer use the Link header that we used in API v1. In API v2, we have a new "Collection" format that includes pagination metadata alongside the data you requested. Here’s an example:

{
  "total": 5,
  "page": 2,
  "per_page": 2,
  "paging": {
    "next": "/images?page=3&per_page=2",
    "previous": "/images?page=1&per_page=2",
    "first": "/images?page=1&per_page=2",
    "last": "/images?page=3&per_page=2"
  },
  "data": [
    {
      /* image object */
    },
    {
      /* image object */
    }
  ]
}

total, page and per_page (previously perPage) behave like they did in API v1, giving you all the important quantity-related metadata. paging is where it gets interesting. This object contains four URIs that are the API calls you can make to get additional pages of content:

  • next is the page of content immediately following the current page. If the current page is the last page, this will be null.
  • previous is the page of content immediately before the current page. If the current page is the first page, this will be null.
  • first is the very first page of content. This will always have a value.
  • last is the very last page of content. This will always have a value.

Note: These pagination links will retain the query parameters used in your API call. In fact, you shouldn’t need to modify the pagination URLs in any way!

🆕 API Endpoint and Response changes

While the URLs for every API endpoint have changed, we worked to make them intuitive to use. Let’s walk through an example below.

Here’s a comparison of the old and new endpoints to get a list of all of your custom pages:

API v1:

GET https://dash.readme.com/api/v1/custompages

API v2:

GET https://api.readme.com/v2/branches/{branch}/custom_pages

API Registry

API v1DocumentationAPI v2Documentation
GET /api-registry/{uuid}Retrieve an entry from the API Registry🚫This endpoint does not currently exist on API v2. Because it does not require authentication credentials you can continue to use the endpoint in API v1.

API Specification

API v1DocumentationAPI v2Documentation
GET /api-specificationGet metadataGET /branches/{branch}/apisGet all API definitions
POST /api-specificationUpload specificationPOST /branches/{branch}/apisCreate an API definition
PUT /api-specification/{id}Update specificationPUT /branches/{branch}/apis/{filename}Update an API definition
DELETE /api-specification/{id}Delete specificationDELETE /branches/{branch}/apis/{filename}Delete an API definition
POST /api-validationValidate API specificationPOST /validate/apiValidate an API
GET schemaGet our OpenAPI Definition🚫This endpoint does not yet exist on API v2. If you need an up-to-date copy of our OpenAPI definition you can grab it from https://docs.readme.com/main/openapi.

Changes to how we process API definition uploads

To better support larger and more complex API definitions, we have moved API definition upload processing into a queue-based system. What this means for you is that now, when you upload a new API definition with POST /branches/{branch}/apis or update an existing with PUT /branches/{branch}/apis/{filename}, your upload will not be immediately processed.

Instead, we now surface an upload.status property on the response of these APIs to inform you where in our processing pipeline your API is. You can poll this field via GET /branches/{branch}/apis/{filename}, which our POST and PUT endpoints conveniently return for you in a uri property. The possible values of upload.status are the following:

  • pending
  • failed
  • done
  • pending_update
  • failed_update

If you ever receive failed or failed_update it means that there was an issue processing the API, usually the result of an OpenAPI validation error, and you can see the reason for the failure in upload.reason. When you receive done, however, your API has finished processing, and any new or changed API reference pages from it are immediately available on your ReadMe project!

Response schema changes

API v1API v2Notes
iduriWe have dropped support for our legacy hexadecimal IDs in favor of the unique resource identifier of a filename path parameter (for the PUT /branches/{branch}/apis/{filename} and DELETE /branches/{branch}/apis/{filename} endpoints). When creating an API, you can specify the filename within the multipart/form-data payload.

In response payloads, the uri will contain the unique resource identifier.
sourcesource.current
source.original
Our source object now informs you of the current, and original, source from which this API was ingested. source.original is immutable from the first upload, but source.current may change from subsequent uploads if you use alternative upload methods.

These values remain largely unchanged except for cli mapping to rdme and cli-gh mapping to rdme_github.
typetypeIn addition to our previously supported openapi and swagger values, dictating the type of API this is, we have added support for postman. Though we convert Postman API schemas to OpenAPI on ingestion, this will be set to postman if that conversion took place.

We have also added a new unknown type for when an invalid API definition is uploaded. See our above documentation for the changes we’ve made to the API definition upload flow for more information.
version(moved to path parameter)Because our API URIs are now centered around supplying a version or branch directly in the API URI via a path parameter, we have dropped support for this value in the request/response body because this value would be identical to what you supplied.

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

Categories

API v1DocumentationAPI v2Documentation
GET /categoriesGet all categoriesGET /branches/{branch}/categories/{section}Get all categories
POST /categoriesCreate categoryPOST /branches/{branch}/categoriesCreate a category
GET /categories/{slug}Get categoryGET /branches/{branch}/categories/{section}Get a category
PUT /categories/{slug}Update categoryPATCH /branches/{branch}/categories/{section}/{title}Update a category
DELETE /categories/{slug}Delete categoryDELETE /branches/{branch}/categories/{section}/{title}Delete a category
GET /categories/{slug}/docsGet docs for categoryGET /branches/{branch}/categories/{section}/{title}/pagesGet the pages within a category

Response schema changes

API v1API v2Notes
orderpositionWe do not currently return position in category responses. However, if you know the position you wish to set a category at in your sidebar, you can supply position to either the POST or PATCH endpoints.
idtitleWe have dropped support for our legacy hexadecimal IDs in favor of the unique resource identifier of a title.
projectlinks.projectThis was previously a hexadecimal ID for the project attached to the page. We have replaced this with a links.project URI to our GET /projects/me endpoint where you can obtain additional information.
slugtitlePreviously, there was no difference in API v1 between slug and title, so we have dropped slug in favor of the latter.
titletitle
typesectionThis has been removed in favor of supplying a section path parameter to the category APIs.
version(moved to path parameter)Because our API URIs are now centered around supplying a version or branch directly in the API URI via a path parameter, we have dropped support for this value in the request/response body because this value would be identical to what you supplied.

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

Changelog

API v1DocumentationAPI v2Documentation
GET /changelogsGet changelogsGET /changelogsGet all changelog entries
POST /changelogsCreate changelogPOST /changelogsCreate a changelog entry
GET /changelogs/{slug}Get changelogGET /changelogs/{identifier}Get a changelog entry
PUT /changelogs/{slug}Update changelogPATCH /changelogs/{identifier}Update a changelog entry
DELETE /changelogs/{slug}Delete changelogDELETE /changelogs/{identifier}Delete a changelog entry

Response schema changes

API v1API v2Notes
bodycontent.body
createdAtcreated_at
hiddenprivacy.viewIf privacy.view is anyone_with_link, the page is hidden.
metadata.descriptionmetadata.description
metadata.imagemetadata.imageThis previously returned an array of various pieces of your image, like the URL and colors that are used in it. For the image URL, you can now access this via metadata.image.url. For the additional metadata that was originally part of this array, you can now access that data by making a request to the GET /image/{identifier} endpoint — the URI of which is available through the metadata.image.uri response parameter.
metadata.keywordsmetadata.keywordsThis was previously a comma-separated list. It is now an array of strings.
metadata.titlemetadata.title
slugslug
titletitle
typetypeWith the exception of now returning none instead of an empty string when a changelog has no type, type remains unchanged.
updatedAtupdated_at

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

Custom Pages

API v1DocumentationAPI v2Documentation
GET /custompagesGet custom pagesGET /branches/{branch}/custom_pagesCreate a custom page
POST /custompagesCreate custom pagePOST /branches/{branch}/custom_pagesGet all custom pages
GET /custompages/{slug}Get custom pageGET /branches/{branch}/custom_pages/{slug}Get a custom page
PUT /custompages/{slug}Update custom pagePATCH /branches/{branch}/custom_pages/{slug}Update a custom page
DELETE /custompages/{slug}Delete custom pageDELETE /branches/{branch}/custom_pages/{slug}Delete a custom page

Response schema changes

API v1API v2Notes
fullscreenappearance.fullscreen
bodycontent.bodyWe previously had this data split across two different properties of body and html. If body has content and html is empty, then your custom page is pure Markdown. If it had html but no body, then it was an HTML custom page. We have merged these two into a single content.body property and given you a discriminator of content.type that you can use to infer the type of content this is.
hiddenprivacy.viewIf privacy.view is anyone_with_link, then the page is hidden.
htmlcontent.bodySee the note in body for how to interpret this data now.
htmlmodecontent.typeIf htmlmode was previously true, then content.type is html. The default type is markdown, meaning your custom page is pure Markdown.
metadata.descriptionmetadata.description
metadata.imagemetadata.imageThis previously returned an array of various pieces of your image, like the URL and colors that are used in it. For the image URL, you can now access this via metadata.image.url. For the additional metadata that was originally part of this array, you can now access that data by making a request to the GET /image/{identifier} endpoint — the URI of which is available through the metadata.image.uri response parameter.
metadata.keywordsmetadata.keywordsThis was previously a comma-separated list. It is now an array of strings.
metadata.titlemetadata.title
slugslug
titletitle
updatedAtupdated_at

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

Docs

Previously, all of our guides and API reference APIs had a resource identifier of a hexadecimal ID with a single endpoint to handle both page types. This has changed with APIv2, where we have reshaped their responses, not only because guides and references have independent sets of identifiers, but also to allow for future expansion (e.g., potentially returning the referenced OpenAPI definition when retrieving an API reference).

Functionally, these two APIs are identical, and the /reference/ family of endpoints includes some additional data.

API v1DocumentationAPI v2Documentation
POST docsCreate docPOST /branches/{branch}/guides
POST /branches/{branch}/reference
Create a guides page
Create a reference page
POST /docs/searchSearch docsGET /searchPerform a search query
GET /docs/{slug}Get docGET /branches/{branch}/guides/{slug}
GET /branches/{branch}/reference/{slug}
Get a guides page
Get a reference page
PUT /docs/{slug}Update DocPATCH /branches/{branch}/guides/{slug}
PATCH /branches/{branch}/reference/{slug}
Update a guides page
Update a reference page
DELETE /docs/{slug}Delete docDELETE /branches/{branch}/guides/{slug}
DELETE /branches/{branch}/reference/{slug}
Delete a guides page
Delete a reference page
GET /docs/{slug}/productionGet production doc🚫With the release of ReadMe Refactored and branching, we have begun to move away from our existing staging architecture. As such, this endpoint no longer exists within API v2. To retrieve a "staged" copy of a guide or reference, you can access it from a specific branch.
† Though we have differing APIs for guides and API reference pages, our GET /search endpoint handles all forms of content in your ReadMe Project: guides, references, recipes, discussions, etc.

Response schema changes

Unless explicitly noted otherwise, all response schema changes documented here apply to both the Guide and API Reference family of endpoints.

API v1API v2Notes
api.methodapi.methodOnly available on the API Reference endpoints.
api.urlapi.pathOnly available on the API Reference endpoints.
bodycontent.body
categorycategory.uriThis was previously a hexadecimal ID to your category, we have revised this to be an API v2 URI to the GET /branches/{branch}/categories endpoint.
deprecatedstateIf state is deprecated.
excerptcontent.excerpt
hiddenprivacy.viewIf privacy.view is anyone_with_link then the page is hidden.
iconappearance.icon
idtitleWe have dropped support for our legacy hexadecimal IDs in favor of the unique resource identifier of a slug.
isApitypeIf type is endpoint.
isReferenceThis has been removed in favor of splitting our Guide and API Reference APIs apart.
link_externallink.new_tab
link_urllink.url
metadata.descriptionmetadata.description
metadata.imagemetadata.imageThis previously returned an array of various pieces of your image, like the URL and colors that are used in it. For the image URL you can now access this via metadata.image.url. For the additional metadata that was originally part of this array, you can now access that data by making a request to the GET /image/{identifier} endpoint — the URI of which is available through the metadata.image.uri response parameter.
metadata.keywordsmetadata.keywordsThis was previously a comma-separated list. It is now an array of strings.
metadata.robotsallow_crawlersThe index value of metadata.robots now equates to if allow_crawlers is enabled, noindex is now disabled.
metadata.titlemetadata.title
next.descriptionnext.description
next.pagesnext.pages
projectproject.*This was previously a hexadecimal ID to the project attached to the page. We have replaced this with a project object that contains various data about the project in question, including an API v2 URI to our GET /projects/me endpoint where you can obtain additional information.
slugslug
titletitle
typetypeWe have deprecated and removed support for fn, error, and page types, but all other page types have remained unchanged.
updatedAtupdated_at
version(moved to path parameter)Because our API URIs are now centered around supplying a version or branch directly in the API URI via a path parameter, we have dropped support for this value in the request/response body because this value would be identical to what you supplied.

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

IP Addresses

API v1DocumentationAPI v2Documentation
GET /outbound-ipsGet ReadMe’s outbound IP addressesGET /outbound_ipsGet ReadMe’s outbound IP addresses

Response schema changes

API v1API v2Notes
ipAddressip_address

Owlbot AI

API v1DocumentationAPI v2Documentation
POST /owlbot/askAsk Owlbot AI a questionPOST /owlbot/askAsk Owlbot AI a question

Response schema changes

Other than the aforementioned core response restructuring of resources being returned within a data object nothing has changed here!

Projects

API v1DocumentationAPI v2Documentation
GET /Get metadata about the current projectGET /projects/meGet project metadata

Response schema changes

API v1API v2Notes
jwt_secretcustom_login.jwt_secret
namename
planplan.type
subdomainsubdomain

If you do not see a piece of data listed here, then we no longer support it in API v2. Are we missing something you relied on? Give us a shout!

Version

As mentioned above, our version APIs have been reframed around the branching functionality now available within ReadMe Refactored. Versions and branches off of those versions are accessed through the same API, just supply the name of that version (e.g., 2.0) or the branch (e.g., 2.0_cool-dogs) within the endpoint URI. You can key off of the type property, version or branch, from the response to know what kind of data you have.

API v1DocumentationAPI v2Documentation
GET /versionGet versionsGET /branchesGet branches
POST /versionCreate versionPOST branchesCreate a branch
GET /version/{versionId}Get versionGET /branches/{branch}Get a branch
PUT /version/{versionId}Update versionPATCH /branches/{branch}Updates an existing branch
DELETE /version/{versionId}Delete versionPATCH /branches/{branch}Delete a branch

Response schema changes

API v1API v2Notes
codenamedisplay_name
forked_frombaseforked_from was previously a hexadecimal ID for the base version. It is now just the name of the base version or branch. (e.g., 2.0 instead of 685345e07d0542c58d8d4cce)
is_betarelease_stageIf release_stage is beta.
is_deprecatedstateIf state is deprecated.
is_hiddenprivacy.viewIf privacy.view is hidden.
is_stablerelease_stageIf release_stage is default.
versionname

If you do not see a piece of data listed here, then we no longer support it in APIv2. Are we missing something you relied on? Give us a shout!