Help center

Create an SOP

Reference for POST /sops, which turns a title plus Markdown or plain text into a new SOP with one card per heading.

Business plan Updated
On this page
  1. Body parameters
  2. Limits
  3. Request
  4. Response
  5. How the new SOP appears in the app
  6. Markdown support
  7. How sections become cards
  8. Supported syntax
  9. What is sanitized
  10. Plain text
  11. Errors
Shell
POST https://app.sopcompass.com/api/v1/sops

Scope: read and write. Content-Type: application/json.

Creates an SOP in one of the workspace’s own libraries from a title plus either markdown or text. Returns 201 Created with the new SOP in full, exactly as Get an SOP returns it.

Body parameters

Name Type Required Description
library_id UUID Yes A library of this workspace, from GET /libraries where shared is false.
title string Yes 1 to 200 characters after trimming spaces.
markdown string One of markdown or text Markdown. Each # or ## heading starts a new card. See Markdown support.
text string One of markdown or text Plain text, as a single card called “Overview”. Blank lines separate paragraphs; single line breaks are kept.
description string or null No The SOP’s short description, up to 2,000 characters.

Send exactly one of markdown and text, as a non-empty string. Unknown fields are ignored; tags, owner, departments and review date can’t be set through the API in v1.

Limits

  • markdown or text: at most 500,000 characters.
  • At most 50 cards (sections).
  • At most about 1 MB of card content once converted to HTML.

Request

Shell
curl https://app.sopcompass.com/api/v1/sops \
  -H "Authorization: Bearer $SOPCOMPASS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "library_id": "4a6c8e0b-2d4f-4a1c-8e3a-5c7e9b1d3f07",
    "title": "Close a client site for the night",
    "description": "Evening close for Acme Cleaning Co. (example) crews.",
    "markdown": "Do this after the last clean of the day.\n\n## Equipment\n\n1. Empty and rinse the mop buckets\n2. Plug in the floor scrubber\n\n## Lock up\n\n- Check every window\n- Set the alarm, then lock the **front door** last\n- Log the time in [the site sheet](https://example.com/site-sheet)"
  }'
JavaScript
const res = await fetch("https://app.sopcompass.com/api/v1/sops", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SOPCOMPASS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    library_id: "4a6c8e0b-2d4f-4a1c-8e3a-5c7e9b1d3f07",
    title: "Close a client site for the night",
    description: "Evening close for Acme Cleaning Co. (example) crews.",
    markdown: [
      "Do this after the last clean of the day.",
      "",
      "## Equipment",
      "",
      "1. Empty and rinse the mop buckets",
      "2. Plug in the floor scrubber",
      "",
      "## Lock up",
      "",
      "- Check every window",
      "- Set the alarm, then lock the **front door** last",
      "- Log the time in [the site sheet](https://example.com/site-sheet)",
    ].join("\n"),
  }),
});
const { data: sop } = await res.json();
console.log(res.status, sop.app_url);
Python
import os
import requests

markdown = """Do this after the last clean of the day.

## Equipment

1. Empty and rinse the mop buckets
2. Plug in the floor scrubber

## Lock up

- Check every window
- Set the alarm, then lock the **front door** last
- Log the time in [the site sheet](https://example.com/site-sheet)
"""

res = requests.post(
    "https://app.sopcompass.com/api/v1/sops",
    headers={"Authorization": f"Bearer {os.environ['SOPCOMPASS_API_KEY']}"},
    json={
        "library_id": "4a6c8e0b-2d4f-4a1c-8e3a-5c7e9b1d3f07",
        "title": "Close a client site for the night",
        "description": "Evening close for Acme Cleaning Co. (example) crews.",
        "markdown": markdown,
    },
    timeout=30,
)
sop = res.json()["data"]

requests sets Content-Type: application/json for you when you pass json=.

Response

201 Created

