<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Scheduled Downtime]]></title><description><![CDATA[3x founder and CTO, currently building Embed Workflow. Weekly maintenance on my thinking—documenting real problems, decisions, and progress building SaaS.]]></description><link>https://scheduleddowntime.com</link><image><url>https://substackcdn.com/image/fetch/$s_!CtcM!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f06ea52-33f9-4ab3-b2c2-cde5d0516aec_542x542.png</url><title>Scheduled Downtime</title><link>https://scheduleddowntime.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 13 May 2026 11:36:31 GMT</lastBuildDate><atom:link href="https://scheduleddowntime.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[David]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[damrani@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[damrani@substack.com]]></itunes:email><itunes:name><![CDATA[David Amrani]]></itunes:name></itunes:owner><itunes:author><![CDATA[David Amrani]]></itunes:author><googleplay:owner><![CDATA[damrani@substack.com]]></googleplay:owner><googleplay:email><![CDATA[damrani@substack.com]]></googleplay:email><googleplay:author><![CDATA[David Amrani]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why We Built Our Own OpenAPI to Markdoc Generator]]></title><description><![CDATA[No existing tool did what we needed, so we built our own.]]></description><link>https://scheduleddowntime.com/p/why-we-built-our-own-openapi-to-markdoc</link><guid isPermaLink="false">https://scheduleddowntime.com/p/why-we-built-our-own-openapi-to-markdoc</guid><dc:creator><![CDATA[David Amrani]]></dc:creator><pubDate>Mon, 04 Aug 2025 13:00:40 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/06c0ce85-2bd4-4398-8ddb-bcb002a12dff_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Part 2 of Building a Modern SDK System From Scratch</strong></p><p>In <a href="https://scheduleddowntime.com/p/stop-maintaining-api-docs-manually">Part 1</a>, I showed how we generate our OpenAPI spec from RSwag tests. Now that we had our spec, we wanted to generate our api specs. </p><h2>Why Markdoc?</h2><p>We evaluated multiple options: Docusaurus, Redocly, even Swagger UI with heavy customization. We needed React components for interactive code examples and API explorers, but we also wanted everyone on the team to be able to update docs without learning React. </p><p>Our API doc generator had to give us total control to make overrides when needed. Platform lock-in was also a major concern. I wanted something we could own completely.</p><p>Markdoc, created by Stripe (whose API docs are legendary), hit the sweet spot. It's basically Markdown that compiles to React, with a powerful tag system for custom components.</p><pre><code>{% code-example 

   title="Create a workflow" 

   languages={ruby: "...", python: "...", typescript: "..."} 

