Get the outline in a space (public reader)

Embed
Preview of the card
<a href="https://forumulate.com/reference/forumulate-api-coursepublic-outline"><img src="https://forumulate.com/reference/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/8042f004-e3d9-445f-b7a4-b6c1f86836e6/pages/366e144d-e42d-4f02-b742-de7187009c74/social-card.svg?v=3e7ae33eec8b" alt="Get the outline in a space (public reader)" width="600" height="315"></a>
GEThttps://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline
Parameters
orgIdstringpathrequired
spaceIdstringpathrequired
forumulate-versionstringheader
API version negotiation. Send forumulate-version: 2026-12-01 on every request from a new integration; it is optional only so that integrations written before versioning keep working. Each version includes everything before it:
Code samples
cURL
curl -X GET "https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline" \
  -H "forumulate-version: <value>"
TypeScript
const res = await fetch('https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline', {
  method: 'GET',
  headers: {
    'forumulate-version': '<value>',
  },
});
const data = await res.json();
Python
import requests

res = requests.request(
    "GET",
    "https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline",
    headers={
    "forumulate-version": "<value>",
    },
)
data = res.json()
Go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline", nil)
	req.Header.Set("forumulate-version", "<value>")
	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(string(body))
}
PHP
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['forumulate-version: <value>']);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Ruby
require 'net/http'
require 'uri'

uri = URI('https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline')
req = Net::HTTP::Get.new(uri)
req['forumulate-version'] = '<value>'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(req)
end
puts res.body
Java
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublishers;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://forumulate.com/api/public/orgs/{orgId}/spaces/{spaceId}/course/outline"))
    .header("forumulate-version", "<value>")
    .GET()
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Responses
200 Success.
Example
{
  "access": {
    "audience": "everyone",
    "drip": [
      {
        "afterDays": 0,
        "module": "string",
        "on": "string"
      }
    ],
    "order": "free",
    "requirePrerequisites": true
  },
  "commerce": {
    "coupons": true,
    "currency": "aud",
    "paid": true,
    "priceCents": 0,
    "refundDays": 0,
    "refundText": "string"
  },
  "course": {
    "cohort": {
      "events": null,
      "sessions": [
        null
      ],
      "startsOn": "string"
    },
    "coverAlt": "string",
    "coverUrl": "string",
    "ctaLabel": "string",
    "durationMinutes": 0,
    "durationSource": "lessons",
    "faq": [
      {
        "answer": "string",
        "question": "string"
      }
    ],
    "instructors": [
      {
        "avatarUrl": null,
        "key": "string",
        "name": "string",
        "role": null,
        "url": null
      }
    ],
    "language": "string",
    "lessonCount": 0,
    "level": "advanced",
    "outcomes": [
      "string"
    ],
    "prerequisites": [
      {
        "spaceId": "string",
        "text": "string",
        "title": "string"
      }
    ],
    "showUpdated": true,
    "subtitle": "string",
    "testimonials": [
      {
        "avatarUrl": "string",
        "name": "string",
        "quote": "string",
        "role": "string"
      }
    ],
    "title": "string",
    "topics": [
      "string"
    ],
    "updatedAt": "string"
  },
  "enrolment": {
    "completedAt": "string",
    "lastLesson": {
      "slug": "string",
      "title": "string"
    },
    "lastSeenAt": "string",
    "startedAt": "string"
  },
  "modules": [
    {
      "id": "string",
      "isDefault": true,
      "lessons": [
        {
          "completed": null,
          "duration": null,
          "freePreview": null,
          "kind": null,
          "locked": null,
          "pageId": null,
          "quiz": null,
          "slug": null,
          "title": null
        }
      ],
      "name": "string",
      "quiz": {
        "passed": true,
        "passingScore": 0,
        "quizId": "string",
        "title": "string"
      },
      "slug": "string",
      "translations": {},
      "week": {
        "endsOn": "string",
        "n": 0,
        "startsOn": "string"
      }
    }
  ],
  "order": [
    {
      "slug": "string",
      "title": "string"
    }
  ],
  "progress": {
    "completed": 0,
    "percent": 0,
    "total": 0
  }
}
Schema
accessobjectrequired
Show child parameters
audiencestringrequired
everyone | groups | signedIn
dripobject[]required
Show child parameters
afterDaysnumber
modulestringrequired
onstring
orderstringrequired
free | linear
requirePrerequisitesbooleanrequired
commerceobjectrequired
Show child parameters
couponsbooleanrequired
currencystringrequired
aud | brl | cad | chf | dkk | eur | gbp | inr | jpy | mxn | nok | nzd | pln | sek | sgd | usd
paidbooleanrequired
priceCentsnumberrequired
refundDaysnumberrequired
refundTextstringrequired
courseobjectrequired
Show child parameters
cohortCourseCohortViewrequired
Show child parameters
eventsobjectrequired
sessionsobject[]required
startsOnstringrequired
coverAltstringrequired
coverUrlstringrequired
ctaLabelstringrequired
durationMinutesnumberrequired
durationSourcestringrequired
lessons | owner
faqobject[]required
Show child parameters
answerstringrequired
questionstringrequired
instructorsCourseInstructor[]required
Show child parameters
avatarUrlstringrequired
keystringrequired
namestringrequired
rolestringrequired
urlstringrequired
languagestringrequired
lessonCountnumberrequired
levelstringrequired
advanced | all | beginner | intermediate
outcomesstring[]required
prerequisitesCoursePrerequisite[]required
Show child parameters
spaceIdstring
textstringrequired
titlestring
showUpdatedbooleanrequired
subtitlestringrequired
testimonialsobject[]required
Show child parameters
avatarUrlstring
namestringrequired
quotestringrequired
rolestring
titlestringrequired
topicsstring[]required
updatedAtstringrequired
enrolmentobject
Show child parameters
completedAtstringrequired
lastLessonobjectrequired
Show child parameters
slugstringrequired
titlestringrequired
lastSeenAtstringrequired
startedAtstringrequired
modulesobject[]required
Show child parameters
idstringrequired
isDefaultbooleanrequired
lessonsobject[]required
Show child parameters
completedbooleanrequired
durationnumber
freePreviewboolean
kindstring
lockedobject
pageIdstringrequired
quizobject
slugstringrequired
titlestringrequired
namestringrequired
quizobject
Show child parameters
passedbooleanrequired
passingScorenumberrequired
quizIdstringrequired
titlestringrequired
slugstringrequired
translationsobject
weekobject
Show child parameters
endsOnstringrequired
nnumberrequired
startsOnstringrequired
orderobject[]required
Show child parameters
slugstringrequired
titlestringrequired
progressobjectrequired
Show child parameters
completednumberrequired
percentnumberrequired
totalnumberrequired
400 The request could not be understood — a malformed header, path or query value (request.malformed). A value that was understood but is not acceptable is a 422 validation.failed.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
404 No such thing — one of the ids in the path does not resolve, or resolves to something this caller may not see (*.not_found).
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
429 Rate limited — see the RateLimit-* headers.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
default Error — the stable { error: { code, message } } envelope.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
The Forumulate teamUpdated