JSON
{
  "data": {
    "id": "f8c0e2a4-6b8d-4c0e-8a2f-6b8d0f2c4e13",
    "title": "Close a client site for the night",
    "description": "Evening close for Acme Cleaning Co. (example) crews.",
    "library_id": "4a6c8e0b-2d4f-4a1c-8e3a-5c7e9b1d3f07",
    "library_title": "Client sites",
    "board_id": "1b3d5f7a-9c2e-4a6b-8d0f-2a4c6e8b0d14",
    "board_name": "Fulfillment and delivery",
    "tags": [],
    "departments": ["operations"],
    "owner_email": null,
    "published": false,
    "shared": false,
    "next_review_on": null,
    "archived_at": null,
    "created_at": "2026-09-19T16:18:30.277161Z",
    "updated_at": "2026-09-19T16:18:30.277161Z",
    "cards": [
      {
        "id": "a1c3e5f7-2b4d-4f6a-8c1e-3b5d7f9a1c17",
        "title": "Overview",
        "position": 0,
        "body_html": "<p>Do this after the last clean of the day.</p>",
        "steps": [],
        "image_urls": []
      },
      {
        "id": "b2d4f6a8-3c5e-4a7b-9d2f-4c6e8a0b2d18",
        "title": "Equipment",
        "position": 1,
        "body_html": "<ol><li><p>Empty and rinse the mop buckets</p></li><li><p>Plug in the floor scrubber</p></li></ol>",
        "steps": [],
        "image_urls": []
      },
      {
        "id": "c3e5a7b9-4d6f-4b8c-8e3a-5d7f9b1c3e19",
        "title": "Lock up",
        "position": 2,
        "body_html": "<ul><li><p>Check every window</p></li><li><p>Set the alarm, then lock the <strong>front door</strong> last</p></li><li><p>Log the time in <a href=\"https://example.com/site-sheet\" target=\"_blank\" rel=\"noopener noreferrer\">the site sheet</a></p></li></ul>",
        "steps": [],
        "image_urls": []
      }
    ],
    "app_url": "https://app.sopcompass.com/sops/f8c0e2a4-6b8d-4c0e-8a2f-6b8d0f2c4e13?t=7c2e4a6b-8d1f-4a3c-9e5b-0d2f4a6c8e04"
  }
}

Content is created as cards with HTML bodies; the API doesn’t create steps, so steps is always [] on a new SOP. departments shows the library’s default departments.

How the new SOP appears in the app

  • It is added at the end of the chosen library and is visible right away to everyone in the workspace who can see that library, like any other SOP. It also appears in search in the app and in GET /search.
  • It is returned with "published": false. That flag marks where the SOP came from; the app doesn’t currently show a draft badge or hide unpublished SOPs, so don’t rely on it to keep content private. If the content needs review before your team sees it, create it in a library only reviewers can access, or review it before you send it.
  • Its Version history starts with a first version marked “created through the API”.
  • It has no owner, tags or review date, and isn’t assigned to anyone. Set those in the app, or assign it with POST /assignments.
  • Nothing is emailed to anyone.

Markdown support

How sections become cards

  • A line starting with # or ## followed by a space starts a new card, and its text becomes the card’s title (trimmed, up to 200 characters). Closing # characters are removed.
  • Text before the first such heading becomes a card titled “Overview”. If there is none, there is no Overview card.
  • A heading with nothing under it before the next heading is dropped. Put content under every heading.
  • Headings inside fenced code blocks are left alone.

Supported syntax

Markdown Becomes
### Heading <h3>. ####, ##### and ###### all become <h4>.
Lines of text A paragraph. Consecutive lines are joined with a space; a blank line starts a new paragraph.
- item, * item, + item A bulleted list.
1. item or 1) item A numbered list. Numbering always starts at 1.
- [ ] item, - [x] item A list item starting with ☐ or ☑.
A line indented 2 or more spaces under a list item Continues that item’s text.
> quote A block quote. It can contain paragraphs, lists and the other blocks.
``` or ~~~ fences A code block. The language tag is ignored.
---, *** or ___ on its own line, after a blank line A horizontal rule.
**bold** or __bold__ Bold.
*italic* or _italic_ Italic.
`code` Inline code.
[label](https://...) A link that opens in a new tab. http, https and mailto URLs only.
![alt](https://...) An image. http and https URLs only. The image stays at that URL; it isn’t copied into SOPCompass.

Not supported, and shown as plain text: tables, nested lists (indented items become items of the same list), underlined (=== or ---) headings, reference-style links, bare URLs as links, strikethrough and footnotes.

What is sanitized

  • All HTML in your input is escaped, so <script>, <iframe>, <b> and every other tag appear as literal text on the card, never as markup. The only HTML in the result is what the converter itself writes for the syntax above.
  • A link or image whose URL isn’t http or https (or mailto for links), such as javascript: or data:, is left as the original Markdown text.
  • The app sanitizes card HTML again when it displays it, so this is the first of two layers.

Plain text

With text, the whole input becomes one card titled “Overview”. Paragraphs are split on blank lines, single line breaks become <br>, and everything is escaped: Markdown syntax is shown literally.

Errors

Status Code When
400 invalid_json The body isn’t a JSON object.
403 read_only_key The key is read only.
403 workspace_read_only The workspace’s subscription has ended.
403 library_read_only The library is shared into this workspace from another one. Create the SOP in the workspace that owns it.
404 not_found No library with that id in this workspace.
415 unsupported_media_type The request has no Content-Type: application/json header.
422 invalid_parameter library_id is missing or not a UUID, or description isn’t a string.
422 invalid_title title is missing, blank or longer than 200 characters.
422 invalid_description description is longer than 2,000 characters.
422 invalid_body Both or neither of markdown and text were sent, or the one sent isn’t a non-empty string.
422 too_large Over 500,000 characters, over 50 cards, or over about 1 MB of converted content.

Important: POST /sops isn’t idempotent: sending the same request twice creates two SOPs. If a request times out with no response, check GET /sops?library_id=...&updated_since=... for it before retrying.