/%}</code></pre><p>This renders our custom React component while keeping the authoring experience simple.</p><h2>The Challenge: OpenAPI &#8594; Markdoc</h2><p>Markdoc was perfect for hand-written docs, but we needed to generate pages from our OpenAPI spec. No existing tool did this the way we wanted. Markdoc's community wasn't as vast as I'd thought and I couldn't find any plugins to generate docs from an openapi spec. </p><p>We needed to group endpoints logically (not just alphabetically), maintain our own navigation order, and extract all the details from the spec like example responses, request params, and field descriptions. Plus we had to generate working code examples for each SDK language.</p><p>So we built our own generator. Here's how it works.</p><h2>The Script Architecture</h2><p>Our generator script follows a simple pattern: parse the OpenAPI spec, group endpoints by resource, then generate Markdoc pages for each resource and endpoint.</p><h3>Core Generation Logic</h3><pre><code>async function generateAPIDocs() {

  // Determine resource groupings

  const resources = groupEndpointsByResource(spec.paths);


  // Ensure main directory exists

  ensureDirectoryExists(CONFIG.outputDir);


  // Generate pages for each resource

  Object.entries(resources).forEach(([resourceName, resource]) =&gt; {

    const resourceDir = path.join(CONFIG.outputDir, resourceName);


    // Ensure resource directory exists

    ensureDirectoryExists(resourceDir);


    // Generate resource overview page

    const overviewContent = generateResourceOverviewPage(

      resourceName,

      resource,

      spec

    );

    writeFile(path.join(resourceDir, "index.md"), overviewContent);


    // Generate individual endpoint pages

    resource.endpoints.forEach((endpoint) =&gt; {

      const filename = getEndpointFilename(endpoint) + ".md";

      const endpointContent = generateEndpointPage(

        resourceName,

        endpoint,

        spec

      );

      writeFile(path.join(resourceDir, filename), endpointContent);

    });

  });


  // Generate API resources constants for global navigations

  const constantsContent = generateAPIResourcesConstants(resources);

  writeFile(CONFIG.constantsPath, constantsContent);

}</code></pre><h3>1. Grouping Endpoints by Resource</h3><p>The first step extracts resources from your API paths:</p><pre><code>function groupEndpointsByResource(paths) {

  const resources = {};

  Object.entries(paths).forEach(([path, methods]) =&gt; {

    // Extract resource from /api/v1/workflows/{id} -&gt; "workflows"

    const resource = path.match(/\/api\/v1\/([^\/]+)/)?.[1];

    if (!resources[resource]) {

      resources[resource] = { endpoints: [] };

    }

    // Add each method (GET, POST, etc) to the resource

    Object.entries(methods).forEach(([method, operation]) =&gt; {

      resources[resource].endpoints.push({ 

        method, 

        path, 

        operation,

        operationId: operation.operationId 

      });

    });

  });

  return resources;

}</code></pre><h3>2. Generating Resource Overview Pages</h3><p>Each resource gets an overview page with all its endpoints:</p><pre><code>function generateResourceOverviewPage(resourceName, resource, spec) {

  const title = resourceName.charAt(0).toUpperCase() + resourceName.slice(1);

  return `---

title: ${title} API

description: Manage ${resourceName} in your account

---

{% columns %}

{% column col=1 %}

## Available Endpoints

{% endpoints %}

${resource.endpoints.map(endpoint =&gt; 

  `- ${endpoint.method}, /api/${resourceName}/${getEndpointFilename(endpoint)}, ${endpoint.path}`

).join('\n')}

{% /endpoints %}

## The ${title} Object

${generateSchemaAttributes(resource, spec)}

{% /column %}

{% column %}

\`\`\`json {% title="${title} Object" %}

${JSON.stringify(getExampleObject(resource, spec), null, 2)}

\`\`\`

{% /column %}

{% /columns %}`;

}</code></pre><h3>3. Generating Individual Endpoint Pages</h3><p>Each endpoint gets its own detailed documentation:</p><pre><code>function generateEndpointPage(resourceName, endpoint, spec) {

  const { method, path, operation } = endpoint;

  return `---

title: ${operation.summary}

description: ${operation.description || ''}

---

{% columns %}

{% column col=1 %}

**Endpoint**

\`${method} https://api.yourcompany.com${path}\`

${generateParameters(operation.parameters)}

${generateRequestBody(operation.requestBody, spec)}

${generateResponseSchema(operation.responses, spec)}

{% /column %}

{% column %}

{% code-example 

   title="${operation.summary}"

   languages={${generateCodeExamples(operation, method, path)}}

/%}

\`\`\`json {% title="Response" %}

${JSON.stringify(getResponseExample(operation, spec), null, 2)}

\`\`\`

{% /column %}

{% /columns %}`;

}</code></pre><h3>4. Navigation Structure</h3><p>The script also generates a constants file for navigation:</p><pre><code>function generateAPIResourcesConstants(resources) {

  const apiResources = Object.keys(resources).map(resourceName =&gt; ({

    name: resourceName,

    title: resourceName.charAt(0).toUpperCase() + resourceName.slice(1),

    endpoints: resources[resourceName].endpoints.map(endpoint =&gt; ({

      method: endpoint.method,

      path: endpoint.path,

      title: endpoint.operation.summary || `${endpoint.method} ${endpoint.path}`

    }))

  }));

  return `export const API_RESOURCES = ${JSON.stringify(apiResources, null, 2)};`;

}</code></pre><h2>Running the Generator</h2><pre><code>npm run generate-api-docs

# Outputs:

&#10003; Loaded OpenAPI spec

&#10003; Found 12 resources

&#10003; Generated 87 endpoint pages

&#10003; Created navigation structure</code></pre><p>The generated files slot right into our Markdoc site, where custom React components handle the interactive elements.</p><h2>Key Decisions</h2><p>We could have used Redocly or similar, but owning the generator gives us complete control. We can order navigation however makes sense for our users (not just alphabetically), format code examples exactly how we want them, keep everything consistent with our design system, and directly integrate with our SDK examples.</p><p>For a developer tools company, documentation *is* the product experience.</p><p>Next up in Part 3: How we generate type-safe SDKs from this same OpenAPI spec.</p><div><hr></div><p><em>Building a modern SDK system from scratch? I'd love to hear your approach - <a href="https://x.com/damrani5">reach out on X</a></em></p>]]></content:encoded></item><item><title><![CDATA[Stop Maintaining API Docs Manually: Our OpenAPI Generation Journey]]></title><description><![CDATA[How we built a system where tests generate docs, SDKs, and code examples from a single source of truth]]></description><link>https://scheduleddowntime.com/p/stop-maintaining-api-docs-manually</link><guid isPermaLink="false">https://scheduleddowntime.com/p/stop-maintaining-api-docs-manually</guid><dc:creator><![CDATA[David Amrani]]></dc:creator><pubDate>Mon, 21 Jul 2025 13:02:07 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d462b94c-8286-4637-a7bb-7e732e29e34b_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!69Ok!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!69Ok!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!69Ok!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1032107,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://scheduleddowntime.com/i/168756058?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!69Ok!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!69Ok!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634255ba-0df7-4523-ab0f-a8cfc179e335_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Part 1 of Building a Modern SDK System From Scratch</em></p><p>After building and maintaining APIs for 15 years, I've learned that the difference between good and great developer experience often comes down to documentation and SDKs. At <a href="https://embedworkflow.com">Embed Workflow</a>, we're building APIs that developers integrate deeply into their products, which means we can't afford to get this wrong.</p><p>Here's how we architected a system where our API tests generate everything&#8212;documentation, SDKs, and even code examples&#8212;from a single source of truth.</p><h2>The Challenge: Building APIs Developers Actually Want to Use</h2><p>When you're building a platform that other developers will integrate deeply into their products, the stakes are high. At <a href="https://embedworkflow.com">Embed Workflow</a>, we need:</p><ul><li><p>Rock-solid API documentation that's always accurate</p></li><li><p>SDKs in multiple languages (TypeScript, Ruby, Python to start)</p></li><li><p>Code examples that actually work (novel concept, I know)</p></li><li><p>A system that doesn't require manual updates every time we ship</p></li></ul><p>After maintaining API docs manually for many different products, I've seen the pattern: documentation drifts from reality, examples break silently, and engineers forget to make updates.</p><h2>The Approach: Tests as the Source of Truth</h2><p>Developer experience is non-negotiable for us. We needed a way to ensure our API docs stay in sync with our actual API, which meant keeping the documentation definitions as close to our Rails API code as possible&#8212;ideally inline with it. </p><h3>1. RSwag without the UI</h3><p>We decided to use rswag to generate our OpenAPI file. I've used it many times before and my previous teams all had access to the ever so popular swagger docs UI. Close your eyes and you'll see that clunky green interface. </p><p>The UI is actually great for internal use, but in the spirit of eating our own dog food, our team should be using the same docs as our customers. And that UI isn't going to cut it. Not for purely visual reasons&#8212;if that were the case, we could custom style it. I want our docs to include SDK code examples, endpoint definitions, links to guides, custom components... you get the idea. We need control so we can be creative and offer the best developer experience possible. </p><p>Our OpenAPI file would be generated by our new 'requests' spec folder. Here's a simple example:</p><pre><code># /spec/requests/api/workflows_spec.rb

RSpec.describe 'Workflows API', type: :request do

  path '/api/v1/workflows' do

    get("List workflows") do

      tags "Workflows"

      produces "application/json"

      security [ bearer: [] ]

&#9; ...

      before do

        ...

      end

      response(200, "successful") do

        ...

        run_test!

      end

    end

  end

end</code></pre><p>What makes this approach powerful is what we can generate from these specs:</p><ul><li><p><strong>Every endpoint</strong> with consistent naming and structure</p></li><li><p><strong>Input schema definitions</strong> (query params, request body) including examples, defaults, and required vs optional fields</p></li><li><p><strong>Response schemas</strong> showing exactly what developers will receive back, with examples</p></li><li><p><strong>Language-specific code examples</strong> for each SDK we offer</p></li><li><p><strong>Consistent parameters across examples</strong> - this is subtle but crucial: all code examples use the same input parameters and return the same response</p></li><li><p><strong>Tags and OperationIds</strong> for better SDK method naming and organization</p></li><li><p><strong>Consistent summaries and descriptions</strong> - naming conventions across the board are extremely important for developer experience</p></li></ul><h3>2. Serializers with Rich Metadata</h3><p>The API request responses and schemas needed to be pulled in automatically. Fortunately, we'd invested significant time in our API architecture and follow strict patterns, which made this portion manageable.</p><p>We use Blueprinter for serialization&#8212;every single API response already has a blueprint defined. For those unfamiliar, Blueprinter is a JSON serialization library that provides a simple, declarative way to define how your Ruby objects get converted to JSON. The plan was to enhance each field in every serializer with the necessary Swagger metadata. While Blueprinter doesn't have native support for Swagger, we designed a way to extract these attributes during our OpenAPI generation process.</p><pre><code>class WorkflowBlueprint &lt; BaseBlueprint

  object :workflow

  identifier :id

  field :description,

    swagger_type: :string,

    swagger_nullable: true,

    swagger_description: "Description of the workflow",

    swagger_example: "Welcome workflow for new users"

end</code></pre><h3>3. Properties and Params</h3><p>We needed a way to define our query parameters and request body schemas. Many parameters are used across multiple endpoints&#8212;for example, all list endpoints share <code>limit</code>, <code>starting_after</code>, <code>ending_before</code>, and <code>expand</code>. DRYing up these common definitions was an easy win. A plain old Ruby object did the trick:</p><pre><code>

  LIMIT_PARAM = {

    name: :limit,

    in: :query,

    type: :integer,

    required: false,

    description: "Number of items to return (max 100)",

    example: 25

  }.freeze

  ...

And we can update our specs like so:

get("List workflows") do

  ...

  parameter SwaggerSchemas::STARTING_AFTER_PARAM.dup

  parameter SwaggerSchemas::ENDING_BEFORE_PARAM.dup

  parameter SwaggerSchemas::LIMIT_PARAM.dup

  parameter SwaggerSchemas::EXPAND_PARAM.dup

  ...

end</code></pre><p>We followed the same pattern for properties and added simple procs for common properties that needed minor customizations based on the resource. </p><h3>4. Code Samples</h3><p>Deciding where code samples should live was perhaps the trickiest architectural decision. SDK repos? SDK code generator repo? Backend API? </p><p>Ultimately, it made the most sense to keep all code samples for all our SDKs defined in our Rails backend&#8212;one central place responsible for generating the OpenAPI file. A key benefit was that we could store the API request/response generated in the test, along with its schema from our serializer definitions, and use this exact response for all SDK examples. To ensure consistency, all inputs needed to be the same too. This approach eliminated coordination overhead between team members and reduced cross-system communication&#8212;everything lives in our API. </p><p>We created a Ruby module with predictable naming conventions:</p><pre><code>RETRIEVE_USER = {

  ruby: -&gt;(path_params:, query_params:, body_params:, **) {

    &lt;&lt;~RUBY.chomp

      EmbedWorkflow::Users.fetch(key: "#{path_params[:key]}")

    RUBY

  },

  typescript: -&gt;(path_params:, query_params:, body_params:, **) {

    &lt;&lt;~TS.chomp

      const user = await embedWorkflow.users.fetch("#{path_params[:key]}");

    TS

  },

  python: -&gt;(path_params:, query_params:, body_params:, **) {

    &lt;&lt;~PYTHON.chomp

      user = embed_workflow.users.fetch("#{path_params[:key]}")

    PYTHON

  }

}</code></pre><p>Our constants follow the same naming convention as each endpoint's summary: <code>get("Retrieve user")</code>. This allows us to dynamically check if a constant exists in our generation script.</p><h3>5. Response Schema Generation</h3><p>Earlier I mentioned how we added metadata to our serializers. Now we needed to extract that metadata and inject it into our Swagger definitions. </p><p>The specs use our custom schema generator:</p><pre><code>response(200, "successful") do

  schema **SwaggerSchemas.response_schema(Api::WorkflowBlueprint)

  ...

end

We then wrote a custom method that generates an OpenAPI 3.0 valid schema: 

def self.response_schema(blueprint, view = :default)

  SwaggerSchemaGenerator.from_blueprint(blueprint, view: view).freeze

end</code></pre><p>This method introspects the Blueprinter serializer and builds a proper OpenAPI schema complete with types, descriptions, examples, and nullable flags&#8212;all from the metadata we added to our serializers.</p><h3>6. Bringing It All Together</h3><p>With all these pieces in place, our CI pipeline runs a final generation script that:</p><p>1. Executes all RSwag tests to generate the base OpenAPI structure</p><p>2. Enhances each endpoint with our custom response schemas</p><p>3. Injects language-specific code examples for each operation</p><p>4. Validates the final OpenAPI spec against the 3.0 standard</p><p>5. Outputs a complete, accurate OpenAPI file ready for consumption</p><p>The real complexity wasn't in any individual piece&#8212;it was in designing a system where all these components work together seamlessly while remaining maintainable by our team.</p><h2>The Results</h2><p>After much refinement, here's what we achieved:</p><ul><li><p><strong>Zero documentation drift:</strong> Our docs are generated from our OpenAPI file. We wrote a custom script to generate endpoint pages in our home-grown doc site using Markdoc</p></li><li><p><strong>Consistent SDK experience:</strong> All SDKs share identical method signatures and behaviors</p></li><li><p><strong>Unlocked AI Possibilities:</strong> We are laying down a strong foundation for exciting AI projects that are underway</p></li><li><p><strong>Reduced support burden:</strong> Clear, accurate docs with working examples dramatically reduced API-related support tickets</p></li></ul><p></p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://scheduleddowntime.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Scheduled Downtime! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>The Bigger Picture</h2><p>This system serves three critical audiences that will define the future of API consumption:</p><ol><li><p><strong>Human developers</strong> get always-accurate documentation with examples that actually work</p></li><li><p><strong>Our SDKs</strong> generate from a single source of truth, ensuring consistency across languages</p></li><li><p><strong>AI agents</strong> can consume our OpenAPI spec to understand and use our API</p></li></ol><p>That last point deserves emphasis. As more development happens through AI assistants, having a machine-readable, always-accurate API specification isn't just nice&#8212;it's essential. We're already seeing AI tools that can generate entire integrations from OpenAPI specs.</p><p><strong>What this unlocks for AI:</strong> Our OpenAPI spec &#8594; TypeScript SDK &#8594; MCP (Model Context Protocol) server &#8594; AI agents that can actually use our API. We're already building an onboarding agent that can configure entire customer accounts through natural language. Imagine telling an AI "set up a welcome workflow for new users" and it actually does it&#8212;properly authenticated, with correct parameters, following your business logic. This is only possible because we have that reliable OpenAPI foundation.</p><p>By investing in this infrastructure now, we're future-proofing our platform.</p><h2>Lessons Learned</h2><p>If you're considering building something similar, here's what I wish I'd known at the start:</p><ol><li><p><strong>Start with the end in mind:</strong> Design your system thinking about all consumers (humans, SDKs, AI) from day one</p></li><li><p><strong>Invest in conventions early:</strong> Consistent naming and patterns pay compound dividends</p></li><li><p><strong>Make the right thing easy:</strong> If developers have to think about documentation, they won't do it</p></li><li><p><strong>Test your examples:</strong> Nothing erodes trust faster than code examples that don't work</p></li></ol><h2>What's Next</h2><p>This is just the foundation of our SDK generation system. In the upcoming posts, I'll dive into <strong>Part 2</strong> on why we chose Markdoc over Docusaurus for our documentation site.</p><p>We're now building AI agents on top of this foundation&#8212;starting with natural language onboarding for new customers.</p><p>What's your approach to API documentation? Are you generating from tests, maintaining manually, or trying something completely different? I'd love to hear about your experiences&#8212;drop a comment or <a href="https://x.com/damrani5">reach out on X</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!X0Uv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!X0Uv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 424w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 848w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 1272w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!X0Uv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png" width="231" height="231" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:231,&quot;width&quot;:231,&quot;resizeWidth&quot;:231,&quot;bytes&quot;:85824,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://scheduleddowntime.com/i/168756058?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!X0Uv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 424w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 848w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 1272w, https://substackcdn.com/image/fetch/$s_!X0Uv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcb91056-2ca1-4ff2-b01b-e66c9adf4ae9_231x231.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><strong>&#128075;  <a href="https://www.linkedin.com/in/amrani-david/">LinkedIn</a> | <a href="https://x.com/intent/follow?screen_name=damrani5">X</a> | <a href="https://github.com/amrani">Github</a></strong></figcaption></figure></div>]]></content:encoded></item></channel></rss>