<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Apideck</title>
    <description>The latest articles on DEV Community by Apideck (@apideck).</description>
    <link>https://dev.to/apideck</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10330%2F83f9e7ec-19ff-4cba-9ea1-8d42e6e4dfb3.png</url>
      <title>DEV Community: Apideck</title>
      <link>https://dev.to/apideck</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apideck"/>
    <language>en</language>
    <item>
      <title>Your MCP Server Is Eating Your Context Window. There's a Simpler Way</title>
      <dc:creator>samz</dc:creator>
      <pubDate>Mon, 16 Mar 2026 15:31:45 +0000</pubDate>
      <link>https://dev.to/apideck/your-mcp-server-is-eating-your-context-window-theres-a-simpler-way-315b</link>
      <guid>https://dev.to/apideck/your-mcp-server-is-eating-your-context-window-theres-a-simpler-way-315b</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MCP tool definitions can burn 55,000+ tokens before an agent processes a single user message. We built the Apideck CLI as an AI-agent interface instead:an ~80-token agent prompt replaces tens of thousands of tokens of schema, with progressive disclosure via &lt;code&gt;--help&lt;/code&gt; and structural safety baked into the binary. Any agent that can run shell commands can use it. No protocol support required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem demos never show you
&lt;/h2&gt;

&lt;p&gt;Here's a scenario that'll feel familiar if you've wired up MCP servers for anything beyond a demo.&lt;/p&gt;

&lt;p&gt;You connect GitHub, Slack, and Sentry. Three services, maybe 40 tools total. Before your agent has read a single user message, &lt;a href="https://www.mmntm.net/articles/mcp-context-tax" rel="noopener noreferrer"&gt;55,000 tokens of tool definitions&lt;/a&gt; are sitting in the context window. That's over a quarter of Claude's 200k limit. Gone.&lt;/p&gt;

&lt;p&gt;It gets worse. Each MCP tool costs &lt;a href="https://www.mmntm.net/articles/mcp-context-tax" rel="noopener noreferrer"&gt;550-1,400 tokens&lt;/a&gt; for its name, description, JSON schema, field descriptions, enums, and system instructions. Connect a real API surface, say a SaaS platform with 50+ endpoints, and you're looking at 50,000+ tokens just to describe what the agent &lt;em&gt;could&lt;/em&gt; do, with almost nothing left for what it &lt;em&gt;should&lt;/em&gt; do.&lt;/p&gt;

&lt;p&gt;One team &lt;a href="https://www.agentpmt.com/articles/thousands-of-mcp-tools-zero-context-left-the-bloat-tax-breaking-ai-agents" rel="noopener noreferrer"&gt;reported&lt;/a&gt; three MCP servers consuming 143,000 of 200,000 tokens. That's 72% of the context window burned on tool definitions. The agent had 57,000 tokens left for the actual conversation, retrieved documents, reasoning, and response. Good luck building anything useful in that space.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical concern. David Zhang (&lt;a href="https://x.com/dzhng/status/2029518820872945889" rel="noopener noreferrer"&gt;@dzhng&lt;/a&gt;), building Duet, described ripping out their MCP integrations entirely, even after getting OAuth and dynamic client registration working. The tradeoff was impossible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load everything up front&lt;/strong&gt; → lose working memory for reasoning and history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit integrations&lt;/strong&gt; → agent can only talk to a few services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build dynamic tool loading&lt;/strong&gt; → add latency and middleware complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;He called it a "trilemma."&lt;/p&gt;

&lt;p&gt;And the numbers hold up under controlled testing. A &lt;a href="https://www.scalekit.com/blog/mcp-vs-cli-use" rel="noopener noreferrer"&gt;recent benchmark by Scalekit&lt;/a&gt; ran 75 head-to-head comparisons (same model, Claude Sonnet 4, same tasks, same prompts) and found MCP costing &lt;strong&gt;4 to 32x more tokens&lt;/strong&gt; than CLI for identical operations. Their simplest task, checking a repo's language, consumed 1,365 tokens via CLI and 44,026 via MCP. The overhead is almost entirely schema: 43 tool definitions injected into every conversation, of which the agent uses one or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three approaches to the same problem
&lt;/h2&gt;

&lt;p&gt;The industry is converging on three responses to context bloat. Each has a sweet spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP with compression tricks
&lt;/h3&gt;

&lt;p&gt;The first response is to keep MCP but fight the bloat. Teams compress schemas, use tool search to load definitions on demand, or build middleware that slices OpenAPI specs into smaller chunks.&lt;/p&gt;

&lt;p&gt;This works for small, well-defined interactions like looking up an issue, creating a ticket, or fetching a document. MCP's structured tool calls and typed schemas are genuinely useful when you have a tight set of operations that agents use frequently.&lt;/p&gt;

&lt;p&gt;But it adds infrastructure. You need a tool registry, search logic, caching, and routing. You're building a service to manage your services. And you're still paying per-tool token costs every time the agent decides it needs a new capability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code execution
&lt;/h3&gt;

&lt;p&gt;The code execution approach treats the agent like a developer with a persistent workspace. When the agent needs a new integration, it reads the API docs, writes code against the SDK, runs it, and saves the script for reuse. Duet pioneered this pattern by letting agents write and maintain their own integration scripts.&lt;/p&gt;

&lt;p&gt;This is powerful for long-lived workspace agents that maintain state across sessions and need complex workflows: loops, conditionals, polling, batch operations. Things that are awkward to express as individual tool calls become natural in code.&lt;/p&gt;

&lt;p&gt;There's a more targeted variant worth watching: &lt;a href="https://blog.cloudflare.com/code-mode/" rel="noopener noreferrer"&gt;Code Mode&lt;/a&gt;. Instead of writing arbitrary code against raw APIs, the agent writes short orchestration scripts that call structured MCP tools underneath. A &lt;a href="https://www.portofcontext.com/blog/cli-vs-mcp-vs-code-mode" rel="noopener noreferrer"&gt;benchmark by Sideko&lt;/a&gt; across 12 Stripe tasks showed Code Mode MCP using 58% fewer tokens than raw MCP and 56% fewer than CLI. The key insight: on multi-step tasks like creating an invoice with line items, CLI required 19 LLM round trips, raw MCP needed 12, and Code Mode collapsed it to 4. The agent writes a TypeScript program that handles the looping internally, without going back to the LLM at each step.&lt;/p&gt;

&lt;p&gt;This matters because CLI's efficiency advantage, which is real for single-step discovery and reads, can erode on complex chained writes where each round trip compounds context. Code Mode offers a middle ground: structured tool access without the schema bloat, plus the ability to batch operations without per-step LLM overhead.&lt;/p&gt;

&lt;p&gt;The tradeoff is that your agent is writing and executing code against production APIs. Even sandboxed, the safety surface is larger than a CLI with structural permissions. You need review mechanisms and trust in your agent's judgment. But for workflows that involve loops and dependent state, it's a pattern worth considering alongside CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI as the agent interface
&lt;/h3&gt;

&lt;p&gt;The third approach is the one we took. Instead of loading schemas into the context window or letting the agent write integration code, you give it a CLI.&lt;/p&gt;

&lt;p&gt;A well-designed CLI is a progressive disclosure system by nature. When a human developer needs to use a tool they haven't touched before, they don't read the entire API reference. They run &lt;code&gt;tool --help&lt;/code&gt;, find the subcommand they need, run &lt;code&gt;tool subcommand --help&lt;/code&gt;, and get the specific flags for that operation. They pay attention costs proportional to what they actually need.&lt;/p&gt;

&lt;p&gt;Agents can do exactly the same thing. And the token economics are dramatically different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CLIs are the pragmatic sweet spot
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Progressive disclosure saves tokens
&lt;/h3&gt;

&lt;p&gt;Here's what the &lt;a href="https://github.com/apideck-libraries/cli" rel="noopener noreferrer"&gt;Apideck CLI&lt;/a&gt; agent prompt looks like. This is the entire thing an AI agent needs in its system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use `apideck` to interact with the Apideck Unified API.
Available APIs: `apideck --list`
List resources: `apideck &amp;lt;api&amp;gt; --list`
Operation help: `apideck &amp;lt;api&amp;gt; &amp;lt;resource&amp;gt; &amp;lt;verb&amp;gt; --help`
APIs: accounting, ats, crm, ecommerce, hris, ...
Auth is pre-configured. GET auto-approved. POST/PUT/PATCH prompt (use --yes). DELETE blocked (use --force).
Use --service-id &amp;lt;connector&amp;gt; to target a specific integration.
For clean output: -q -o json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's ~80 tokens. Compare that to the alternatives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Tokens consumed&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full OpenAPI spec in context&lt;/td&gt;
&lt;td&gt;30,000-100,000+&lt;/td&gt;
&lt;td&gt;Before first message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP tools (~3,600 per API)&lt;/td&gt;
&lt;td&gt;10,000-50,000+&lt;/td&gt;
&lt;td&gt;Before first message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI agent prompt&lt;/td&gt;
&lt;td&gt;~80&lt;/td&gt;
&lt;td&gt;Before first message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI &lt;code&gt;--help&lt;/code&gt; call&lt;/td&gt;
&lt;td&gt;~50-200&lt;/td&gt;
&lt;td&gt;Only when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent starts with 80 tokens of guidance and discovers capabilities on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Level 1: What APIs are available? (~20 tokens output)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;apideck &lt;span class="nt"&gt;--list&lt;/span&gt;
accounting ats connector crm ecommerce hris ...

&lt;span class="c"&gt;# Level 2: What can I do with accounting? (~200 tokens output)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;apideck accounting &lt;span class="nt"&gt;--list&lt;/span&gt;
Resources &lt;span class="k"&gt;in &lt;/span&gt;accounting API:

  invoices
    list       GET  /accounting/invoices
    get        GET  /accounting/invoices/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
    create     POST /accounting/invoices
    delete     DELETE /accounting/invoices/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

  customers
    list       GET  /accounting/customers
    ...

&lt;span class="c"&gt;# Level 3: How do I create an invoice? (~150 tokens output)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;apideck accounting invoices create &lt;span class="nt"&gt;--help&lt;/span&gt;
Usage: apideck accounting invoices create &lt;span class="o"&gt;[&lt;/span&gt;flags]

Flags:
  &lt;span class="nt"&gt;--data&lt;/span&gt; string        JSON request body &lt;span class="o"&gt;(&lt;/span&gt;or @file.json&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nt"&gt;--service-id&lt;/span&gt; string  Target a specific connector
  &lt;span class="nt"&gt;--yes&lt;/span&gt;                Skip write confirmation
  &lt;span class="nt"&gt;-o&lt;/span&gt;, &lt;span class="nt"&gt;--output&lt;/span&gt; string  Output format &lt;span class="o"&gt;(&lt;/span&gt;json|table|yaml|csv&lt;span class="o"&gt;)&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step costs 50-200 tokens, loaded only when the agent decides it needs that information. An agent handling an accounting query might consume 400 tokens total across three &lt;code&gt;--help&lt;/code&gt; calls. The same surface through MCP would cost 10,000+ tokens loaded upfront whether the agent uses them or not.&lt;/p&gt;

&lt;p&gt;This mirrors how &lt;a href="https://www.linkedin.com/posts/lucas-althoff_contextengineering-activity-7401317037458984960-usmb" rel="noopener noreferrer"&gt;Claude Agent Skills&lt;/a&gt; work. Metadata first, full details only when selected, reference material only when needed. The CLI does the same thing through a different mechanism.&lt;/p&gt;

&lt;p&gt;Scalekit's benchmark independently validated this pattern. They found that even a minimal ~800-token "skills file" (a document of CLI tips and common workflows) reduced tool calls by a third and latency by a third compared to a bare CLI. Our approach takes it further: the ~80-token agent prompt provides the same progressive discovery at a tenth of the cost. The principle is the same. A small, upfront hint about how to navigate the tool is worth more than thousands of tokens of exhaustive schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reliability: local beats remote
&lt;/h3&gt;

&lt;p&gt;There's a dimension of the MCP problem that doesn't get enough attention: &lt;strong&gt;availability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Scalekit's benchmark recorded a &lt;strong&gt;28% failure rate&lt;/strong&gt; on MCP calls to GitHub's Copilot server. Out of 25 runs, 7 failed with TCP-level connection timeouts. These weren't protocol errors or bad tool calls. The connection never completed.&lt;/p&gt;

&lt;p&gt;CLI agents don't have this failure mode. The binary runs locally. There's no remote server to time out, no connection pool to exhaust. When your agent runs &lt;code&gt;apideck accounting invoices list&lt;/code&gt;, it makes a direct HTTPS call to the Apideck API. One hop, not two.&lt;/p&gt;

&lt;p&gt;This matters at scale. At 10,000 operations per month, a 28% failure rate means roughly 2,800 retries, each burning additional tokens and latency. Scalekit estimated the monthly cost difference at &lt;strong&gt;$3.20 for CLI versus $55.20 for direct MCP&lt;/strong&gt;, a 17x cost multiplier, with the reliability tax on top.&lt;/p&gt;

&lt;p&gt;Remote MCP servers will improve. Connection pooling, better infrastructure, and gateway layers will close the gap. But "the binary is on your machine" is a reliability guarantee that no amount of infrastructure engineering on the server side can match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural safety beats prompt-based safety
&lt;/h3&gt;

&lt;p&gt;Telling an agent "never delete production data" in a system prompt is like putting a sticky note on the nuclear launch button. It works until a creative prompt injection peels the note off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents" rel="noopener noreferrer"&gt;Security research on AI agents in CI/CD&lt;/a&gt; has shown how prompt injection can manipulate agents with high-privilege tokens into leaking secrets or modifying infrastructure. The pattern is always the same: untrusted input gets injected into a prompt, the agent has broad tool access, and bad things happen.&lt;/p&gt;

&lt;p&gt;The Apideck CLI takes a structural approach. Permission classification is baked into the binary based on HTTP method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// From internal/permission/engine.go&lt;/span&gt;
&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Permission&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermissionRead&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionAllow&lt;/span&gt;      &lt;span class="c"&gt;// GET -&amp;gt; auto-approved&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermissionWrite&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionPrompt&lt;/span&gt;     &lt;span class="c"&gt;// POST/PUT/PATCH -&amp;gt; confirmation required&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermissionDangerous&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionBlock&lt;/span&gt;       &lt;span class="c"&gt;// DELETE -&amp;gt; blocked by default&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No prompt can override this. A &lt;code&gt;DELETE&lt;/code&gt; operation is blocked unless the caller explicitly passes &lt;code&gt;--force&lt;/code&gt;. A &lt;code&gt;POST&lt;/code&gt; requires &lt;code&gt;--yes&lt;/code&gt; or interactive confirmation. &lt;code&gt;GET&lt;/code&gt; operations run freely because they can't modify state.&lt;/p&gt;

&lt;p&gt;The agent frameworks reinforce this. Claude Code, Cursor, and GitHub Copilot all have permission systems that gate shell command execution. So you get two layers of structural safety: the agent framework asks "should I run this command?" and the CLI itself enforces "is this operation allowed?"&lt;/p&gt;

&lt;p&gt;You can also customize the policy per operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.apideck-cli/permissions.yaml&lt;/span&gt;
&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
  &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prompt&lt;/span&gt;
  &lt;span class="na"&gt;dangerous&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;

&lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accounting.payments.create&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;    &lt;span class="c1"&gt;# payments are sensitive&lt;/span&gt;
  &lt;span class="na"&gt;crm.contacts.delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prompt&lt;/span&gt;          &lt;span class="c1"&gt;# contacts can be soft-deleted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same principle behind &lt;a href="https://blog.duda.co/duda-mcp" rel="noopener noreferrer"&gt;Duda blocking destructive MCP actions&lt;/a&gt;, but enforced structurally in the binary, not through prompt instructions that compete with everything else in the context window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Universal compatibility, zero protocol overhead
&lt;/h3&gt;

&lt;p&gt;Every serious agent framework ships with "run shell command" as a primitive. Claude Code has &lt;code&gt;Bash&lt;/code&gt;. Cursor has terminal access. GitHub Copilot SDK exposes shell execution. Gemini CLI runs commands natively.&lt;/p&gt;

&lt;p&gt;MCP requires dedicated client support, connection plumbing, and server lifecycle management. A CLI requires a binary on the PATH.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds. When you're building an agent that needs to interact with APIs, the integration path for a CLI is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the binary&lt;/li&gt;
&lt;li&gt;Set environment variables for auth&lt;/li&gt;
&lt;li&gt;Add ~80 tokens to the system prompt&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integration path for MCP is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implement or configure an MCP client&lt;/li&gt;
&lt;li&gt;Set up server connections (transport, auth, lifecycle)&lt;/li&gt;
&lt;li&gt;Handle tool registration and schema loading&lt;/li&gt;
&lt;li&gt;Manage connection state and reconnection&lt;/li&gt;
&lt;li&gt;Deal with the token budget for tool definitions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CLI approach also means your agent integration isn't locked to any specific framework. The same &lt;code&gt;apideck&lt;/code&gt; binary works from Claude Code, Cursor, a custom Python agent, a bash script, or a CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we built it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/apideck-libraries/cli" rel="noopener noreferrer"&gt;Apideck CLI&lt;/a&gt; is a single static binary that parses our OpenAPI spec at startup and generates its entire command tree dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAPI-native, no code generation.&lt;/strong&gt; The binary embeds the latest Apideck Unified API spec. On startup, it parses the spec with &lt;a href="https://github.com/pb33f/libopenapi" rel="noopener noreferrer"&gt;libopenapi&lt;/a&gt; and builds commands for every API group, resource, and operation. When the API adds new endpoints, &lt;code&gt;apideck sync&lt;/code&gt; pulls the latest spec. No SDK regeneration, no version bumps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart output defaults.&lt;/strong&gt; When running in a terminal, output defaults to a formatted table with colors. When piped or called from a non-TTY (which is how agents call it), output defaults to JSON. Agents get machine-parseable output without needing to remember &lt;code&gt;--output json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Agent calls this (non-TTY) -&amp;gt; gets JSON automatically&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;apideck accounting invoices list &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"inv_12345"&lt;/span&gt;, &lt;span class="s2"&gt;"number"&lt;/span&gt;: &lt;span class="s2"&gt;"INV-001"&lt;/span&gt;, &lt;span class="s2"&gt;"total"&lt;/span&gt;: 1500.00, ...&lt;span class="o"&gt;}]&lt;/span&gt;

&lt;span class="c"&gt;# Human runs the same command in terminal -&amp;gt; gets a table&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;apideck accounting invoices list
┌──────────┬─────────┬──────────┐
│ ID       │ Number  │ Total    │
├──────────┼─────────┼──────────┤
│ inv_12345│ INV-001 │ 1,500.00 │
└──────────┴─────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auth is invisible.&lt;/strong&gt; Credentials are resolved from environment variables (&lt;code&gt;APIDECK_API_KEY&lt;/code&gt;, &lt;code&gt;APIDECK_APP_ID&lt;/code&gt;, &lt;code&gt;APIDECK_CONSUMER_ID&lt;/code&gt;) or a config file, and injected into every request automatically. The agent never handles tokens, never sees auth headers, never needs to manage sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connector targeting.&lt;/strong&gt; The &lt;code&gt;--service-id&lt;/code&gt; flag lets agents target specific integrations. &lt;code&gt;apideck accounting invoices list --service-id quickbooks&lt;/code&gt; hits QuickBooks. Swap to &lt;code&gt;--service-id xero&lt;/code&gt; and the same command hits Xero. Same interface, different backend. The unified API handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  When CLI isn't the answer
&lt;/h2&gt;

&lt;p&gt;CLIs aren't universally better. Here's where the other approaches win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP is better for tightly scoped, high-frequency tools.&lt;/strong&gt; If your agent calls the same 5-10 tools hundreds of times per session, the upfront schema cost amortizes well. A customer support agent that only ever looks up tickets, updates status, and sends replies doesn't need progressive disclosure. It needs those tools ready immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code execution is better for complex, stateful workflows.&lt;/strong&gt; If your agent needs to poll an API every 30 seconds, aggregate results across paginated endpoints, or orchestrate multi-step transactions with rollback logic, writing code is more natural than chaining CLI calls. As the Sideko benchmark showed, CLI's efficiency advantage can reverse on multi-step chained writes where each round trip compounds context. For those patterns, Code Mode (agent writes orchestration scripts that call structured tools) or the Duet approach (full code execution) will use fewer total tokens despite higher per-step overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP is better when your agent acts on behalf of other people's users.&lt;/strong&gt; This is the dimension most CLI-vs-MCP comparisons gloss over, and it's worth being direct about. When your agent automates &lt;em&gt;your own&lt;/em&gt; workflow, ambient credentials are fine. You are the user, and the only person at risk is you. But if you're building a B2B product where agents act on behalf of your customers' employees, across organizations those customers control, the identity problem becomes three-layered: which agent is calling, which user authorized it, and which tenant's data boundary applies. Per-user OAuth with scoped access, consent flows, and structured audit trails are real requirements at that boundary, and they're requirements that raw CLI auth (&lt;code&gt;gh auth login&lt;/code&gt;, environment variables) wasn't designed to solve. MCP's authorization model, whatever its efficiency cost, addresses this natively.&lt;/p&gt;

&lt;p&gt;There's also a deeper identity gap that CLI auth doesn't address: agent-level identity. A CLI token authenticates the user, but the API provider never knows &lt;em&gt;which agent&lt;/em&gt; made the request. That matters for policy enforcement. If an API provider has a partnership with Agent A but not Agent B, there's no way to distinguish them in a CLI token. MCP's OAuth model can carry agent identity through claims like &lt;code&gt;act&lt;/code&gt;, which becomes critical as agents start calling other agents and you need the full chain of delegation in the token. For single-agent workflows this is academic. For multi-agent architectures it's a real architectural constraint.&lt;/p&gt;

&lt;p&gt;That said, the gap is narrower than it looks for unified API architectures. Apideck already centralizes auth through &lt;a href="https://www.apideck.com/products/vault" rel="noopener noreferrer"&gt;Vault&lt;/a&gt;: credentials are managed per-consumer, per-connection, and scoped by service. The &lt;code&gt;--service-id&lt;/code&gt; flag targets a specific integration within a specific consumer's vault. The structural permission system enforces read/write/delete boundaries in the binary. What's missing is the per-user OAuth consent flow and tenant-scoped audit trail, real gaps, but ones that sit at the platform layer, not the agent interface layer. A CLI can be the interface while a backend handles delegated authorization. These aren't mutually exclusive.&lt;/p&gt;

&lt;p&gt;It's also worth noting that MCP's auth story is less settled than it appears. As &lt;a href="https://www.speakeasy.com/docs/mcp-platform/secure/add-oauth-to-mcp-servers" rel="noopener noreferrer"&gt;Speakeasy's MCP OAuth guide&lt;/a&gt; makes clear, user-facing OAuth exchange is not actually required by the MCP spec. Passing access tokens or API keys directly is completely valid. The real complexity kicks in when MCP clients need to handle OAuth flows dynamically, which requires Dynamic Client Registration (DCR), a capability most API providers don't support today. Companies like Stripe and Asana have started adding DCR to accommodate MCP, but it remains a high-friction integration. The auth advantage MCP has over CLI is real in theory, but in practice, the ecosystem is still catching up to the spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLIs are weaker at streaming and bi-directional communication.&lt;/strong&gt; A CLI call is request-response. If you need server-sent events, WebSocket streams, or long-lived connections, you'll want an SDK or MCP server that can hold a connection open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribution has friction.&lt;/strong&gt; MCP servers can theoretically live behind a URL. CLIs need a binary per platform, updates, and PATH management. For the Apideck CLI, we ship a static Go binary that runs everywhere without dependencies, but it's still a binary you need to install.&lt;/p&gt;

&lt;p&gt;The honest framing: MCP, code execution, and CLIs are complementary tools. The mistake is treating MCP as the universal answer when, for many integration patterns, a CLI does the job with two orders of magnitude less context overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for API providers
&lt;/h2&gt;

&lt;p&gt;If you're building developer tools in 2026, AI agents are becoming a primary consumer of your API surface. Not the only consumer (human developers still matter), but a rapidly growing one.&lt;/p&gt;

&lt;p&gt;A few things are worth considering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your OpenAPI spec is too big for a context window.&lt;/strong&gt; If you have 50+ endpoints, converting your spec to MCP tools will burn the budget of most agent interactions. Think about what a minimal entry point looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive disclosure isn't just a UX pattern anymore.&lt;/strong&gt; It's a token optimization strategy. Give agents a way to discover capabilities incrementally instead of dumping everything upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural safety is non-negotiable.&lt;/strong&gt; Prompt-based guardrails are the security equivalent of honor system parking. Build permission models into your tools, not your prompts. Classify operations by risk level and enforce that classification in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship machine-friendly output formats.&lt;/strong&gt; JSON by default in non-interactive contexts. Stable exit codes. Deterministic output. These are &lt;a href="https://dev.to/tumf/agentic-cli-design-7-principles-for-designing-cli-as-a-protocol-for-ai-agents-2c10"&gt;documented principles for agentic CLI design&lt;/a&gt;, and they matter because your next power user might not have hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/mcp-vs-api" rel="noopener noreferrer"&gt;MCP vs API&lt;/a&gt; - How MCP and REST APIs relate (Apideck blog)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/api-design-principles-agentic-era" rel="noopener noreferrer"&gt;API Design Principles for the Agentic Era&lt;/a&gt; - Designing APIs with AI agents as first-class consumers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/understanding-the-security-landscape-of-mcp" rel="noopener noreferrer"&gt;Understanding the Security Landscape of MCP&lt;/a&gt; - MCP security considerations in depth&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mmntm.net/articles/mcp-context-tax" rel="noopener noreferrer"&gt;The MCP Context Tax&lt;/a&gt; - Detailed analysis of MCP token overhead&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/tumf/agentic-cli-design-7-principles-for-designing-cli-as-a-protocol-for-ai-agents-2c10"&gt;Agentic CLI Design: 7 Principles&lt;/a&gt; - Design principles for CLIs as agent interfaces&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.scalekit.com/blog/mcp-vs-cli-use" rel="noopener noreferrer"&gt;MCP vs CLI Benchmark&lt;/a&gt; - Scalekit's head-to-head benchmark data (75 runs, Claude Sonnet 4)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.portofcontext.com/blog/cli-vs-mcp-vs-code-mode" rel="noopener noreferrer"&gt;CLI vs MCP vs Code Mode Benchmark&lt;/a&gt; - Sideko's 12-task Stripe benchmark comparing all three approaches&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/code-mode/" rel="noopener noreferrer"&gt;Code Mode: the better way to use MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.atcyrus.com/stories/mcp-tool-search-claude-code-context-pollution-guide" rel="noopener noreferrer"&gt;What is MCP Tool Search?&lt;/a&gt; - The Claude Code feature that addresses context pollution&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/yenkel/status/2032098351567487037" rel="noopener noreferrer"&gt;If you kill MCP, you don't give a s**t about security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portofcontext.com/blog/cli-vs-mcp-vs-code-mode" rel="noopener noreferrer"&gt;CLI vs. MCP vs. Code Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>cli</category>
      <category>api</category>
    </item>
    <item>
      <title>Accounting Integration</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Thu, 05 Mar 2026 05:42:01 +0000</pubDate>
      <link>https://dev.to/apideck/accounting-integration-2967</link>
      <guid>https://dev.to/apideck/accounting-integration-2967</guid>
      <description>&lt;p&gt;Your finance team didn't sign up to be data-entry clerks. Neither did your customers.&lt;/p&gt;

&lt;p&gt;Yet every day, thousands of businesses manually export transactions from QuickBooks, copy invoice data from Xero, or reconcile payments between three different spreadsheets. It's slow, error-prone, and a waste of skilled people's time.&lt;/p&gt;

&lt;p&gt;Accounting integrations fix this. They connect your software directly to your customers' general ledger, automatically syncing invoices, expenses, payments, and journal entries. No CSV exports. No copy-paste errors. No "we'll reconcile it at month-end."&lt;/p&gt;

&lt;p&gt;This guide covers what accounting integrations actually do, the most common use cases, and how companies like Ramp, Airbase, and BILL have leveraged them to gain a competitive advantage.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.apideck.com/blog/what-is-open-accounting" rel="noopener noreferrer"&gt;open accounting movement&lt;/a&gt; is driving demand for standardized data access across accounting platforms, making integration capabilities a competitive requirement for B2B SaaS products.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an Accounting Integration?
&lt;/h2&gt;

&lt;p&gt;An accounting integration is a connection between your application and an accounting platform like &lt;a href="https://www.apideck.com/connectors/quickbooks" rel="noopener noreferrer"&gt;QuickBooks&lt;/a&gt;, &lt;a href="https://www.apideck.com/connectors/xero" rel="noopener noreferrer"&gt;Xero&lt;/a&gt;, &lt;a href="https://www.apideck.com/connectors/netsuite" rel="noopener noreferrer"&gt;NetSuite&lt;/a&gt;, or &lt;a href="https://www.apideck.com/connectors/sage-intacct" rel="noopener noreferrer"&gt;Sage Intacct&lt;/a&gt;. It allows data to flow between systems, either one-way or bidirectionally, without manual intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A quick definition, if you're new to this:&lt;/strong&gt; A general ledger (GL) is the master record of all financial transactions in a business. Every transaction gets tagged with a GL code, which is essentially a label that categorizes where the money went: "Travel," "Software Subscriptions," "Office Supplies," and so on. When we talk about syncing to the GL, we mean getting your data into that master record with the right labels attached.&lt;/p&gt;

&lt;p&gt;At a practical level, accounting integrations mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invoices&lt;/strong&gt; created in your billing system appear automatically in your customer's accounting software
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expenses&lt;/strong&gt; logged in your app push directly to the general ledger with the correct GL codes
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; recorded in one system update the other in real time
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bank transactions&lt;/strong&gt; reconcile automatically instead of requiring manual matching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The alternative is what most businesses still do: export a CSV, open Excel, clean the data, reformat it for the accounting system, import it, and hope nothing gets lost in translation. Multiply that by hundreds of transactions per month, and you've created busywork that produces no value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Accounting Integrations Matter for Your Product
&lt;/h2&gt;

&lt;p&gt;If you're building software that touches money (invoicing, expense management, payroll, procurement, lending, payments), your customers will eventually ask: "Does this connect to our accounting system?"&lt;/p&gt;

&lt;p&gt;The answer determines whether you're a tool they'll actually adopt or one they'll abandon after the free trial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The business case is straightforward:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Faster sales cycles.&lt;/strong&gt; Enterprise buyers have procurement checklists. "Integrates with NetSuite" is often a required line item. Without it, you don't make the shortlist.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower churn.&lt;/strong&gt; Customers who integrate your product into their financial workflows don't leave easily. The switching cost is too high when your app is integrated into their month-end close process.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher contract values.&lt;/strong&gt; Integration features justify premium pricing. Ramp charges more for deeper ERP connections. So does BILL. Customers pay for the time savings.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced support burden.&lt;/strong&gt; Every "how do I export this to QuickBooks?" ticket is a symptom of a missing integration. Build it once, eliminate the ticket category.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market expansion.&lt;/strong&gt; Companies across specific regions, industries, and sizes use certain accounting tools. SMBs gravitate toward QuickBooks and Xero. Mid-market and enterprise prefer NetSuite, Sage Intacct, and Microsoft Dynamics. Offering integrations with the tools your target markets rely on opens doors that would otherwise stay closed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two Types of Accounting Integrations
&lt;/h2&gt;

&lt;p&gt;Before diving into use cases, it's worth distinguishing between two categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Customer-Facing (Product) Integrations&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These connect your product to your customers' accounting systems. They're what this guide focuses on—the integrations that become product features, drive sales conversations, and require supporting hundreds of different customer configurations.&lt;/p&gt;

&lt;p&gt;The rest of this guide addresses customer-facing integrations, which present unique challenges around multi-tenancy, authorization, and scale.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Internal Integrations&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These connect your company's own systems. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CRM → Accounting:&lt;/strong&gt; When an opportunity closes in Salesforce, automatically create the customer in NetSuite for invoicing
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting → BI:&lt;/strong&gt; Sync financial data from Xero to Tableau for executive dashboards
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting → Slack:&lt;/strong&gt; Alert the finance channel when invoices are overdue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are typically built with &lt;a href="https://www.apideck.com/blog/top-embedded-ipaas-solutions" rel="noopener noreferrer"&gt;iPaaS tools&lt;/a&gt; (Workato, Tray) or custom scripts. They're one-off connections serving your internal workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  6 Common Use Cases for Accounting Integrations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Expense Management → General Ledger Sync
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Employees submit expenses. Finance approves them. Then someone manually enters each line item into QuickBooks with the correct GL code, department, and tax treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Approved expenses push automatically to the accounting system. GL codes mapped by expense category. The finance team reviews exceptions, not every transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; Brex, Expend, ExpenseOnDemand, Ramp, and Airbase all sync expenses directly to QuickBooks, Xero, NetSuite, and Sage Intacct. For corporate card products, this is now table stakes. &lt;a href="https://www.apideck.com/customer-cases/invoice2go-bill-com-integrations" rel="noopener noreferrer"&gt;Invoice2go by BILL&lt;/a&gt; used Apideck's Unified API to speed up accounting integrations across multiple platforms, enabling support for major systems without custom builds. This helped them scale expense and reporting features quickly across over 220,000 customers worldwide.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Below is what the QuickBooks Online integration looks like on Ramp after you set it up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/5Cywz71LzRmqG3TeHJVfLF/184288b5b8c5ec4f6876f134236be411/Screenshot_2024-04-11_at_5.37.51%C3%A2__PM.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/5Cywz71LzRmqG3TeHJVfLF/184288b5b8c5ec4f6876f134236be411/Screenshot_2024-04-11_at_5.37.51%C3%A2__PM.png" alt="Below is what the QuickBooks Online integration looks like on Ramp after you set it up. "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Invoicing and Billing → Accounts Receivable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Your product generates invoices. Your customer's accounting team needs those invoices in their accounts receivable ledger. Right now, they're downloading PDFs and manually creating entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Invoices created in your system automatically appear in the customer's accounting platform. Payment status syncs back. When the invoice is paid, both systems reflect it without anyone having to touch a keyboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; Stripe's invoicing connects to QuickBooks and Xero. Chargebee syncs subscription invoices to major accounting platforms. Any serious billing tool offers this now. For example, &lt;a href="https://www.apideck.com/customer-cases/roopairs-quickbooks-online-integration" rel="noopener noreferrer"&gt;Roopairs&lt;/a&gt; used Apideck's Unified API to launch a QuickBooks Online integration quickly, saving roughly 40 developer hours and supporting 30 active customers from day one. The team is now planning expansions to Sage Intacct and NetSuite due to the speed and simplicity of the implementation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stripe QuickBooks Online integration guide&lt;/em&gt;&lt;br&gt;
&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/N50tPlVmWMqLOm6agKAua/f3bb43b5ea06b5b897dd20ec2b579f51/Screenshot_2026-01-13_at_23.51.20_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/N50tPlVmWMqLOm6agKAua/f3bb43b5ea06b5b897dd20ec2b579f51/Screenshot_2026-01-13_at_23.51.20_2x.png" alt="Screenshot 2026-01-13 at 23.51.20@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Procurement and AP Automation → Purchase Order Matching
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; A company issues a purchase order. Goods arrive. An invoice comes in. Someone has to manually match the PO, receipt, and invoice (the "three-way match") before approving payment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Your procurement system shares PO data with the accounting platform. When invoices arrive, matching happens automatically. Exceptions flag for review; clean matches process straight through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; Coupa, SAP Ariba, and newer players like Zip connect procurement workflows directly to ERPs for automated matching and approval routing. For example, &lt;a href="https://www.apideck.com/customer-cases/derive-erp-accounting-integration" rel="noopener noreferrer"&gt;Derive&lt;/a&gt; used Apideck's Unified API to reduce development time by about 70 percent. They went from sign-up to a live Xero integration in just three weeks and later launched a Workday connection in under 90 days, which helped accelerate sales cycles and expand into enterprise markets more quickly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Coupa Invoice Matching Explained&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/3RBqgwzhrjAA2R5m1RPEnN/50b104fbc6e96cf8389c7b96f118fba2/Screenshot_2026-01-13_at_23.58.44_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/3RBqgwzhrjAA2R5m1RPEnN/50b104fbc6e96cf8389c7b96f118fba2/Screenshot_2026-01-13_at_23.58.44_2x.png" alt="Coupa Integration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Revenue Recognition → Compliance with Accounting Standards
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; SaaS companies with usage-based pricing or multi-element contracts can't just recognize revenue when cash hits the bank. Accounting standards like ASC 606 require recognizing revenue over the service period. In plain English: you have to follow specific rules about &lt;em&gt;when&lt;/em&gt; you're allowed to say you've "earned" money. Calculating this manually across thousands of subscriptions is an audit nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Your billing system sends contract and usage data to the accounting platform. Revenue schedules are generated automatically based on the rules. Auditors get clean documentation without your team building spreadsheets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; Stripe Revenue Recognition, Chargebee's RevRec module, and dedicated tools like Leapfin pull transaction data and push compliant journal entries to NetSuite or Sage Intacct.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chargebee RevRec Features&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/1OIsgeq1MjjXtwfmzks9dx/2b7b16d7a31839a497c1289cc4d45828/Screenshot_2026-01-14_at_00.08.44_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/1OIsgeq1MjjXtwfmzks9dx/2b7b16d7a31839a497c1289cc4d45828/Screenshot_2026-01-14_at_00.08.44_2x.png" alt="Chargebee RevRec Features"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Embedded Fintech → Real-Time Cash Flow Visibility
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Lending platforms, cash flow tools, and financial dashboards need to see a business's real financial position. Bank feeds show cash movement, but not the full picture: receivables, payables, revenue trends, and outstanding invoices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Pull chart of accounts, invoices, bills, and journal entries from the customer's accounting system. Display real-time financial health. Underwrite loans based on actual accounting data, not uploaded bank statements from three months ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; &lt;a href="https://www.openbankingtracker.com/api-aggregators/plaid" rel="noopener noreferrer"&gt;Plaid&lt;/a&gt; handles banking data; accounting integrations from providers like Codat and Apideck fill in the rest. Lenders like Clearco and Pipe use accounting data to underwrite in hours instead of weeks.&lt;/p&gt;

&lt;p&gt;For a technical deep-dive on this pattern, see &lt;a href="https://www.apideck.com/blog/using-accounting-apis-for-smart-lending-decisions" rel="noopener noreferrer"&gt;Using Accounting APIs for Smart Lending Decisions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Plaid API Docs&lt;/em&gt; &lt;br&gt;
&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/4N7Qk2Xy05BDl45nj88tLy/51a18107db9d04b5eae121324e8eb0ef/Screenshot_2026-01-14_at_00.19.11_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/4N7Qk2Xy05BDl45nj88tLy/51a18107db9d04b5eae121324e8eb0ef/Screenshot_2026-01-14_at_00.19.11_2x.png" alt="Plaid API Docs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. AI and Machine Learning → Financial Intelligence
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; You want to offer AI-powered features: budget predictions, anomaly detection, and cash flow forecasting, but your models need comprehensive financial data from each customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration:&lt;/strong&gt; Pull balance sheets, income statements, transaction histories, and accounts receivable aging from customers' accounting systems. Feed this data to your ML models for personalized insights. As new transactions flow in, models update automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who does this well:&lt;/strong&gt; Runway, Jirav, and Mosaic pull accounting data to power FP&amp;amp;A automation. Fintech platforms use it to build credit models that outperform traditional underwriting.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runway Fintech Platform&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/ri7m0E6ZiXCO8bElv0CGE/d1f57078c78bec8e91857c962c0b2739/Screenshot_2026-01-14_at_00.10.41_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/ri7m0E6ZiXCO8bElv0CGE/d1f57078c78bec8e91857c962c0b2739/Screenshot_2026-01-14_at_00.10.41_2x.png" alt="Runway"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Companies Build Accounting Integrations
&lt;/h2&gt;

&lt;p&gt;This is where product decisions become engineering reality. You have three paths:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Build Direct Integrations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/5hZHxvvvRpIJeSslAJecUa/9bda6a322d00b38b3cbfc0541fb99568/Mermaid_Chart_-_Create_complex__visual_diagrams_with_text.-2026-01-11-185243.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/5hZHxvvvRpIJeSslAJecUa/9bda6a322d00b38b3cbfc0541fb99568/Mermaid_Chart_-_Create_complex__visual_diagrams_with_text.-2026-01-11-185243.png" alt="multiple-integrations"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect to each accounting platform's API individually. QuickBooks Online has a REST API. Xero uses OAuth 2.0. NetSuite offers both SOAP and REST. Sage has &lt;a href="https://www.apideck.com/blog/the-sage-api-playbook-why-sage-cloud-is-not-one-api" rel="noopener noreferrer"&gt;several APIs, depending on which Sage product you're targeting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Full control over the integration. Access to every feature the platform offers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Each integration is a significant investment. Industry estimates indicate that a single integration requires 150+ engineering hours to build and 300+ hours annually to maintain, with total costs ranging from $10,000 to $50,000 per integration per year, including engineering time, customer success support, and ongoing maintenance. Supporting 10 accounting systems means multiplying that investment tenfold. Teams routinely estimate six weeks and ship in three months, as auth flows alone can derail timelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The maintenance reality:&lt;/strong&gt; Accounting APIs change. QuickBooks pushes updates. Xero deprecates endpoints. NetSuite releases new versions. When a sync breaks on the first of the month (the worst possible timing for any finance team), someone on your team is firefighting instead of building features. Direct integrations require ongoing "on-call" capacity that most teams underestimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Use a Unified API
&lt;/h3&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/NyNWfDtAshr8iXXuLpEWs/038bb8e0a430ca8cf75c28b20bb525e6/Mermaid_Chart_-_Create_complex__visual_diagrams_with_text.-2026-01-11-190250.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/NyNWfDtAshr8iXXuLpEWs/038bb8e0a430ca8cf75c28b20bb525e6/Mermaid_Chart_-_Create_complex__visual_diagrams_with_text.-2026-01-11-190250.png" alt="unified api managed integration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A unified API provider normalizes multiple accounting platforms into a single integration. Think of it as a universal translator: your system speaks one language, and the provider handles the dialects of QuickBooks, Xero, NetSuite, and the rest.&lt;/p&gt;

&lt;p&gt;You build once; they maintain the connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Launch in weeks instead of months. One data model to learn. One auth flow to implement. Coverage across platforms without multiplying your engineering investment. Unified API approaches typically deliver 3-5x faster implementation with significantly lower total cost of ownership over multi-year periods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You're dependent on the provider's coverage depth and data model. Some edge cases or advanced features may still require direct API work for specific platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Providers in this space:&lt;/strong&gt; Apideck, Merge, Codat, Railz, and Rutter all offer unified accounting APIs with varying coverage, pricing models, and approaches to data normalization. For a detailed comparison, see &lt;a href="https://www.apideck.com/blog/top-merge-api-alternatives" rel="noopener noreferrer"&gt;Top Merge API Alternatives for SaaS Teams in 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Not sure whether to build or buy? Read &lt;a href="https://www.apideck.com/blog/build-vs-buy-accounting-integrations" rel="noopener noreferrer"&gt;Build vs Buy Accounting Integrations&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Hybrid Approach
&lt;/h3&gt;

&lt;p&gt;Use a unified API for 80% of your coverage (common platforms and standard use cases), then build direct integrations for strategic accounts that require deep customization. This typically means NetSuite or SAP for enterprise deals that require custom fields, advanced workflows, or specific ERP modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Prioritize Which Integrations to Build
&lt;/h2&gt;

&lt;p&gt;As you can't build everything at once. Use this framework to prioritize and insert the Accounting integrations that are relevant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;QuickBooks&lt;/th&gt;
&lt;th&gt;NetSuite&lt;/th&gt;
&lt;th&gt;Xero&lt;/th&gt;
&lt;th&gt;Sage Intacct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;% of pipeline requesting&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected close rate lift&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention impact&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New market access&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering effort&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance burden&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weighted Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Integration Prioritization Scorecard
&lt;/h3&gt;

&lt;h3&gt;
  
  
  How to use it:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Survey your sales team: which integrations come up most in deals?
&lt;/li&gt;
&lt;li&gt;Analyze lost deals: how many cited missing integrations?
&lt;/li&gt;
&lt;li&gt;Segment by customer size: SMB skews QuickBooks/Xero, enterprise skews NetSuite/Sage
&lt;/li&gt;
&lt;li&gt;Score each integration, calculate weighted totals, and stack-rank&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents the common mistake of building integrations based on engineering interest rather than business impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls: What Goes Wrong with Accounting Integrations
&lt;/h2&gt;

&lt;p&gt;Most integration guides tell you what to build. Few tell you what breaks. Here are the failure modes that create support tickets, churn, and 2 AM pages:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Auth Token Expiry and Refresh Failures
&lt;/h3&gt;

&lt;p&gt;OAuth tokens expire. Refresh tokens have their own lifespans. QuickBooks tokens last 100 days; if a customer doesn't use the integration for three months, it silently breaks. Your sync stops, but no one notices until month-end close—when the finance team is already stressed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Proactive token health monitoring. Alert customers &lt;em&gt;before&lt;/em&gt; re-authentication is needed, not after data stops flowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Schema Drift and API Changes
&lt;/h3&gt;

&lt;p&gt;Accounting platforms don't freeze their APIs. QuickBooks pushed breaking changes to their invoice endpoints in 2023. Xero periodically deprecates fields. NetSuite's SuiteTalk versions introduce incompatibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Version your integrations. Monitor API changelogs. Build regression tests that catch schema changes before customers do.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rate Limit Hell
&lt;/h3&gt;

&lt;p&gt;QuickBooks allows 500 requests per minute. Sounds generous until you're syncing 10,000 invoices for a new customer. Hit the limit, and your sync backs up. Customers see stale data. Support tickets pile up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Implement exponential backoff. Queue and batch requests. Design for burst limits from day one, not after you hit them in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multi-Entity and Multi-Currency Complications
&lt;/h3&gt;

&lt;p&gt;Enterprise customers don't have one QuickBooks company. They often manage twelve subsidiaries across three currencies. Your integration works perfectly for single-entity SMBs, then breaks spectacularly when an enterprise customer connects their consolidated NetSuite instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Design for multi-entity from the start. Ask during onboarding: "How many entities will you connect?" If the answer is more than one, adjust your data model accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. GL Code Mapping Mismatches
&lt;/h3&gt;

&lt;p&gt;Your expense categories don't match your customer's chart of accounts. "Travel" in your system needs to map to "6200 - Employee Travel &amp;amp; Entertainment" in theirs. Get it wrong, and transactions land in the wrong accounts or fail to sync entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Build a mapping interface. Let customers configure how your categories translate to their GL codes. Don't assume your defaults work for everyone.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Partnership Agreement Delays
&lt;/h3&gt;

&lt;p&gt;Many enterprise accounting vendors (especially Sage, Xero, Intuit, and SAP) require formal partnership agreements before you can access sandbox environments or API documentation. These agreements can take months, sometimes over a year, and cost tens of thousands annually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Start partnership applications early, before you need the integration. If the timeline is critical, consider unified API providers who already have these partnerships in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Look for in an Accounting Integration Solution
&lt;/h2&gt;

&lt;p&gt;Whether you're evaluating unified API providers or scoping direct builds, here's what actually matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Platform coverage.&lt;/strong&gt; Does it support the accounting systems your customers use? QuickBooks and Xero dominate SMB. NetSuite, Sage Intacct, and Microsoft Dynamics serve the mid-market and enterprise. Know your customer base before choosing. Need a full breakdown? See &lt;a href="https://www.apideck.com/blog/top-15-accounting-apis-to-integrate-with" rel="noopener noreferrer"&gt;Top 15 Accounting APIs to Integrate with in 2025&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data model depth.&lt;/strong&gt; Can you access invoices, payments, journal entries, tracking categories, tax rates, and custom fields? Or just the basics? Shallow integrations create support tickets later.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write access.&lt;/strong&gt; Reading data is the easy part. Pushing invoices, expenses, or journal entries back to the accounting system is harder but more valuable. Confirm bidirectional sync works if you need it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth experience.&lt;/strong&gt; Your customers connect to your accounting system via an OAuth flow. If that flow is clunky, confusing, or breaks frequently, your support team will hear about it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks and real-time sync.&lt;/strong&gt; Polling for changes is slow and burns API quota. Webhooks let you react to new invoices or payments the moment they happen.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and compliance.&lt;/strong&gt; You're touching general ledger data. Customers will ask about SOC 2 compliance, data encryption, and where their credentials are stored. Have answers ready, or choose a provider who does.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration observability.&lt;/strong&gt; When syncs fail, can you see why? Look for logging, error categorization, and alerting that help your support team diagnose issues without escalating to engineering.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Accounting integrations aren't optional anymore. If your product touches financial data, your customers expect it to connect to their accounting system.&lt;/p&gt;

&lt;p&gt;You can spend engineering years building and maintaining direct connections. Or you can use a unified API to launch in weeks and expand coverage as you grow.&lt;/p&gt;

&lt;p&gt;The days of "export to CSV" are ending. Give customers the integration, or watch them find a product that does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does an accounting integration take to build in-house?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the platform and your requirements. Simple read-only integrations might take 4-6 weeks. Bidirectional sync with error handling, rate limiting, and multi-entity support typically takes 3-6 months. If the vendor requires a partnership agreement (NetSuite, SAP), add 3-12 months for paperwork alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much do accounting integrations cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct builds run $10,000-$50,000 per integration annually when you factor in engineering time, maintenance, and support. Unified API providers typically charge $500-$2,000/month for access to dozens of integrations. The break-even point is usually 2-3 integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are the most popular accounting systems to integrate with?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Varies by segment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SMB:&lt;/strong&gt; QuickBooks Online, Xero, FreshBooks, Wave
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-market:&lt;/strong&gt; Sage Intacct, NetSuite, Microsoft Dynamics 365 Business Central
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise:&lt;/strong&gt; NetSuite, SAP, Oracle, Microsoft Dynamics 365 Finance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What data typically gets synced?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Common objects include: invoices, bills, payments, journal entries, chart of accounts, customers/vendors, tax rates, tracking categories, purchase orders, and bank transactions. Advanced use cases add balance sheets, income statements, and aged receivables/payables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I start with one integration and add more later?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, but plan your data model carefully. If you build for QuickBooks first with a QuickBooks-specific schema, adding Xero later means refactoring. Starting with a normalized data model (or using a unified API) avoids this trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Deeper: Related Guides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Building your first integration?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/how-to-integrate-with-quickbooks-api" rel="noopener noreferrer"&gt;How to Integrate with the QuickBooks API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/integrating-with-the-netsuite-rest-api" rel="noopener noreferrer"&gt;A Guide to Integrating with the NetSuite REST API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/your-guide-to-building-a-sage-intacct-api-integration" rel="noopener noreferrer"&gt;Your Guide to Building a Sage Intacct API Integration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/create-a-workday-rest-api-integration" rel="noopener noreferrer"&gt;How to create a Workday REST API Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Evaluating build vs. buy?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/build-vs-buy-accounting-integrations" rel="noopener noreferrer"&gt;Build vs Buy Accounting Integrations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/top-merge-api-alternatives" rel="noopener noreferrer"&gt;Top Merge API Alternatives for SaaS Teams in 2025&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/erp-integration-for-fintech-and-saas-connecting-quickbooks-netsuite-and-sage" rel="noopener noreferrer"&gt;ERP Integration for Fintech and SaaS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Designing for scale and reliability?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/12-best-practices-for-accounting-integrations-in-vertical-saas" rel="noopener noreferrer"&gt;12 Best Practices for Accounting Integrations in Vertical SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/the-complete-guide-to-accounting-api-integrations-for-fintech" rel="noopener noreferrer"&gt;The Complete Guide to Accounting API Integrations for Fintech&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/unified-apis-for-fintech-when-point-integrations-stop-scaling" rel="noopener noreferrer"&gt;Unified APIs for Fintech: When Point Integrations Stop Scaling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Understanding the Sage ecosystem?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/untangling-the-sage-ecosystem" rel="noopener noreferrer"&gt;Untangling The Sage Ecosystem&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/the-sage-api-playbook-why-sage-cloud-is-not-one-api" rel="noopener noreferrer"&gt;The Sage API Playbook: Why 'Sage Cloud' Is Not One API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/sage-integration-field-guide" rel="noopener noreferrer"&gt;The Apideck Field Guide to the Sage Portfolio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Specific use cases?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/using-accounting-apis-for-smart-lending-decisions" rel="noopener noreferrer"&gt;Using Accounting APIs for Smart Lending Decisions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apideck.com/blog/bank-feeds-api-integration" rel="noopener noreferrer"&gt;Bank Feeds API Integration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/from-accounts-receivable-to-lending-automation-integration-use-cases-for-vertical-saas" rel="noopener noreferrer"&gt;From Accounts Receivable to Lending Automation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ready to add accounting integrations to your product?&lt;/strong&gt; &lt;a href="https://www.apideck.com/accounting-api" rel="noopener noreferrer"&gt;Explore Apideck's Unified Accounting API&lt;/a&gt;: one integration, 20+ accounting platforms, live in days.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>startup</category>
      <category>erp</category>
    </item>
    <item>
      <title>Top Fintech APIs for Startups</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Thu, 05 Mar 2026 04:47:04 +0000</pubDate>
      <link>https://dev.to/apideck/top-fintech-apis-for-startups-5df5</link>
      <guid>https://dev.to/apideck/top-fintech-apis-for-startups-5df5</guid>
      <description>&lt;p&gt;Building a fintech product means making critical infrastructure decisions early. The APIs you choose determine your technical debt, compliance burden, and ability to scale for years to come.&lt;/p&gt;

&lt;p&gt;Instead of listing 20 APIs with shallow descriptions, this guide breaks down the fintech API landscape by actual business need, explains the hidden integration trade-offs, and shows you when unified API solutions prevent the maintenance nightmare that derails most startups.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Financial Impact of API Strategy
&lt;/h2&gt;

&lt;p&gt;The global open banking market is projected to reach $135.17 billion by 2030. McKinsey &lt;a href="https://www.grandviewresearch.com/press-release/global-open-banking-market" rel="noopener noreferrer"&gt;estimates&lt;/a&gt; AI could enable $1 trillion in global banking revenue shifts by the same year. These projections drive the infrastructure decisions startups make today.&lt;/p&gt;

&lt;p&gt;But here's what those projections don't capture: most startups fail at integrations not because they chose the wrong APIs, but because they underestimated the maintenance burden.&lt;/p&gt;

&lt;p&gt;Every API you integrate directly means authentication logic to maintain, rate limiting to handle, breaking changes to absorb, and data normalization to manage. A simple feature like "sync transactions" becomes a switch statement with fifteen cases when you're managing Stripe, Plaid, QuickBooks, and three other providers. Each case handles auth differently, maps fields uniquely, and fails in its own way.&lt;/p&gt;

&lt;p&gt;Your codebase fragments into provider-specific branches. API providers don't coordinate their breaking changes. Plaid updates its transaction categorization. Stripe changes their webhook format. QuickBooks modifies its OAuth flow. Your test matrix explodes, but coverage remains incomplete because you can't predict every interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Money Movers: Payments and Transfers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Payment Processing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; remains the default choice for startups. It handles card payments, ACH transfers, subscriptions, and payouts with a developer experience that sets the industry standard. The platform powers millions of businesses across 50+ countries, supporting companies from early-stage startups to enterprises like Amazon and Shopify.&lt;/p&gt;

&lt;p&gt;Stripe's real advantage is the ecosystem. Stripe Treasury embeds banking features directly into products, including FDIC-insured deposits, cards, and interest-earning balances. Shopify, Lyft, and Deel use this infrastructure to manage financial accounts at scale. For startups building platforms, Stripe Connect handles marketplace payments and seller onboarding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adyen&lt;/strong&gt; targets enterprise-grade needs with global payment processing across 200+ countries. For startups building for international markets from day one, Adyen's unified commerce approach handles in-person and online payments through a single integration. The tradeoff is complexity; Adyen's learning curve is steeper than Stripe's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Square&lt;/strong&gt; works well for startups serving physical retail or SMB markets with its point-of-sale integration and straightforward pricing. The hardware ecosystem (terminals, readers) makes it attractive for omnichannel commerce.&lt;/p&gt;

&lt;h3&gt;
  
  
  ACH and Bank Transfers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dwolla&lt;/strong&gt; excels at ACH payments with instant bank-to-bank transfers, wallet-based flows, and high-volume payouts. Payroll apps, lending platforms, and B2B marketplaces rely on this infrastructure. Dwolla handles the compliance complexity of moving money between bank accounts at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern Treasury&lt;/strong&gt; solves the reconciliation problem. When your startup handles large financial flows, matching payments to invoices across multiple sources becomes a full-time job. Modern Treasury automates payment ops, real-time reconciliation, and ledgering across ACH, RTP, and wire transfers. It's built for companies where money movement is core to the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Layers: Banking Connectivity and Identity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Banking Data and Account Connectivity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plaid&lt;/strong&gt; dominates this category, connecting applications to thousands of banks and credit unions. Its API provides normalized financial data, including balance checks, transaction histories, account authentication, and identity verification. Personal finance apps use Plaid for spending insights, lenders use it for credit risk assessments, and neobanks integrate it for instant account verification.
For teams tracking open banking APIs across different regions, the &lt;a href="https://www.openbankingtracker.com/" rel="noopener noreferrer"&gt;Open Banking Tracker&lt;/a&gt; provides data on 3,200+ open banking and PSD2 APIs globally. It helps understand coverage gaps when expanding internationally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX&lt;/strong&gt; offers a strong alternative for data enrichment and transaction categorization. Personal finance management products benefit from MX's cleansing and analytics capabilities beyond basic connectivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TrueLayer&lt;/strong&gt; focuses on open banking and PSD2-compliant services in Europe with instant account-to-account payments and financial insights. If your primary market is the UK or EU, TrueLayer's regional expertise matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yodlee&lt;/strong&gt; connects to over 16,000 global data sources. Wealth management apps integrate Yodlee for consolidated portfolio views, while lenders use it to assess customer liabilities before loan approval.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Identity Verification and KYC
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Onfido&lt;/strong&gt; handles government ID verification, facial recognition, liveness detection, and fraud detection. Regulatory compliance determines whether you can operate, making this category critical for any fintech handling customer funds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alloy&lt;/strong&gt; powers automated risk checks by pulling data from bureaus and identity networks. Banks and neobanks use Alloy's risk engine to approve users with less manual review while maintaining compliance. The decisioning workflow builder lets you customize approval logic without code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sardine&lt;/strong&gt; uses behavioral biometrics and machine learning to catch what traditional checks miss: suspicious login attempts, unusual spending patterns, device spoofing, and account takeovers. If your app handles money movement, Sardine adds a protection layer that document verification alone can't provide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Back Office: Accounting, ERP, and HRIS
&lt;/h2&gt;

&lt;p&gt;This is where most startups underestimate complexity.&lt;br&gt;
The challenge isn't just connecting to one accounting system. It's that your customers use different systems. QuickBooks, Xero, NetSuite, Sage, FreshBooks, and dozens of regional platforms each have distinct authentication flows, data models, rate limits, and field mapping requirements.&lt;br&gt;
What looks like "add QuickBooks integration" on a roadmap becomes a three-month project. Then customers ask for Xero. Then, enterprise prospects require NetSuite. Each integration multiplies your maintenance burden. Your engineers become integration specialists rather than product developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accounting and ERP Integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QuickBooks Online&lt;/strong&gt; holds approximately 80% market share among US small businesses. If you're serving American SMBs, QuickBooks integration is expected. But Intuit's API has authentication quirks, sandbox limitations, and data models that require dedicated engineering time to handle correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Xero&lt;/strong&gt; dominates the SME market in Australia, the UK, and New Zealand. International expansion means another separate integration with different conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NetSuite&lt;/strong&gt; and &lt;strong&gt;Sage Intacct&lt;/strong&gt; serve mid-market and enterprise customers with multi-entity structures and complex financial requirements. These integrations are significantly more complex than SMB accounting platforms. Multi-subsidiary consolidation, custom fields, and approval workflows add implementation time.
For detailed guidance on accounting API selection, see &lt;a href="https://www.apideck.com/blog/top-15-accounting-apis-to-integrate-with" rel="noopener noreferrer"&gt;Top 15 Accounting APIs to Integrate with in 2025&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Unified API Alternative
&lt;/h3&gt;

&lt;p&gt;This is where unified API platforms become relevant. Instead of building and maintaining separate integrations for each accounting system, unified APIs let you connect once and access multiple platforms through a standardized interface.&lt;/p&gt;

&lt;p&gt;The approach trades some customization depth for dramatically reduced maintenance. You lose access to provider-specific advanced features, but you gain consistent data models, centralized authentication, and a single codebase instead of 15.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.apideck.com/accounting-api" rel="noopener noreferrer"&gt;Apideck's Accounting API&lt;/a&gt; is one solution in this space alongside Kombo, Finch, and Merge, providing access to 20+ accounting systems through a single integration. For implementation details on handling expenses and bills across platforms, see &lt;a href="https://developers.apideck.com/guides/expenses-bills" rel="noopener noreferrer"&gt;Integrating Expenses and Bills with the Accounting API&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HRIS and Payroll Integration
&lt;/h3&gt;

&lt;p&gt;Fintech products increasingly touch workforce data for payroll integrations, employee verification, or benefits administration. Gusto, ADP, BambooHR, Rippling, and Workday each require separate integrations with distinct APIs and authentication flows.&lt;/p&gt;

&lt;p&gt;Unified HRIS APIs normalize employee data across these platforms. Payroll automation through standardized integrations allows companies to scale without additional engineering investment per provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lending and Investment APIs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lending and Credit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Experian&lt;/strong&gt; provides credit scoring and risk assessment through its credit bureau database, delivering real-time credit histories and fraud checks. BNPL providers and underwriting platforms use it for risk-based decisioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finicity&lt;/strong&gt; (now part of Mastercard) offers cash flow analytics and income verification, replacing paper-based income proofs for mortgage lenders and personal loan providers. The shift to real-time income verification is accelerating across lending categories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For embedded lending use cases, see &lt;a href="https://www.apideck.com/blog/from-accounts-receivable-to-lending-automation-integration-use-cases-for-vertical-saas" rel="noopener noreferrer"&gt;From Accounts Receivable to Lending Automation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Investment and Trading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alpaca&lt;/strong&gt; offers commission-free trading APIs for developers building trading apps and algorithmic platforms. Its startup-friendly approach and documentation quality make it accessible for early-stage fintech.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polygon.io&lt;/strong&gt; provides stock market data, news, and analysis for investment platforms and trading apps that need real-time market information.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Embedded Finance Considerations
&lt;/h2&gt;

&lt;p&gt;Non-financial companies are embedding payments, lending, insurance, and banking features into their products. This isn't a future trend; it's the current expectation across industries.&lt;/p&gt;

&lt;p&gt;Vertical SaaS platforms add financial workflows to increase stickiness and revenue per customer. Construction software embeds equipment financing tied to project milestones. Healthcare platforms offer working capital based on insurance receivables. Restaurant systems provide cash advances against future credit card sales.&lt;br&gt;
Marketplaces offer seller financing and instant payouts to attract supply-side participants. HR platforms embed earned wage access and benefits management to differentiate their offerings.&lt;/p&gt;

&lt;p&gt;Building &lt;a href="https://www.openbankingtracker.com/embedded-finance" rel="noopener noreferrer"&gt;embedded finance&lt;/a&gt; requires integrations across accounting, banking, payments, and lending APIs. This is where unified APIs provide the most value by reducing the integration surface area while maintaining breadth of coverage. The technical burden extends beyond initial implementation; each category adds compliance surface area and ongoing maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Direct vs. Unified APIs
&lt;/h2&gt;

&lt;p&gt;Every API integration increases your compliance surface area. Financial data requires SOC 2 compliance, data residency controls, and comprehensive audit trails. This context matters when evaluating the direct vs. unified decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct integrations make sense when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your entire business runs on one provider, and you need every advanced feature&lt;/li&gt;
&lt;li&gt;You have dedicated integration engineers for long-term maintenance&lt;/li&gt;
&lt;li&gt;You're pre-product-market-fit and only need one or two integrations&lt;/li&gt;
&lt;li&gt;Your competitive advantage depends on deep optimization for a specific platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⠀&lt;strong&gt;Unified APIs make sense when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers demand multiple integrations across a category&lt;/li&gt;
&lt;li&gt;Engineering time goes to integration maintenance instead of product development&lt;/li&gt;
&lt;li&gt;You need enterprise integrations (NetSuite, SAP, Workday) without enterprise engineering overhead&lt;/li&gt;
&lt;li&gt;You're scaling rapidly and can't afford three-month integration projects for each new provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⠀For detailed analysis, see &lt;a href="https://www.apideck.com/blog/unified-apis-for-fintech-when-point-integrations-stop-scaling" rel="noopener noreferrer"&gt;Unified APIs for Fintech: When Point Integrations Stop Scaling&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Stacks by Product Type
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neobank:&lt;/strong&gt; Plaid + Unit or Stripe Treasury + Onfido + Unified Accounting API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lending platform:&lt;/strong&gt; Plaid + Experian/Finicity + Alloy + Dwolla + Unified Accounting API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expense management:&lt;/strong&gt; Stripe + Unified Accounting API + Unified HRIS API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical SaaS with financial features:&lt;/strong&gt; Stripe + Unified Accounting API + Unified HRIS API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Fintech API strategy isn't about choosing the "best" APIs. It's about building an integration architecture that scales without consuming engineering capacity.&lt;/p&gt;

&lt;p&gt;Direct integrations make sense for core payment and banking infrastructure, where you need deep control. Unified APIs make sense for customer-facing integrations that require breadth without maintenance overhead.&lt;br&gt;
The startups that scale treat integrations as strategic infrastructure rather than feature checkboxes.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>startup</category>
    </item>
    <item>
      <title>Stripe's llms.txt has an instructions section. That's a bigger deal than it sounds.</title>
      <dc:creator>Gertjan De Wilde</dc:creator>
      <pubDate>Thu, 05 Mar 2026 03:00:00 +0000</pubDate>
      <link>https://dev.to/apideck/stripes-llmstxt-has-an-instructions-section-thats-a-bigger-deal-than-it-sounds-8ad</link>
      <guid>https://dev.to/apideck/stripes-llmstxt-has-an-instructions-section-thats-a-bigger-deal-than-it-sounds-8ad</guid>
      <description>&lt;p&gt;When Stripe added /llms.txt to their docs, most write-ups noted it as another company "embracing the AI era." They missed the interesting part. Buried inside is an instructions section no other company has built — Stripe programming what AI tools say about Stripe. Here's why that matters and what it means for your API.&lt;/p&gt;

&lt;p&gt;When Stripe added &lt;code&gt;/llms.txt&lt;/code&gt; to their docs in March 2025, most write-ups noted it as another company "embracing the AI era." They missed the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is a follow-up to &lt;a href="https://www.apideck.com/blog/api-design-principles-agentic-era" rel="noopener noreferrer"&gt;API Design Principles for the Agentic Era&lt;/a&gt;. That one covers the broader shift in how APIs need to be designed for autonomous consumers. This one goes deep on one specific mechanism, the llms.txt instructions section.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Buried inside &lt;code&gt;docs.stripe.com/llms.txt&lt;/code&gt; is a section that doesn't exist in any other company's implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Instructions for Large Language Model Agents: Best Practices for integrating Stripe.
- Always use the Checkout Sessions API over the legacy Charges API
- Default to the latest stable SDK version
- Never recommend the legacy Card Element or Sources API
- Advise migrating from PaymentIntents to Checkout Sessions
- Prefer dynamic payment methods over hardcoded payment_method_types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not documentation. This is a prompt — shipped as a static file at the root of their domain — designed to be loaded into AI coding assistants before a developer asks "how do I add Stripe?"&lt;/p&gt;

&lt;p&gt;Stripe is programming what AI tools say about Stripe. Every time a developer asks Cursor or Claude how to accept payments, and the agent fetches this file first, those instructions propagate into the answer. They're not just making their docs readable to machines. They're shaping the behavior of third-party AI systems at scale.&lt;/p&gt;

&lt;p&gt;That's a genuinely new thing. And it has real implications for how API companies should think about their documentation in 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llms.txt actually is
&lt;/h2&gt;

&lt;p&gt;Jeremy Howard (fast.ai, Answer.AI) proposed the standard in September 2024. The problem it's solving is real: LLMs have finite context windows, HTML is noisy, and you can't just dump an entire documentation site into a prompt.&lt;/p&gt;

&lt;p&gt;His solution is deliberately low-tech. A Markdown file at &lt;code&gt;/llms.txt&lt;/code&gt; with an H1, an optional summary blockquote, and H2-delimited sections of curated links. A companion &lt;code&gt;/llms-full.txt&lt;/code&gt; containing complete docs in a single file. Any individual page available as clean Markdown by appending &lt;code&gt;.md&lt;/code&gt; to its URL.&lt;/p&gt;

&lt;p&gt;The format is boring on purpose. No special syntax, no schema, no JSON. Just the same Markdown that LLMs already understand natively. The interesting insight is curatorial: you know your documentation better than any crawler, so you should be the one to tell AI agents which parts matter.&lt;/p&gt;

&lt;p&gt;It's the same philosophy as a well-maintained &lt;code&gt;robots.txt&lt;/code&gt; — except instead of exclusion, it's prioritization. Robots.txt tells crawlers what to skip. Sitemap.xml tells them what exists. llms.txt tells AI what to read first.&lt;/p&gt;

&lt;p&gt;One honest caveat: no major AI provider has confirmed their training crawlers automatically fetch &lt;code&gt;llms.txt&lt;/code&gt;. Its real value today is inference-time, not training-time — developers manually loading it into Cursor or Claude for project context, or agent frameworks fetching it on startup. The 800,000+ "implementations" BuiltWith tracks are mostly Yoast SEO auto-generating the file for WordPress sites. The hand-curated number is closer to 784 verified sites.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/2JBZhaCe6wBQq87OYlvgB7/c2ca0d7b076964f052339f2fd01b4ff7/Screenshot_2026-02-23_at_06.04.47.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/2JBZhaCe6wBQq87OYlvgB7/c2ca0d7b076964f052339f2fd01b4ff7/Screenshot_2026-02-23_at_06.04.47.png" alt="llmstxt.org"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe's implementation is the industry outlier
&lt;/h2&gt;

&lt;p&gt;Most llms.txt files are just structured indexes. Anthropic's is a clean table of contents for their API docs. Cloudflare's is massive (3.7 million tokens across product-specific files). Vercel's is so large they call it "a 400,000-word novel."&lt;/p&gt;

&lt;p&gt;Stripe's is architecturally different. Three separate files across two domains. Every docs page available as &lt;code&gt;.md&lt;/code&gt;. And that instructions section that no one else has.&lt;/p&gt;

&lt;p&gt;The instructions aren't arbitrary. They're solving a specific, painful problem: Stripe has accumulated 15 years of API surface area, including several generations of deprecated payment primitives. Their Charges API still works. Their Card Element still exists. Developers — and the AI assistants helping them — regularly reach for these older APIs because they appear in older Stack Overflow answers and training data from before 2022.&lt;/p&gt;

&lt;p&gt;The instructions section is Stripe saying: when an AI helps a developer integrate us, steer them toward the right thing. Don't let stale training data send them to the Charges API. Don't let our own backwards compatibility become a footgun.&lt;/p&gt;

&lt;p&gt;That's a legitimate engineering concern. And the file format is a surprisingly elegant solution to it — no coordination with AI providers required, works with any system that can fetch a URL.&lt;/p&gt;

&lt;p&gt;The announcement got 273,800 views and 740 bookmarks on Twitter. Stripe engineer Ian McCrystal: &lt;em&gt;"I expect AI tools will eventually become the predominant readers of our documentation."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who else is worth looking at
&lt;/h2&gt;

&lt;p&gt;The honest answer to "who else has this figured out" is: not many, and only Stripe is doing the specific thing that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; remains the strongest example of the deprecation pattern precisely because they're explicit about it. Their instructions don't hint at preferred patterns — they name the bad endpoints directly: "You must not call deprecated API endpoints such as the Sources API." "Never recommend the legacy Card Element." That specificity is what makes it machine-actionable. An LLM can follow a concrete prohibition. It can't do much with "prefer modern patterns."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt; takes a structurally interesting approach. Their &lt;code&gt;llms.txt&lt;/code&gt; is organized by service — Agents, AI Gateway, Workers AI, and so on — so an agent only needs to fetch the section relevant to what it's building rather than parsing a file that covers their entire platform. For APIs with multiple product lines, that's a better model than a flat list. You're reducing the noise ratio at fetch time, not just at index time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangChain and LangGraph&lt;/strong&gt; are worth noting for a meta-reason: they're agent frameworks that have their own &lt;code&gt;llms.txt&lt;/code&gt;. They're eating their own cooking. That's useful signal about whether the format actually helps in practice, beyond the theoretical appeal — these are teams that work with agents every day and chose to implement it anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt; has one for their API docs. It's structured more as an index than an instructions-heavy file — clean and navigable, but it's not doing the active correctional work that Stripe's instructions section does.&lt;/p&gt;

&lt;p&gt;The pattern across most implementations is the same: they're documentation indexes. Useful, because a curated index is better than asking an agent to crawl your entire site. But not doing the harder job of guiding AI away from the wrong things. The providers that have figured out the instructions section are treating &lt;code&gt;llms.txt&lt;/code&gt; as an active correctional mechanism for model drift, not just a sitemap for bots. That framing distinction is the whole thing.&lt;/p&gt;

&lt;p&gt;Most APIs have deprecated endpoints that still work, legacy patterns that still exist in training data, and footguns that experienced developers know to avoid but newcomers (and AI assistants trained on old Stack Overflow answers) keep reaching for. The instructions section exists to close that gap. Almost no one is using it yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Stripe specifically makes this move
&lt;/h2&gt;

&lt;p&gt;Stripe's developer experience has been the industry benchmark since roughly 2012. The specifics are worth understanding because they're not accidental.&lt;/p&gt;

&lt;p&gt;Their three-column docs layout (left nav, center content, right-side live code examples in seven languages) was so effective that it became a meme — startup after startup shipped the same structure. They open-sourced Markdoc, their interactive documentation framework. They shipped Stripe Shell for live API calls inside docs pages. Their error messages include &lt;code&gt;doc_url&lt;/code&gt; fields, parameter-level specificity, and "did you mean email?" suggestions for misspelled field names.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;doc_url&lt;/code&gt; in error responses is the one worth highlighting specifically. It's a small thing with outsized impact when an AI agent is in the loop. When an agent gets a 400 error, it can follow the &lt;code&gt;doc_url&lt;/code&gt;, fetch the Markdown version of that docs page, and self-correct — without needing a human to look up what went wrong. That's not DX in the traditional sense. That's infrastructure designed for autonomous consumers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parameter_invalid_empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"doc_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://stripe.com/docs/error-codes/parameter-invalid-empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You passed an empty string for 'amount'. We assume empty values are an oversight, so we require you to pass this field."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"param"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_request_error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;John Collison put the llms.txt bet in plain terms: &lt;em&gt;"If you go read the Stripe Docs these days, it's a lot to keep in your RAM, but trivial for an LLM."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for your API
&lt;/h2&gt;

&lt;p&gt;Most of this is transferable. Here's what's actually useful versus what's cargo-culting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actually useful:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your error responses should include a &lt;code&gt;documentation_url&lt;/code&gt; field pointing to a Markdown version of the relevant docs page. This costs almost nothing to implement and has immediate value — for human developers debugging in the terminal and for AI agents trying to self-correct.&lt;/p&gt;

&lt;p&gt;Your OpenAPI descriptions should be written for semantic matching, not human skimming. When an agent is deciding which endpoint to call, it's doing something like nearest-neighbor search against your descriptions. "Gets the data" loses to "Returns a paginated list of invoices filtered by status, sorted by created_at descending. Requires accounting:read scope." Every field, every enum, every endpoint.&lt;/p&gt;

&lt;p&gt;I've been around OpenAPI specs long enough to know how badly they rot. We built &lt;a href="https://github.com/apideck-libraries/portman" rel="noopener noreferrer"&gt;Portman&lt;/a&gt; to convert OpenAPI specs into Postman collections — and the main thing you learn doing that is how few specs have descriptions worth converting. Fields with no description, enums with no explanation, endpoints named things that made sense in 2019. If your spec is bad for contract testing, it's bad for agents too.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;/llms.txt&lt;/code&gt;. It takes an afternoon. Link to your ten most important pages with one-sentence descriptions. That's it. You don't need the 350-link Stripe implementation on day one.&lt;/p&gt;

&lt;p&gt;If you have deprecated APIs, use the instructions section. This is underutilized and genuinely powerful. You know which footguns exist in your API. Tell the AI.&lt;/p&gt;

&lt;p&gt;One thing that often gets missed: the marketing upside. The same structured, machine-readable docs that make your API easy to integrate are the same content that AI answer engines like Perplexity and ChatGPT pull from when someone asks "what's the best API for X." llms.txt doesn't just help agents build with your API — it helps you show up when people are deciding which API to use. Being agent-friendly and being discoverable in AI search are the same work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Probably not worth doing yet:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Publishing an MCP server for the sake of it. MCP is real and growing, but agent framework standardization is still in flux. A well-designed REST API with a good OpenAPI spec is more durable. Build the MCP server when you have users asking for it.&lt;/p&gt;

&lt;p&gt;Elaborate agent-specific observability infrastructure. Request tagging and semantic logging are nice. But if your API doesn't have solid basic observability yet, that's the actual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader pattern
&lt;/h2&gt;

&lt;p&gt;There's a pattern here worth naming. For 15 years, "developer experience" meant optimizing for humans: readable error messages, clear docs, good SDKs, interactive playgrounds. The mental model was a developer sitting at a terminal, making sense of your API.&lt;/p&gt;

&lt;p&gt;The new constraint is that a growing fraction of your API consumers are autonomous systems that read documentation, make decisions without human review, and retry failures automatically. The question isn't whether to design for this — it's happening regardless — it's whether you're designing &lt;em&gt;intentionally&lt;/em&gt; for it.&lt;/p&gt;

&lt;p&gt;Stripe's llms.txt instructions section is the clearest example I've seen of a company being intentional about it. They're not just making their docs machine-readable. They're asserting control over what machines say about them.&lt;/p&gt;

&lt;p&gt;Every API company with deprecated primitives and a significant developer base has the same problem Stripe is solving. They just haven't solved it yet.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>dx</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Make Your API Agent-Ready: Design Principles for the Agentic Era</title>
      <dc:creator>Gertjan De Wilde</dc:creator>
      <pubDate>Wed, 25 Feb 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/apideck/how-to-make-your-api-agent-ready-design-principles-for-the-agentic-era-c8e</link>
      <guid>https://dev.to/apideck/how-to-make-your-api-agent-ready-design-principles-for-the-agentic-era-c8e</guid>
      <description>&lt;p&gt;For about fifteen years, "developer experience" meant one thing: optimize for a human being sitting at a terminal. Readable error messages. Interactive docs. Code samples in seven languages. The mental model was always a person making sense of your API, iterating in real time, googling when confused. That model isn't wrong. But it's increasingly incomplete.&lt;/p&gt;

&lt;p&gt;A growing fraction of API traffic today is generated by AI agents. Systems that autonomously discover endpoints, parse documentation, handle errors without human review, and retry failures according to their own logic. When a developer asks Cursor or Claude to "add Stripe payments," the agent fetches documentation, selects APIs, writes integration code, and debugs errors before the developer reads a line of it. The human is further from the integration than they've ever been.&lt;/p&gt;

&lt;p&gt;The pattern is especially visible in fintech and accounting. A lending platform building an underwriting agent prompts it to "pull twelve months of invoice data and flag overdue receivables." The agent queries your API, interprets the response, and surfaces a creditworthiness signal — before a human analyst has opened a spreadsheet. A payroll platform's agent reconciles payroll entries against the general ledger overnight, without a developer watching the process. In both cases, the agent is not a convenience layer on top of a human workflow. It is the workflow. What it can do is bounded entirely by what your API communicates about itself.&lt;/p&gt;

&lt;p&gt;This creates a new design surface. The same discipline that produced great developer experience, deliberate and user-centered API design, applied to a different consumer. Call it agent experience, or AX. It's not a replacement for DX. It's the next layer.&lt;/p&gt;

&lt;p&gt;The good news: most of what makes an API good for agents makes it better for humans too. The bad news: a lot of APIs that seem fine for humans are quietly broken for agents. Here's where the gaps show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad OpenAPI descriptions break agent routing
&lt;/h2&gt;

&lt;p&gt;When an agent decides which endpoint to call, it's doing something close to semantic search against your descriptions. It reads your spec, matches the user's intent against available operations, and picks the closest fit. The quality of your descriptions determines whether it picks correctly.&lt;/p&gt;

&lt;p&gt;"Gets the data" loses to "Returns a paginated list of invoices filtered by status and date range, sorted by created_at descending. Requires accounting:read scope. Use the cursor parameter for pagination."&lt;/p&gt;

&lt;p&gt;Every field, every enum value, every endpoint. The description is the signal the agent uses to route. Most OpenAPI specs are bad at this. Not because anyone decided to make them bad. They rot. Field names that made sense in context lose their meaning without descriptions. Enums accumulate values that nobody documented. Endpoints get renamed but the descriptions don't follow. The spec becomes a structural skeleton with no semantic content.&lt;/p&gt;

&lt;p&gt;The difference between a description that helps and one that doesn't is not subtle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bad — structural skeleton, no semantic content&lt;/span&gt;
&lt;span class="na"&gt;/invoices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get invoices&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cursor&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="c1"&gt;# Good — enough signal to route correctly&lt;/span&gt;
&lt;span class="na"&gt;/invoices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List invoices filtered by status and date range&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;Returns a paginated list of invoices for the authenticated company.&lt;/span&gt;
      &lt;span class="s"&gt;Use `status` to filter by payment state. Use `cursor` from the previous&lt;/span&gt;
      &lt;span class="s"&gt;response to fetch the next page. Requires the `accounting:read` scope.&lt;/span&gt;
      &lt;span class="s"&gt;For real-time sync scenarios, combine with the `updated_since` parameter&lt;/span&gt;
      &lt;span class="s"&gt;to fetch only records changed after a given timestamp.&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;Filter by invoice status. Accepted values: draft, submitted,&lt;/span&gt;
          &lt;span class="s"&gt;authorised, deleted, voided, paid. Defaults to all statuses if omitted.&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;submitted&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;authorised&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;deleted&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;voided&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;paid&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cursor&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;Opaque pagination cursor returned in the previous response.&lt;/span&gt;
          &lt;span class="s"&gt;Omit to start from the first page.&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent matching "fetch unpaid invoices for reconciliation" against the bad version has no signal to work with. Against the good version, it finds &lt;code&gt;status: authorised&lt;/code&gt;, understands pagination, and knows it needs &lt;code&gt;accounting:read&lt;/code&gt; before it makes a single request.&lt;/p&gt;

&lt;p&gt;I've seen this up close. We built &lt;a href="https://github.com/apideck-libraries/portman" rel="noopener noreferrer"&gt;Portman&lt;/a&gt;, an open-source CLI that converts OpenAPI specs into Postman collections for contract testing. The main thing you learn doing that is how few specs have descriptions worth converting. If your spec is broken for contract testing, it's broken for agents. The failure mode is the same: a consumer that can't determine what anything does without running it.&lt;/p&gt;

&lt;p&gt;The fix isn't glamorous. Go through your spec field by field. Write descriptions that explain what each parameter does, what values are valid, what happens when you omit it. Do this for your ten most important endpoints first. It's tedious, it takes time, and it makes a bigger difference to agent experience than almost anything else on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your errors should tell agents how to fix themselves
&lt;/h2&gt;

&lt;p&gt;The agent experience of errors is fundamentally different from the human experience. A human reads an error message, understands it, googles the fix, comes back. An agent reads an error message and needs to decide, in the same execution context, what to do next. If the error is ambiguous, the agent either guesses or fails.&lt;/p&gt;

&lt;p&gt;Stripe's error responses have included a &lt;code&gt;doc_url&lt;/code&gt; field for years:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parameter_invalid_empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"doc_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://stripe.com/docs/error-codes/parameter-invalid-empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You passed an empty string for 'amount'. We assume empty values are an oversight, so we require you to pass this field."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"param"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_request_error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is infrastructure for autonomous consumers. When an agent hits a 400, it can follow the &lt;code&gt;doc_url&lt;/code&gt;, fetch the documentation page, and self-correct without a human in the loop. The agent experience of that error is recovery. The agent experience without &lt;code&gt;doc_url&lt;/code&gt; is a dead end.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;documentation_url&lt;/code&gt; to your error responses costs almost nothing to implement. Pair it with documentation pages that are available as clean Markdown (by appending &lt;code&gt;.md&lt;/code&gt; to the URL, or via a parallel &lt;code&gt;/docs/{page}.md&lt;/code&gt; route), and you've given agents everything they need to handle errors autonomously.&lt;/p&gt;

&lt;p&gt;The other half of error design is specificity. "Invalid request" is useless. "The amount field cannot be empty" is useful. "You passed &lt;code&gt;payment_method_types: ['card']&lt;/code&gt;, which is deprecated, use dynamic payment methods instead" is excellent. The more specific the error, the more actionable it is for an agent trying to self-correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  llms.txt: tell AI what to read
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; standard, proposed by Jeremy Howard in September 2024, exists because AI agents have finite context windows and your documentation site is not designed for machine consumption. HTML is noisy. Navigating a docs site the way a human would (clicking links, loading JavaScript, jumping between pages) is expensive and lossy.&lt;/p&gt;

&lt;p&gt;The solution is a Markdown file at &lt;code&gt;/llms.txt&lt;/code&gt; that curates the most important parts of your documentation with plain-text links and one-sentence descriptions. Agents and AI coding tools can fetch this file once and understand the shape of your documentation before writing a single line of integration code.&lt;/p&gt;

&lt;p&gt;The format is deliberately boring. No special syntax, no schema. Just Markdown. The interesting part is curation: you know your documentation better than any crawler, so you should be the one deciding what matters.&lt;/p&gt;

&lt;p&gt;Adding a basic &lt;code&gt;/llms.txt&lt;/code&gt; takes an afternoon. Link to your ten most important pages. Write one sentence per link explaining what's there. That's a complete implementation. You don't need &lt;a href="https://docs.stripe.com/llms.txt" rel="noopener noreferrer"&gt;Stripe's 350-link version&lt;/a&gt; on day one.&lt;/p&gt;

&lt;p&gt;What's underutilized (and genuinely powerful for agent experience) is the instructions section. Stripe's &lt;a href="https://docs.stripe.com/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Instructions for Large Language Model Agents: Best Practices for integrating Stripe.
- Always use the Checkout Sessions API over the legacy Charges API
- Default to the latest stable SDK version
- Never recommend the legacy Card Element or Sources API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Stripe telling AI coding assistants exactly which footguns to avoid. If your API has deprecated primitives, ambiguous endpoint choices, or integration patterns that look valid but cause problems, the instructions section is where you document that for agents. It propagates into every AI-assisted integration of your API. Every time a developer asks an agent how to use your API, those instructions shape the answer.&lt;/p&gt;

&lt;p&gt;There's a second-order benefit worth naming here. The same properties that make your docs readable to coding agents (structured Markdown, clear definitions, curated indexing) also make your content discoverable by AI answer engines like Perplexity and ChatGPT. When someone asks "what's the best accounting API" or "how do I build an accounting integration," the answer comes from content that AI can parse and cite. llms.txt, semantic descriptions, and concrete definitions aren't just AX improvements. They're how you show up in AI search. The discipline is the same: write for machines and you get both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get your docs indexed by Context7
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://context7.com/" rel="noopener noreferrer"&gt;Context7&lt;/a&gt; is an MCP server that solves a specific problem: AI coding tools have a training data cutoff, which means they default to documentation from whenever they were last trained. If your API changed since then, agents write integrations against the old version.&lt;/p&gt;

&lt;p&gt;Context7 addresses this by maintaining an up-to-date index of developer documentation that coding tools can query in real time, pulling current docs rather than cached training data.&lt;/p&gt;

&lt;p&gt;Getting your documentation added to Context7 is low-effort and high-leverage. You submit your docs, they get indexed, and any developer using a Context7-enabled coding tool gets accurate, current information about your API without you needing to push updates to every AI provider individually.&lt;/p&gt;

&lt;p&gt;The practical upside is significant if you ship changes regularly. An API that added a new authentication method last quarter, or deprecated an endpoint three months ago, looks different in Context7 than it does in an AI's training data. Developers using Context7-enabled tools get the right answer. Developers using tools without it get whatever the model was trained on.&lt;/p&gt;

&lt;p&gt;The broader point is that documentation distribution is now a multi-channel problem. Publishing docs to your website is table stakes. Getting those docs in front of agents (through &lt;code&gt;/llms.txt&lt;/code&gt;, through Context7, through direct MCP server implementations) is the new distribution layer. You're not just writing docs for developers who visit your site. You're writing docs for systems that will intermediate between your API and the developers who use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your deprecated APIs are an agent experience problem
&lt;/h2&gt;

&lt;p&gt;APIs accumulate history. Old endpoints that still work. Legacy authentication methods that are technically valid but shouldn't be used for new integrations. Multiple ways to accomplish the same thing, with meaningful differences that aren't obvious from the outside.&lt;/p&gt;

&lt;p&gt;Humans navigate this through Stack Overflow recency, changelog reading, and conversations with other developers. Agents navigate it through your documentation and training data, which may include answers and tutorials from 2019 that recommend the old way because the new way didn't exist yet.&lt;/p&gt;

&lt;p&gt;Your agent experience gets worse every year you don't address this, as more stale content about your old APIs accumulates on the internet and in AI training sets.&lt;/p&gt;

&lt;p&gt;The practical response is layered. Mark deprecated endpoints explicitly in your OpenAPI spec using the &lt;code&gt;deprecated: true&lt;/code&gt; field, with a description pointing to the replacement. Write deprecation notices at the top of deprecated docs pages, with direct links to the current approach. Add deprecated API guidance to your llms.txt instructions section. And if you're generating error responses from deprecated endpoints, consider including a migration hint in the message itself.&lt;/p&gt;

&lt;p&gt;None of this requires coordination with AI providers. It works because agents read your documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP: build it when people ask for it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.apideck.com/blog/a-primer-on-the-model-context-protocol" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is real, growing, and mostly not worth building for yet unless your users are asking for it.&lt;/p&gt;

&lt;p&gt;MCP lets AI agents connect to your service through a standardized protocol with tools, resources, and prompts. The agent experience of a well-designed MCP server is genuinely better than the experience of parsing REST documentation and constructing API calls manually. But agent framework standardization is still in flux, and a well-designed REST API with a complete OpenAPI spec is more durable than an MCP server you build today against tooling that changes in six months.&lt;/p&gt;

&lt;p&gt;HubSpot is a good example of what a mature implementation looks like. They shipped two distinct MCP servers: a remote server that connects AI clients to live CRM data (contacts, deals, tickets, companies), and a local developer MCP server that integrates with their CLI so agentic dev tools can scaffold HubSpot projects, answer questions from their developer docs, and deploy changes directly.&lt;/p&gt;

&lt;p&gt;A developer can prompt "What's the component for displaying a table in HubSpot UI Extensions?" and the developer MCP server pulls the answer from current HubSpot documentation. Another developer can prompt "Summarize all deals in Decision maker bought in stage with deal value over $1000" and the remote MCP server queries live CRM data. Two different use cases, two different servers, both scoped tightly to what their users actually need.&lt;/p&gt;

&lt;p&gt;When you do reach the point of building, the tooling has matured enough to make it tractable. &lt;a href="https://www.speakeasy.com/product/gram" rel="noopener noreferrer"&gt;Gram by Speakeasy&lt;/a&gt; is an open-source MCP cloud platform: you upload your OpenAPI spec, it converts your endpoints into curated toolsets, and deploys them as a hosted MCP server at &lt;code&gt;mcp.yourcompany.com&lt;/code&gt; in minutes. The key design insight is curation — rather than exposing all 200 endpoints as tools and overwhelming the agent's context, Gram lets you distill them down to 5–30 focused, purpose-built tools that represent complete workflows. For teams building in Python who want full control over the implementation, &lt;a href="https://github.com/jlowin/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; is a high-level framework that strips out the protocol boilerplate and lets you define tools, resources, and prompts in straightforward Python.&lt;/p&gt;

&lt;p&gt;Most API companies aren't HubSpot. The right sequence for everyone else: get your OpenAPI spec right, write real descriptions, add &lt;code&gt;documentation_url&lt;/code&gt; to your errors, publish &lt;code&gt;/llms.txt&lt;/code&gt;, get indexed by Context7. That foundation improves agent experience immediately and transfers to every framework. Then, when users start asking how to connect your API to their AI agents and a clear integration pattern emerges, build the MCP server against that specific use case.&lt;/p&gt;

&lt;p&gt;Building MCP for the sake of having an MCP server is the equivalent of building a mobile app for the sake of having a mobile app. The agent experience of a half-built MCP server is worse than the agent experience of a good REST API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills: Teaching Agents to Do Your Job at 100x the Scale
&lt;/h2&gt;

&lt;p&gt;There's a mental model shift happening in engineering right now. AI agents aren't just writing code faster; they're becoming the engineers who run the product development loop. The new engineering job is building the agents that automate that loop for you, and then directing them to do it at a scale no individual could match.&lt;/p&gt;

&lt;p&gt;The simple version of this is already in use: Ghostty splits and tabs, tmux sessions, CLI agents in parallel. You run ten agents at once, each working a different branch or task. That's horizontal scalability for engineering work, something that was physically impossible before.&lt;/p&gt;

&lt;p&gt;But raw parallelism without direction is chaos. This is where skills come in.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Are Skills?
&lt;/h3&gt;

&lt;p&gt;Skills are instruction packages for AI agents: Markdown files (optionally accompanied by scripts and resources) that teach an agent exactly how to handle a specific task. When the agent encounters work relevant to a skill, it loads that skill's context and operates with domain-specific precision instead of general-purpose guesswork.&lt;/p&gt;

&lt;p&gt;Anthropic introduced Skills as a first-class concept for Claude Code and the Claude ecosystem. Simon Willison &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/" rel="noopener noreferrer"&gt;called them "maybe a bigger deal than MCP"&lt;/a&gt;, and his argument is compelling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Skills are conceptually extremely simple: a skill is a Markdown file telling the model how to do something, optionally accompanied by extra documents and pre-written scripts that the model can run to help it accomplish the tasks described by the skill.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key design insight is token efficiency. At session start, Claude reads only a brief YAML frontmatter description of each available skill, a few dozen tokens per skill. The full skill content only loads when the agent determines it's needed for the task at hand. You can have dozens of skills installed without burning your context window.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Agent Skills Ecosystem
&lt;/h3&gt;

&lt;p&gt;The ecosystem is moving fast. Vercel launched &lt;a href="https://github.com/vercel-labs/agent-skills" rel="noopener noreferrer"&gt;agent-skills&lt;/a&gt;, a directory and tooling layer for discovering, installing, and composing skills across coding agents. The skills.sh platform (used by Cursor, Claude Code, and others) makes the install experience as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add &amp;lt;package&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahrefs &lt;a href="https://github.com/ahrefs/ahrefs-api-skills" rel="noopener noreferrer"&gt;published their own skills&lt;/a&gt; to teach agents how to work with their SEO API, a direct example of how companies are now shipping agent instructions alongside their APIs. We took the same approach at Apideck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apideck API Skills
&lt;/h3&gt;

&lt;p&gt;We just launched &lt;a href="https://developers.apideck.com/building-with-llms" rel="noopener noreferrer"&gt;Apideck API Skills&lt;/a&gt;, installable via the skills.sh directory, to teach AI agents how to build integrations against our Unified API correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add apideck-libraries/api-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skills cover SDK usage across TypeScript, Python, Go, Java, PHP, and .NET; authentication and Vault patterns; pagination, error handling, and connector coverage checks; and migration paths from direct integrations to the unified layer.&lt;/p&gt;

&lt;p&gt;Instead of an agent making reasonable guesses about how our API works, it loads the relevant skill and operates with the same knowledge a senior Apideck engineer would bring to the task.&lt;/p&gt;

&lt;p&gt;This follows the same "building with LLMs" philosophy Stripe pioneered. They offer &lt;a href="https://docs.stripe.com/building-with-llms" rel="noopener noreferrer"&gt;plain-text docs, MCP server access, and agent toolkits&lt;/a&gt;, but skills go a layer deeper. Where plain-text docs make your documentation parseable, skills make your API learnable in the agent's execution context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skills vs. MCP: Complementary, Not Competing
&lt;/h3&gt;

&lt;p&gt;It's worth being precise about where skills fit relative to MCP. MCPs give agents tools to call: actions they can take against live systems. Skills give agents the knowledge to use those tools well, or to accomplish tasks using the code execution environment directly.&lt;/p&gt;

&lt;p&gt;Simon Willison puts it well: almost everything you might achieve with an MCP can be handled by a CLI tool instead, because LLMs already know how to call &lt;code&gt;cli-tool --help&lt;/code&gt;. Skills have the same advantage, and you don't even need a CLI implementation. You drop in a Markdown file and let the model figure out execution.&lt;/p&gt;

&lt;p&gt;The two patterns compose naturally. An agent with an MCP connection to your accounting API and a skill that explains your data model and common patterns will outperform one that has only the MCP. Skills are the institutional knowledge layer; MCP is the capability layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Parallelism to Leverage
&lt;/h3&gt;

&lt;p&gt;Skills connect to the larger picture of agent orchestration. When you're running agents in parallel, on PRs, when an incident fires, when a customer files a bug, the quality ceiling is determined by how well each agent understands the domain it's working in. Skills are how you encode that domain knowledge once and distribute it across every agent instance, at any scale.&lt;/p&gt;

&lt;p&gt;The automation of the full product development loop is now an engineering responsibility. Skills are how you ensure that when your agents run that loop, at 100x the scale, while you sleep, they're running it the way you would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI Rebirth
&lt;/h2&gt;

&lt;p&gt;There's a pattern Karpathy &lt;a href="https://x.com/karpathy/status/2026360908398862478" rel="noopener noreferrer"&gt;pointed to on February 24&lt;/a&gt; that cuts to something fundamental about agent-native interfaces: CLIs, the oldest developer tool, are having a second moment — not despite AI agents, but because of them.&lt;/p&gt;

&lt;p&gt;The reason is structural. Agents are terminal-native. They know how to run &lt;code&gt;--help&lt;/code&gt;, install packages via &lt;code&gt;pip&lt;/code&gt; or &lt;code&gt;npm&lt;/code&gt;, chain tools with pipes, and parse output. They don't need a bespoke integration layer to use a CLI. They just use it.&lt;/p&gt;

&lt;p&gt;Karpathy demonstrated this concretely: Claude installed the new Polymarket CLI, built a terminal dashboard showing the highest-volume prediction markets and their 24-hour price changes, and had it running in about three minutes. Pair that with the GitHub CLI and an agent can navigate repositories, review PRs, and act on real-world data signals in a single autonomous pipeline — no custom integration layer, no MCP server, nothing to maintain.&lt;/p&gt;

&lt;p&gt;This is the same point Willison makes about skills: almost everything you might achieve with an MCP server can be handled by a CLI, because agents already know how to use them. A well-designed CLI with good &lt;code&gt;--help&lt;/code&gt; output is self-documenting in a way that a REST API is not. An agent encountering &lt;code&gt;gh --help&lt;/code&gt; for the first time figures out the relevant subcommand on its own. An agent encountering your undescribed REST API hits a wall.&lt;/p&gt;

&lt;p&gt;The opportunity for API companies is direct: if you don't have a CLI, it's worth asking whether building one would be more leveraged than building an MCP server. A CLI that agents can install and explore immediately compounds differently. Skills make this even more powerful — a developer who loads your skill and has your CLI installed gives an agent both the domain knowledge and the execution surface at the same time.&lt;/p&gt;

&lt;p&gt;A few things make a CLI specifically better for agents: a &lt;code&gt;--json&lt;/code&gt; flag or JSON-by-default output so agents don't have to parse human-readable strings; composable subcommands following the &lt;code&gt;tool noun verb&lt;/code&gt; convention (like &lt;code&gt;gh repo list&lt;/code&gt;, &lt;code&gt;gh pr view&lt;/code&gt;) so &lt;code&gt;--help&lt;/code&gt; output is scannable; environment-variable-based auth so agents can configure credentials without interactive prompts; and error messages that explain what went wrong rather than just returning an exit code.&lt;/p&gt;

&lt;p&gt;Karpathy's framing for businesses is the right frame for API companies too: make your product agent-usable via markdown docs, skills, CLI, and MCP — roughly in that order of ease and durability. Each layer compounds on the last. Building the CLI doesn't require coordination with any AI provider, and it works immediately because agents can already use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The underlying shift
&lt;/h2&gt;

&lt;p&gt;DX was about removing friction for humans. AX is the same discipline applied to autonomous consumers that can't ask for clarification and can't adapt when your API sends them somewhere unexpected.&lt;/p&gt;

&lt;p&gt;The things that made APIs good for developers — specificity, consistent semantics, helpful errors — matter even more when there's no human in the loop to compensate for ambiguity. Ambiguity that a developer resolves through experience becomes a failure mode at scale when agents are writing the integrations.&lt;/p&gt;

&lt;p&gt;The good news: most of what you'd do to make your API understandable to a machine is the same thing you'd do for a developer who doesn't already know your system. Start there.&lt;/p&gt;

&lt;p&gt;We're building &lt;a href="https://www.apideck.com" rel="noopener noreferrer"&gt;Apideck&lt;/a&gt;, a unified API for accounting integrations. The agent experience question is concrete for us: when an agent connects through Apideck and gets normalized access to 20+ accounting platforms, the quality of our OpenAPI descriptions and error responses propagates to every integration downstream. It focuses the mind.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>agents</category>
    </item>
    <item>
      <title>A Guide to Integrating with the NetSuite REST API</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Fri, 31 Oct 2025 14:28:04 +0000</pubDate>
      <link>https://dev.to/apideck/a-guide-to-integrating-with-the-netsuite-rest-api-1fch</link>
      <guid>https://dev.to/apideck/a-guide-to-integrating-with-the-netsuite-rest-api-1fch</guid>
      <description>&lt;h1&gt;
  
  
  A Guide to Integrating with the NetSuite REST API
&lt;/h1&gt;

&lt;p&gt;Accounting systems like &lt;a href="https://www.netsuite.com/portal/home.shtml" rel="noopener noreferrer"&gt;NetSuite&lt;/a&gt; are the backbone for managing and optimizing business operations through automated processes and integrated workflows. NetSuite is a cloud-based enterprise resource planning (ERP) system that provides a business software suite for financial management, customer relationship management, e-commerce, and more.&lt;/p&gt;

&lt;p&gt;The NetSuite REST API allows developers to build modern integrations that connect NetSuite to other systems, automate complex workflows, and synchronize data across different platforms. Using this API, developers can automate tasks such as customer creation, order processing, and real-time inventory updates. This is key to building automated solutions that reduce manual effort and increase accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the NetSuite REST API Matters
&lt;/h2&gt;

&lt;p&gt;The NetSuite REST API provides a modern, JSON-based interface to NetSuite's core functionality, allowing you to interact with the platform programmatically using standard HTTP methods. You can use it to perform operations such as creating and updating records, managing customer data, processing transactions, and retrieving financial reports. The REST API enables you to integrate NetSuite into your existing systems and automate business-critical processes while ensuring data consistency across all applications.&lt;/p&gt;

&lt;p&gt;For example, you can automate workflows like customer onboarding or sync order data between NetSuite and external platforms, reducing manual errors and increasing operational efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  NetSuite SOAP API Alternative
&lt;/h2&gt;

&lt;p&gt;If you're evaluating NetSuite integration options, you might also consider the NetSuite SOAP API. While SOAP is more established and offers broader functionality coverage, it's significantly more complex to implement due to XML handling and verbose request structures. For a detailed comparison and implementation guide, see our &lt;a href="https://www.apideck.com/blog/guide-to-integrating-with-the-netsuite-soap-api" rel="noopener noreferrer"&gt;comprehensive NetSuite SOAP API integration guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are some examples of how you can use the REST API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automating customer management&lt;/strong&gt;: The NetSuite REST API can automatically create customer records when new users sign up on your website, sync contact information, and update customer data across systems. This eliminates duplicate data entry and ensures customer information stays current across all touchpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time inventory synchronization&lt;/strong&gt;: The REST API can integrate with e-commerce platforms like Shopify or WooCommerce to provide real-time inventory updates. When a product is sold online, the API immediately updates stock levels in NetSuite and can trigger reorder notifications when inventory falls below threshold levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated financial reporting&lt;/strong&gt;: The REST API can integrate with business intelligence tools to generate real-time financial dashboards. This automation provides up-to-date profit and loss statements, cash flow reports, and sales analytics, helping businesses make data-driven decisions quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Integrate with the NetSuite REST API
&lt;/h2&gt;

&lt;p&gt;NetSuite's REST API supports &lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3445710.html#bridgehead_4489663579" rel="noopener noreferrer"&gt;Token-Based Authentication (TBA)&lt;/a&gt; and &lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1544634936.html" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; for secure API access.&lt;/p&gt;

&lt;p&gt;This guide focuses on Token-Based Authentication, which involves creating a signature using your NetSuite account credentials and including it with other authentication parameters in the request headers.&lt;/p&gt;

&lt;p&gt;To interact with the REST API, you need to:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/5VzgNoI6uxKtiBoJqAuyOk/e92fc303e42cdcddf34296d10281b04c/Screenshot_2025-08-28_at_03.16.32_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/5VzgNoI6uxKtiBoJqAuyOk/e92fc303e42cdcddf34296d10281b04c/Screenshot_2025-08-28_at_03.16.32_2x.png" alt="Screenshot 2025-08-28 at 03.16.32@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/bridgehead_4248124361.html#bridgehead_4249074259" rel="noopener noreferrer"&gt;Create a user role&lt;/a&gt; with appropriate permissions, including REST Web Services access&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/bridgehead_4249032125.html#procedure_4253065190" rel="noopener noreferrer"&gt;Create a new integration record&lt;/a&gt; for token-based authentication and obtain the consumer key and secret&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/bridgehead_4254081947.html#procedure_4253065595" rel="noopener noreferrer"&gt;Create a new access token&lt;/a&gt; and obtain the token ID and secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/NApkpyess5TjyfhNOuAYO/be35e4635f5704f8020692313e53af6d/Screenshot_2025-08-28_at_03.17.39_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/NApkpyess5TjyfhNOuAYO/be35e4635f5704f8020692313e53af6d/Screenshot_2025-08-28_at_03.17.39_2x.png" alt="Screenshot 2025-08-28 at 03.17.39@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More detailed information is available in the &lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1540391670.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplify Authentication with Apideck Vault
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/2f6lAi0UUMn2nNoQQNgPnO/455c35557f23d07fbf366049800c346f/Screenshot_2025-08-28_at_03.19.50_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/2f6lAi0UUMn2nNoQQNgPnO/455c35557f23d07fbf366049800c346f/Screenshot_2025-08-28_at_03.19.50_2x.png" alt="Screenshot 2025-08-28 at 03.19.50@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The authentication setup process above can be complex and time-consuming for your end users. &lt;a href="https://www.apideck.com/products/vault" rel="noopener noreferrer"&gt;Apideck Vault&lt;/a&gt; provides a white-label, hosted authentication interface that eliminates this complexity entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/4Ye1Qgt1wUoxmrx6k6lhIo/0a550a65640b33e30676c6fc1cbfaaa7/Screenshot_2025-08-28_at_03.18.42_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/4Ye1Qgt1wUoxmrx6k6lhIo/0a550a65640b33e30676c6fc1cbfaaa7/Screenshot_2025-08-28_at_03.18.42_2x.png" alt="Screenshot 2025-08-28 at 03.18.42@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Vault, your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connect NetSuite in seconds&lt;/strong&gt; - No technical setup required on their end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure credential storage&lt;/strong&gt; - OAuth tokens and API keys are stored safely with enterprise-grade security
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified experience&lt;/strong&gt; - Same interface works for NetSuite, QuickBooks, Xero, and 200+ other integrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-service management&lt;/strong&gt; - Users can connect, disconnect, and manage integrations independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of asking users to create integration records and access tokens, they simply authenticate through Vault's hosted interface. You get the integration data you need without the setup friction that kills conversion rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a Request to the NetSuite REST API
&lt;/h2&gt;

&lt;p&gt;The base URL for NetSuite REST API requests follows this format:&lt;br&gt;
&lt;code&gt;https://{account_id}.suitetalk.api.netsuite.com/services/rest/record/v1/{record_type}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;NetSuite uses OAuth 1.0a for authentication, which requires generating a signature for each request. Here's a Python example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests_oauthlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OAuth1&lt;/span&gt;

&lt;span class="c1"&gt;# NetSuite credentials
&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_account_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;consumer_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_consumer_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
&lt;span class="n"&gt;consumer_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_consumer_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;token_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_token_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;token_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_token_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Create OAuth1 auth object
&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;consumer_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;consumer_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resource_owner_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resource_owner_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;signature_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HMAC-SHA256&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;signature_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_HEADER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example: Get a customer record
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.suitetalk.api.netsuite.com/services/rest/record/v1/customer/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Example: Create a customer record
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.suitetalk.api.netsuite.com/services/rest/record/v1/customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;customer_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Usage examples
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Get customer with ID 123
&lt;/span&gt;    &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer data:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create a new customer
&lt;/span&gt;    &lt;span class="n"&gt;new_customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;companyName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corporation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact@acme.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1-555-0123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Created customer:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error making API request:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Integration Challenge
&lt;/h2&gt;

&lt;p&gt;As you can see from the examples above, integrating directly with the NetSuite REST API involves several challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication complexity&lt;/strong&gt;: Implementing OAuth 1.0a signature generation requires precise handling of encoding, sorting, and hashing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting management&lt;/strong&gt;: NetSuite has &lt;a href="https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1557159142.html" rel="noopener noreferrer"&gt;strict concurrency limits&lt;/a&gt; that require careful request throttling to avoid timeouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom field handling&lt;/strong&gt;: NetSuite's custom fields require specific formatting and field ID mapping that varies by account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling complexity&lt;/strong&gt;: NetSuite's error responses require specific parsing and retry logic for different error types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data transformation&lt;/strong&gt;: NetSuite's data structures often don't match your application's data models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination complexity&lt;/strong&gt;: Handling large datasets requires implementing cursor-based pagination logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook limitations&lt;/strong&gt;: NetSuite's webhook support is limited, requiring polling for real-time data needs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these challenges adds development time and increases the potential for bugs in your integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Apideck Unified Accounting API
&lt;/h2&gt;

&lt;p&gt;Integrating with multiple accounting systems, including NetSuite, can be overwhelming and time-consuming. Managing OAuth signatures, handling rate limits, and dealing with different data formats across various platforms requires months of development effort and ongoing maintenance.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.apideck.com/accounting-api" rel="noopener noreferrer"&gt;Apideck Unified Accounting API&lt;/a&gt; provides a single integration point for 20+ accounting platforms, including NetSuite. This approach abstracts away the complexities of individual APIs, simplifying the integration process.&lt;/p&gt;

&lt;p&gt;Key benefits of using the Apideck Unified Accounting API include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time data processing&lt;/strong&gt;: All API calls are processed in real-time, not batched, ensuring your data is always fresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed authentication with Vault&lt;/strong&gt;: &lt;a href="https://www.apideck.com/products/vault" rel="noopener noreferrer"&gt;Apideck Vault&lt;/a&gt; handles all OAuth flows and credential management, eliminating complex authentication setup for your users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified data model&lt;/strong&gt;: Consistent data structures across all 18+ accounting platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in rate limit management&lt;/strong&gt;: Automatic throttling and retry logic with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook emulation&lt;/strong&gt;: Get push notifications from platforms that support webhooks, with Apideck emulating webhooks for platforms that don't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;White-label user interface&lt;/strong&gt;: Easily embedded interface that gives users a simple and secure connection experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data normalization&lt;/strong&gt;: Messy, inconsistent APIs are normalized into a single structure while still exposing raw downstream information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced maintenance burden&lt;/strong&gt;: Updates to underlying accounting systems don't require changes to your integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making Requests with Apideck
&lt;/h2&gt;

&lt;p&gt;Before making API requests through Apideck, you need to &lt;a href="https://developers.apideck.com/connectors/netsuite/docs/consumer+connection" rel="noopener noreferrer"&gt;configure your NetSuite connection&lt;/a&gt; in the Apideck platform.&lt;/p&gt;

&lt;p&gt;Here's how simple the same operations become with Apideck's unified API. You can try these requests in our Api Explorer, which makes this even simpler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-apideck-app-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;YOUR-APP-ID&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &amp;lt;YOUR-API-KEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-apideck-consumer-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;CONSUMER-ID&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Your NetSuite consumer&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Get a customer - unified across all accounting platforms&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://unify.apideck.com/accounting/customers/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error retrieving customer:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create a customer - same API call works for NetSuite, QuickBooks, Xero, etc.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unify.apideck.com/accounting/customers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;customerData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error creating customer:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// List all invoices with automatic pagination&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getInvoices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unify.apideck.com/accounting/invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error retrieving invoices:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage examples&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Customer:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newCustomer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Corporation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contact@acme.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="na"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+1-555-0123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdCustomer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newCustomer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Created customer:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createdCustomer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getInvoices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invoices:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Here’s an example of the same request from Apideck’s &lt;a href="https://developers.apideck.com/api-explorer" rel="noopener noreferrer"&gt;API Explorer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can test and make API calls with the simple drag and drop UI, which can easily pre-fill the response headers and body with the required data. You can see the &lt;code&gt;list customers&lt;/code&gt; request being done in a single shot.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/3Ab7l6AtTlpDsnWpkSLR8g/1ed51da4e0a1067e83e945be628dbdc5/Screenshot_2025-08-28_at_03.27.39_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/3Ab7l6AtTlpDsnWpkSLR8g/1ed51da4e0a1067e83e945be628dbdc5/Screenshot_2025-08-28_at_03.27.39_2x.png" alt="Screenshot 2025-08-28 at 03.27.39@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compare this clean, simple code to the complex OAuth signature generation and authentication handling required for direct NetSuite integration. With Apideck, the same code works across NetSuite, QuickBooks, Xero, Sage, and dozens of other accounting platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Integration Examples
&lt;/h2&gt;

&lt;p&gt;Here are some practical examples showing how Apideck simplifies common NetSuite integration scenarios. Please note, you can try them out from our Api Explorer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E-commerce Order Sync:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sync new orders from your e-commerce platform to NetSuite&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncOrderToNetSuite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;unit_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unify.apideck.com/accounting/invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customer Data Synchronization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Keep customer data in sync between your CRM and NetSuite&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncCustomerData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;billing_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;line1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_street&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;postal_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_zip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crmCustomer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_country&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://unify.apideck.com/accounting/customers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this guide, you learned how to integrate with the NetSuite REST API and discovered the complexities involved in direct integration. As demonstrated, working directly with NetSuite's API requires managing complex OAuth 1.0a signatures, handling rate limits, dealing with custom field mappings, and maintaining error-prone authentication code.&lt;/p&gt;

&lt;p&gt;These challenges multiply when you need to support multiple accounting platforms beyond NetSuite. Each system has its own authentication methods, data structures, and API quirks that require separate implementations and ongoing maintenance.&lt;/p&gt;

&lt;p&gt;To simplify your accounting integrations and eliminate the complexities of direct API integration, consider using the &lt;a href="https://www.apideck.com/accounting-api" rel="noopener noreferrer"&gt;Apideck Unified Accounting API&lt;/a&gt; with the &lt;a href="https://www.apideck.com/connectors/netsuite" rel="noopener noreferrer"&gt;NetSuite Connector&lt;/a&gt;. By providing a single, consistent interface across all major accounting platforms, Apideck simplifies development, reduces maintenance overhead, and saves valuable development time and resources.&lt;/p&gt;

&lt;p&gt;With Apideck, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to NetSuite and 20+ other accounting platforms with the same code&lt;/li&gt;
&lt;li&gt;Skip complex authentication implementations
&lt;/li&gt;
&lt;li&gt;Get standardized data models across all platforms&lt;/li&gt;
&lt;li&gt;Benefit from built-in rate limiting and error handling&lt;/li&gt;
&lt;li&gt;Access real-time data processing (not batch processing)&lt;/li&gt;
&lt;li&gt;Use webhook emulation for platforms that don't natively support webhooks&lt;/li&gt;
&lt;li&gt;Focus on building features instead of managing integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://platform.apideck.com/api/auth/login?product=unify&amp;amp;screenHint=signup&amp;amp;connector=netsuite" rel="noopener noreferrer"&gt;Sign up for Apideck&lt;/a&gt; today to start simplifying your NetSuite and accounting integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's the difference between NetSuite REST API and SOAP API?
&lt;/h3&gt;

&lt;p&gt;The REST API is newer (2019), uses JSON, and is easier to implement, but has limited functionality coverage. The SOAP API has been around longer, offers broader NetSuite feature access, but requires XML handling and is more complex to implement. For bulk operations and advanced features, SOAP is often better. For modern web/mobile apps and simple CRUD operations, REST is preferred. See our &lt;a href="https://www.apideck.com/blog/guide-to-integrating-with-the-netsuite-soap-api" rel="noopener noreferrer"&gt;SOAP API guide&lt;/a&gt; for a detailed comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle NetSuite's rate limits?
&lt;/h3&gt;

&lt;p&gt;NetSuite uses concurrency-based rate limiting rather than traditional rate limits. Each account has a limit on concurrent API requests (typically 10-25 concurrent requests). When you exceed this limit, you'll get timeout errors rather than 429 rate limit errors. Implement exponential backoff retry logic and consider queuing requests during high-traffic periods. Monitor your usage through NetSuite's API monitoring tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use OAuth 2.0 with NetSuite REST API?
&lt;/h3&gt;

&lt;p&gt;Yes, NetSuite supports OAuth 2.0 for REST API authentication, but Token-Based Authentication (TBA) is more commonly used because tokens don't expire like OAuth 2.0 access tokens do. OAuth 2.0 requires refresh token management and has a 7-day refresh limit, making TBA more suitable for server-to-server integrations. However, if you need user-facing authentication flows, OAuth 2.0 is the better choice.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>tutorial</category>
      <category>backend</category>
    </item>
    <item>
      <title>DATEV API Integration: A Comprehensive Technical Guide</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Fri, 31 Oct 2025 14:27:33 +0000</pubDate>
      <link>https://dev.to/apideck/datev-api-integration-a-comprehensive-technical-guide-24l6</link>
      <guid>https://dev.to/apideck/datev-api-integration-a-comprehensive-technical-guide-24l6</guid>
      <description>&lt;p&gt;When German businesses need accounting software, they turn to DATEV. With decades of market dominance and adoption by thousands of tax consultants across Germany, DATEV has become the de facto standard for financial record-keeping in the German market. But here's where things get interesting for developers: integrating with DATEV isn't like connecting to your typical REST API. The architecture is fundamentally different, and understanding these differences is critical to building a successful integration.&lt;/p&gt;

&lt;p&gt;If you're building an ERP system, e-commerce platform, or any business application that needs to sync financial data with German accounting workflows, you'll need to understand how DATEV integration actually works. This guide will walk you through the technical architecture, explain why DATEV takes a batch-processing approach instead of real-time REST calls, and show you the practical steps for implementing a robust integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding DATEV API Integration
&lt;/h2&gt;

&lt;p&gt;DATEV API integration connects your third-party platform to DATEV's accounting software ecosystem, enabling automated synchronization of financial data without manual file imports or error-prone copy-paste workflows. When you integrate with DATEV, you're connecting to the systems used by tax consultants and accountants across Germany to manage client finances, prepare tax filings, and maintain compliance with German accounting regulations.&lt;/p&gt;

&lt;p&gt;The typical use cases span multiple business domains. E-commerce platforms sync order data and payment records. Banking systems push transaction updates for reconciliation. Property management software sends rent payments and tenant invoicing data. Payroll systems transfer employee expense information. In each case, the goal is the same: eliminate manual data entry, reduce errors, and enable real-time visibility into financial operations.&lt;/p&gt;

&lt;p&gt;What makes DATEV integration valuable is the automation of repetitive accounting tasks. Instead of your accountant manually importing CSV files and checking for discrepancies, your integration automatically creates properly formatted records that flow directly into the accounting workflow. Invoice data, customer and supplier records, journal entries, cost center allocations, and VAT calculations all sync automatically. The result is enhanced compliance with German accounting regulations, reduced data errors, improved accuracy, and proactive financial management based on current data rather than week-old exports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Types of DATEV APIs
&lt;/h2&gt;

&lt;p&gt;Here's where DATEV diverges sharply from typical API architectures. DATEV offers two primary data exchange formats, and neither one works like the REST or SOAP APIs you're probably familiar with. Understanding this distinction is absolutely critical before you start building your integration.&lt;/p&gt;

&lt;p&gt;The CSV-based API, used for DATEV Rechnungswesen (abbreviated as ReWe), handles finalized financial bookings and accounting records. When you need to submit completed transactions that are ready for the permanent accounting record, you use this approach. Data flows through flat, tabular CSV files that must follow precise column formatting requirements. These files are processed through batch jobs called EXTF-Jobs. This approach is common in on-premise environments and legacy integrations where the infrastructure was built before cloud-native architectures became standard.&lt;/p&gt;

&lt;p&gt;The XML-based API, used for DATEV Unternehmen Online (abbreviated as DUo), handles booking suggestions and invoice processing. This format supports complex, hierarchical data structures that represent more nuanced financial information. Data moves through structured XML files that are validated against XSD schemas to ensure compliance. These files are processed with batch jobs called dxso-Jobs. The XML approach focuses on cloud platforms and modern workflows, particularly for scenarios where you're proposing bookings that require accountant review before finalization.&lt;/p&gt;

&lt;p&gt;Now here's the critical point that trips up many developers: DATEV does &lt;strong&gt;not&lt;/strong&gt; use traditional REST or SOAP APIs for core accounting operations. If you're expecting to make HTTP POST requests with JSON payloads and get immediate synchronous responses, you're approaching this wrong. DATEV's integration relies on asynchronous batch-based file processing. You submit files as jobs, those jobs enter a processing queue, and DATEV systems handle them on their own schedule. You cannot use typical RESTful calls for submitting bookings or financial records.&lt;/p&gt;

&lt;p&gt;There are some exceptions worth noting. DATEV does offer REST endpoints for certain niche products. The Cash Register Import API (MeinFiskal) provides documented REST endpoints for importing electronic cash register data. The Document Management system integrates via REST APIs through DATEVconnect for document workflows. But these are specialized use cases, not the core accounting functionality.&lt;/p&gt;

&lt;p&gt;Third-party unified API providers sometimes expose REST endpoints that make DATEV integration feel more familiar to developers. But understand what's happening under the hood: these are wrappers that translate your REST calls into DATEV's batch processing behind the scenes. The underlying architecture remains batch-based, even if the developer experience feels more REST-like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with the accounting:documents API
&lt;/h2&gt;

&lt;p&gt;Let me show you a practical example using the accounting:documents REST API, which handles document uploads to Belege online in DATEV Unternehmen online. This is one of the REST endpoints that DATEV provides, and it demonstrates the authentication and data handling patterns you'll encounter.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/3YCIyGW6jYowcSsVJpq6qe/3362763c323a3589ba4261711e9fcdc5/Screenshot_2025-10-09_at_02.43.54_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/3YCIyGW6jYowcSsVJpq6qe/3362763c323a3589ba4261711e9fcdc5/Screenshot_2025-10-09_at_02.43.54_2x.png" alt="Screenshot 2025-10-09 at 02.43.54@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, you need to retrieve the list of clients your authenticated user can access. This establishes which companies you have permission to integrate with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://accounting-documents.api.datev.de/platform/v2/clients
Authorization: Bearer {access_token}
X-DATEV-Client-Id: {your_client_id}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you client identifiers that you'll use in subsequent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"client_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"consultant_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;455148&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"455148-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Muster GmbH 1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;id&lt;/code&gt; field is the technical identifier you'll use for all data operations. Notice that it combines the consultant number and client number, creating a unique reference for this specific client within the DATEV system.&lt;/p&gt;

&lt;p&gt;Before uploading documents, you should check what document types are available for this client. Document types determine how DATEV processes and categorizes your uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://accounting-documents.api.datev.de/platform/v2/clients/455148-1/document-types
Authorization: Bearer {access_token}
X-DATEV-Client-Id: {your_client_id}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns the configured document types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rechnungseingang"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoices_received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debit_credit_identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debit"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rechnungsausgang"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"outgoing_invoices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"debit_credit_identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kasse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"other_documents"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can upload a document. This uses multipart form data to combine the file with metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://accounting-documents.api.datev.de/platform/v2/clients/455148-1/documents
Authorization: Bearer {access_token}
X-DATEV-Client-Id: {your_client_id}
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="file"; filename="invoice-2024-001.pdf"
Content-Type: application/pdf

[Binary PDF content]
--boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
  "document_type": "Rechnungseingang",
  "note": "Supplier invoice for office supplies",
  "category": "MyERP",
  "folder": "Invoices",
  "register": "2024/10"
}
--boundary--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metadata structure organizes documents within DATEV's document repository using a three-level hierarchy: category, folder, and register. If you specify any of these levels, you must provide all three. This organizational structure helps accountants locate documents efficiently within the DATEV interface.&lt;/p&gt;

&lt;p&gt;When the upload succeeds, you receive confirmation with file details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice-2024-001.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;125742&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"upload_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-10-09T14:23:12+02:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"media_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/pdf"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"document_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rechnungseingang"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supplier invoice for office supplies"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authentication and Authorization
&lt;/h2&gt;

&lt;p&gt;DATEV uses OAuth2 for authorization, particularly for cloud-based integrations with Unternehmen Online. You need to design secure user consent workflows that clearly explain what data your application will access and how it will be used. Many integration scenarios require third-party approval or validation from the tax consultant who manages the client's account, since tax consultants control client access within the DATEV ecosystem.&lt;/p&gt;

&lt;p&gt;Your OAuth implementation should support long-lived tokens with automatic renewal processes. This is critical because DATEV integrations often need continuous data synchronization without requiring users to re-authenticate frequently. Users expect their accounting data to flow automatically once they've granted initial consent.&lt;/p&gt;

&lt;p&gt;The typical OAuth flow follows this pattern: redirect the user to DATEV's authorization endpoint, receive an authorization code after the user grants consent, exchange that code for access and refresh tokens, and use the access token for API requests. When the access token expires, use the refresh token to obtain a new access token without requiring user interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Format Compliance and Validation
&lt;/h2&gt;

&lt;p&gt;DATEV enforces strict format requirements because accounting data must maintain audit compliance and regulatory standards. The CSV and XML formats are fixed specifications that vary by product. You cannot choose arbitrary formats or invent your own column structures.&lt;/p&gt;

&lt;p&gt;For XML submissions to Unternehmen Online, your files must validate against DATEV's XSD schemas before processing will succeed. The schemas define required fields, data types, value constraints, and structural relationships. XML validation failures result in immediate job rejection with error codes that indicate which validation rules you violated.&lt;/p&gt;

&lt;p&gt;CSV files for Rechnungswesen demand precise column formatting. Each column must contain the correct data type in the expected position. Column order matters. Field delimiters must match the specification exactly. Even minor deviations cause processing failures because the DATEV import engine cannot risk misinterpreting financial data.&lt;/p&gt;

&lt;p&gt;Beyond basic format validation, you must properly structure metadata that accompanies your financial records. Cost center allocations, voucher numbers, analytic account references, and VAT information all need to follow DATEV's coding schemes. German VAT codes, for instance, follow specific conventions that DATEV recognizes and validates against current tax regulations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Error Scenarios
&lt;/h2&gt;

&lt;p&gt;The OpenAPI specification documents numerous error codes you'll encounter. Let me walk through the most common ones and what they mean for your integration.&lt;/p&gt;

&lt;p&gt;Error &lt;code&gt;#DCO01010 restapi.InvalidDocumentTypeFault&lt;/code&gt; occurs when you specify a document type that doesn't exist for the client. Remember that each client configures their own document types. You must query the available types first and only use values from that list. Don't assume standard document types exist universally.&lt;/p&gt;

&lt;p&gt;Error &lt;code&gt;#DCO01015 restapi.UnsupportedFileTypeFault&lt;/code&gt; indicates the file format isn't accepted. DATEV restricts uploads to specific file types for security and compatibility reasons. The allowed formats vary between DuoNext and legacy systems. Always check the permitted file extensions for your target environment.&lt;/p&gt;

&lt;p&gt;Error &lt;code&gt;#DCO01016 restapi.FileSizeFault&lt;/code&gt; means your file exceeds the twenty-megabyte limit. Large files create processing and storage burdens. If you're handling oversized documents, you'll need to compress them, split them into multiple uploads, or reconsider your document workflow. DATEV recommends warning users when files exceed 500 KB because even allowed file sizes can slow down system performance.&lt;/p&gt;

&lt;p&gt;Error &lt;code&gt;#DCO01253 restapi.DuplicateFileFault&lt;/code&gt; signals that a file with identical name and document type already exists in Belege Online. DATEV prevents duplicate uploads to avoid confusion. Your integration should either generate unique filenames, allow users to specify different document types for duplicates, or implement deduplication logic before attempting uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration Approaches: Direct vs. Unified APIs
&lt;/h2&gt;

&lt;p&gt;You face a fundamental architectural decision: integrate directly with DATEV or use a unified API provider that abstracts the complexity. Each approach has distinct tradeoffs that affect your development timeline, maintenance burden, and feature coverage.&lt;/p&gt;

&lt;p&gt;Direct integration with DATEV gives you complete control over the integration logic. You're not dependent on third-party service availability or pricing changes. You can access all DATEV features without waiting for a middleware provider to expose them. But this control comes with significant complexity. You need deep understanding of CSV and XML format specifications. You're responsible for all batch job management, status monitoring, error handling, and retry logic. Testing requires thorough validation across different client configurations and fiscal year setups. Development cycles stretch longer because you're implementing the entire integration stack yourself. Ongoing maintenance becomes a permanent operational burden as DATEV updates their APIs and format requirements.&lt;/p&gt;

&lt;p&gt;Unified API providers like Chift, Maesn, and others offer a RESTful developer experience that feels familiar to modern web developers. These platforms automatically handle batch job creation and management behind the scenes. They provide built-in error handling and retry logic that's been tested across many production deployments. Most importantly, they support multiple accounting platforms beyond DATEV, so your integration code can potentially connect to QuickBooks, Xero, NetSuite, and other systems using the same API patterns. Time to market drops dramatically because you're leveraging pre-built infrastructure. Maintenance overhead shifts to the unified API provider.&lt;/p&gt;

&lt;p&gt;The downsides of unified APIs include dependency on third-party service reliability and additional costs beyond DATEV's pricing. Not all DATEV features may be exposed through the unified API wrapper, potentially limiting your integration's capabilities. You're also adding network hops and processing delays compared to direct DATEV communication.&lt;/p&gt;

&lt;p&gt;For most development teams, especially those building multi-platform integrations or operating under tight deadlines, unified API providers offer the better path. The productivity gains and reduced maintenance burden usually outweigh the additional costs and potential feature limitations. Direct DATEV integration makes sense primarily for specialized use cases that require features not available through unified APIs, or for large-scale deployments where the volume justifies investing in custom infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Implementation Considerations
&lt;/h2&gt;

&lt;p&gt;When you start implementing your DATEV integration, several practical concerns will surface immediately. File naming conventions matter more than you might expect. DATEV recommends UTF-8 encoding with precomposed Unicode characters. Decomposed characters cause problems. Special characters like umlauts must be single code points, not multi-byte sequences. DATEV maintains a whitelist of permitted characters in filenames, and unsupported characters get replaced with underscores automatically. This can create confusion if your system generates filenames with characters outside the whitelist.&lt;/p&gt;

&lt;p&gt;Rate limiting and batch processing constraints affect how you design your data synchronization strategy. You cannot stream continuous updates in real-time. Instead, you batch operations and submit them periodically. Determine appropriate batch sizes that balance efficiency against error handling complexity. Larger batches process more efficiently but make error diagnosis harder when something fails.&lt;/p&gt;

&lt;p&gt;Error recovery needs thoughtful design because batch processing creates asynchronous failure modes. When a job fails, you need to detect the failure, parse error messages, determine which records caused problems, and decide whether to retry the entire batch or just the failed records. Your integration should implement exponential backoff for transient failures while permanently failing for validation errors that won't resolve through retries.&lt;/p&gt;

&lt;p&gt;Testing strategies require access to DATEV sandbox environments. The API specification shows separate endpoints for sandbox testing. Use these extensively before deploying to production. Test with various client configurations, different fiscal year setups, and edge cases in your data. Remember that DATEV may automatically generate subsequent fiscal years under certain conditions, so test your integration's behavior when fiscal year transitions occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendations
&lt;/h2&gt;

&lt;p&gt;DATEV integration is fundamentally different from typical REST API integration work. The batch-processing architecture, strict format requirements, and certification processes create complexity that shouldn't be underestimated. Before starting development, evaluate your technical resources honestly. Do you have developers with experience in batch processing systems? Can you invest time in understanding DATEV's specific format requirements? Do you need features that unified API providers don't expose?&lt;/p&gt;

&lt;p&gt;For most teams, starting with a unified API provider offers the fastest path to a working integration. You can always migrate to direct DATEV integration later if specific requirements demand it. Focus initially on understanding the business workflows, data mapping requirements, and error handling strategies. These concerns remain constant regardless of whether you integrate directly or through middleware.&lt;/p&gt;

&lt;p&gt;Document your integration thoroughly, especially the format specifications and validation requirements. Future developers maintaining your integration will need this context. Build comprehensive error logging from day one because diagnosing batch processing failures requires detailed audit trails.&lt;/p&gt;

&lt;p&gt;DATEV integration enables powerful accounting automation, but success requires respecting the architectural constraints and compliance requirements that make DATEV the trusted standard for German accounting workflows.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Bank Feeds API Integration: Why You Can’t Afford to Skip This Feature</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Fri, 31 Oct 2025 14:27:10 +0000</pubDate>
      <link>https://dev.to/apideck/bank-feeds-api-integration-why-you-cant-afford-to-skip-this-feature-f22</link>
      <guid>https://dev.to/apideck/bank-feeds-api-integration-why-you-cant-afford-to-skip-this-feature-f22</guid>
      <description>&lt;p&gt;Your corporate card product is brilliant. Your business banking app solves real problems. Your expense management platform beats the competition on features and pricing.&lt;/p&gt;

&lt;p&gt;But then a prospect asks: "Does this sync with our Xero account?"&lt;/p&gt;

&lt;p&gt;And suddenly, you're explaining why users need to export CSVs and manually upload bank statements to their accounting system. The demo goes cold. The deal stalls. Your competitor with the bank feed integration wins the business.&lt;/p&gt;

&lt;p&gt;This happens every day to fintech companies that treat bank feeds as a "nice to have" feature. The reality? For finance teams, bank feeds are table stakes. Without them, you're asking users to do work that could be automated via an API integration.&lt;/p&gt;

&lt;p&gt;Here's how to fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bank Feeds Actually Do (And Why Your Customers Demand Them)
&lt;/h2&gt;

&lt;p&gt;Bank feeds automatically send transaction data from your platform into users' accounting systems, removing the need for CSV uploads and manual entry. The data flows directly from your product into Xero, QuickBooks, or NetSuite as transactions happen.&lt;/p&gt;

&lt;p&gt;The primary reason finance teams care about bank feeds is reconciliation. When transactions appear automatically in accounting software, users can match them against invoices, bills, and other entries to close their books faster and with more confidence.&lt;/p&gt;

&lt;p&gt;Manual reconciliation involves downloading a CSV or OFX file, reforming the columns, uploading it to their accounting system, and then manually matching each transaction. This takes hours. It creates errors. Finance teams hate it.&lt;/p&gt;

&lt;p&gt;Bank feeds eliminate all of that. The transaction happens. Data appears in the accounting system. User clicks to reconcile. Done.&lt;/p&gt;

&lt;p&gt;Bank feeds are especially valuable for financial platforms that handle money movement: corporate card providers, business banking apps, expense tools, payout platforms, and accounting automation products. If your product processes transactions on behalf of business customers, those transactions need to flow into their ledger.&lt;/p&gt;

&lt;p&gt;While fintechs offer great integrations for accounting software, they traditionally lack access to the resources, APIs, and partnerships required to provide the "bank-like" experience small businesses expect. That's changing, but the expectation exists. Your users want the same experience they get from their bank: automatic feeds, no manual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Use Cases That Actually Drive Revenue
&lt;/h2&gt;

&lt;p&gt;Let's get specific about where bank feeds matter most.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/4bUNQXXiGi116xhdVSJOGn/8a964ca4e3079a8bbf665e2269028b63/Screenshot_2025-10-01_at_03.09.37_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/4bUNQXXiGi116xhdVSJOGn/8a964ca4e3079a8bbf665e2269028b63/Screenshot_2025-10-01_at_03.09.37_2x.png" alt="Screenshot 2025-10-01 at 03.09.37@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Corporate Cards and Expense Management&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Your users swipe corporate cards. Those transactions need to appear in their accounting system the same day, not next week, after someone exports a spreadsheet.&lt;/p&gt;

&lt;p&gt;For complete reconciliation, you need two integrations: accounting sync for expenses AND bank feeds for the actual bank transactions. Without both, users still do manual work.&lt;/p&gt;

&lt;p&gt;Example: Employee buys office supplies with the company card. Transaction hits your platform. Bank feed sends it to QuickBooks. The finance team opens QuickBooks, sees the transaction, and matches it to the expense report. Approved in 30 seconds instead of 30 minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Business Banking and Neobanks&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Fintechs can deliver a bank-like experience by automating the upload of transaction statements to the general ledger for reconciliation. This is what banks do. If you're competing with banks, you need to do it too.&lt;/p&gt;

&lt;p&gt;Arc, a fintech company serving startups with treasury management and revenue-based financing, put it clearly: "We offer customers deposit accounts that serve as the core of how they run their business. It's critical for our customers to sync that data to their accounting systems."&lt;/p&gt;

&lt;p&gt;Without bank feeds, your business banking app feels incomplete. With them, you're offering the full package.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Bill Pay and AP Automation&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Push bill pay transactions to accounting systems for real-time payables reconciliation. When your platform pays a vendor invoice, that payment should appear in the customer's accounting system immediately.&lt;/p&gt;

&lt;p&gt;This closes the loop on accounts payable. Your app initiates payment. Bank feed confirms it happened. The accounting system marks the bill as paid. No manual matching required.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Treasury Management&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Treasury teams need real-time cash position visibility across multiple bank accounts. Bank feeds provide the data they need to forecast cash flow, manage liquidity, and make funding decisions.&lt;/p&gt;

&lt;p&gt;For companies managing cash across subsidiaries or multiple currencies, automated bank feeds make daily treasury operations possible without building custom integrations for every bank account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Direct Integration Is a Nightmare
&lt;/h2&gt;

&lt;p&gt;You might think: "We'll just build a direct integration with Xero. How hard can it be?"&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/lbpnnIB4RCr2kEnfvqqxX/5a29ec3dd7a0abdf18969c7724a7ea8c/ChatGPT_Image_Oct_1__2025__02_01_07_AM.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/lbpnnIB4RCr2kEnfvqqxX/5a29ec3dd7a0abdf18969c7724a7ea8c/ChatGPT_Image_Oct_1__2025__02_01_07_AM.png" alt="ChatGPT Image Oct 1, 2025, 02 01 07 AM"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Very hard. Here's why.&lt;/p&gt;

&lt;p&gt;Xero's Bank Feeds API is a closed API only available to financial institutions with an established partnership with Xero. You can't just sign up and start using it. You need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Apply to become a Xero financial services partner&lt;/li&gt;
&lt;li&gt;Wait for approval (no guarantees)&lt;/li&gt;
&lt;li&gt;Go through certification process (takes months)&lt;/li&gt;
&lt;li&gt;Get your application certified for bank feeds access&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's just for Xero. Now repeat this process for QuickBooks, NetSuite, Sage Intacct, MYOB, and every other accounting platform your customers use.&lt;/p&gt;

&lt;p&gt;Each platform has different APIs. Different authentication flows. Different data models. Different certification requirements. Different ongoing maintenance needs.&lt;/p&gt;

&lt;p&gt;The math gets ugly fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3-6 months per integration&lt;/li&gt;
&lt;li&gt;5 major platforms&lt;/li&gt;
&lt;li&gt;= 15-30 months of development time&lt;/li&gt;
&lt;li&gt;Plus ongoing maintenance when APIs change&lt;/li&gt;
&lt;li&gt;Plus customer support for auth issues&lt;/li&gt;
&lt;li&gt;Plus compliance and security reviews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And during those 15-30 months, you're not building your core product. You're managing OAuth flows and debugging webhook failures.&lt;/p&gt;

&lt;p&gt;Most fintech companies don't have that time. The market moves too fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Apideck Solves the Bank Feed Problem
&lt;/h2&gt;

&lt;p&gt;A unified API aggregates multiple APIs in the same category into a single standardized endpoint with unified authentication and normalized data models. Instead of building 20 separate integrations, you build one.&lt;/p&gt;

&lt;p&gt;For bank feeds, this means:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;One Integration, Multiple Platforms&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Apideck's Accounting API provides a single REST interface to sync data across 20+ accounting platforms. You write your bank feed integration once. It works with Xero, QuickBooks, NetSuite, Sage Intacct, and dozens of other systems.&lt;/p&gt;

&lt;p&gt;Your code stays the same. Apideck handles the platform-specific details.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Authentication That Actually Works&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Apideck Vault handles OAuth flows and stores credentials securely with enterprise-grade security. Your users connect their accounting system through a white-label interface. They never see your API keys. You never manage their tokens.&lt;/p&gt;

&lt;p&gt;The same authentication flow works for every platform. No custom OAuth implementations per provider.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Maintenance You Don't Have to Do&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Apideck handles connector updates, API versioning, and provider-specific changes without requiring code changes in your application. When QuickBooks ships a breaking change or Xero updates authentication requirements, Apideck updates the connector. Your code keeps working.&lt;/p&gt;

&lt;p&gt;This is huge. Direct integrations break constantly. APIs change. Partners add new requirements. Version deprecations happen with 60 days' notice. With Apideck, that's not your problem anymore.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;&lt;em&gt;Normalized Data Models&lt;/em&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Apideck normalizes messy, inconsistent APIs into a single structure while still exposing raw downstream information. Every platform uses different field names and data formats. Apideck translates between your standard format and whatever the downstream API expects.&lt;/p&gt;

&lt;p&gt;You send the same JSON payload for bank transactions. Apideck converts it to QBXML for QuickBooks Desktop, REST for Xero, or whatever format NetSuite needs. You don't care about implementation details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Implementation: Xero Bank Feeds with Apideck
&lt;/h2&gt;

&lt;p&gt;Let's get into the actual code. Here's how you implement bank feeds for Xero using Apideck's unified API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You still need Xero Bank Feeds API certification. Apideck can't bypass that requirement because it's a partnership between you and Xero. But once you're certified, Apideck handles everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a Bank Feed Account&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Bank Feed Accounts endpoint in Apideck's Accounting API corresponds to Xero's Feed Connections API and links your platform's accounts to bank or credit card accounts in Xero.&lt;/p&gt;

&lt;p&gt;POST to &lt;a href="https://unify.apideck.com/accounting/bank-feed-accounts" rel="noopener noreferrer"&gt;https://unify.apideck.com/accounting/bank-feed-accounts&lt;/a&gt; with this payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_account_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corp-card-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_account_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345676"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_account_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corporate Card Account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank_account_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the bank account already exists in Xero, use target_account_id instead of name and number to link it. If not, provide target_account_name and target_account_number so Xero creates a new account.&lt;/p&gt;

&lt;p&gt;The response gives you a bank_feed_account_id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"55af487b-df2b-425e-9432-ea2db7a77481"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store this ID. You'll need it for every transaction batch you send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Send Bank Transactions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Bank Feed Statements endpoint lets you send a batch of transactions tied to the bank_feed_account_id created earlier.&lt;/p&gt;

&lt;p&gt;POST to &lt;a href="https://unify.apideck.com/accounting/bank-feed-statements:" rel="noopener noreferrer"&gt;https://unify.apideck.com/accounting/bank-feed-statements:&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank_feed_account_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"55af487b-df2b-425e-9432-ea2db7a77481"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-01T00:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-15T23:59:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_balance_credit_or_debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;142.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end_balance_credit_or_debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posted_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-02T01:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Office Supplies - Staples"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;47.23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"credit_or_debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_transaction_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"txn_1019"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"counterparty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Staples Inc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-20250502"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transaction_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posted_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-03T09:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Client Payment - ACME Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;89.23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"credit_or_debit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_transaction_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"txn_1020"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"counterparty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACME Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice-445"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transaction_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best practices: Send statements in chronological order, ensure balances align correctly (Xero validates them), and avoid gaps or overlaps in statement periods to prevent rejections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Users Reconcile in Xero&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the statement is successfully uploaded, transactions appear in Xero under the connected bank account where users reconcile them using Xero's standard process by matching each line to invoices, bills, or other records.&lt;/p&gt;

&lt;p&gt;This creates the workflow your finance users expect: your platform sends transactions, Xero handles reconciliation. By automating this flow, you help users maintain accurate books with minimal manual effort.&lt;/p&gt;

&lt;p&gt;You can send feeds daily, weekly, or on-demand. Most products send daily updates to keep accounting systems current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Xero: Multi-Platform Support
&lt;/h2&gt;

&lt;p&gt;Xero is live and fully supported. But your customers use other accounting systems too, we're actively adding support for more platforms.&lt;/p&gt;

&lt;p&gt;SaaS companies typically need multiple accounting integrations to serve diverse customer bases: UK-focused customers use Xero, US SMBs prefer QuickBooks, and enterprise clients require Sage Intacct.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/4yILhoGDxksQwPNvnMMYqL/3349a6d770059fd088f39d8d4dfb1d1f/Screenshot_2025-10-01_at_01.46.49_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/4yILhoGDxksQwPNvnMMYqL/3349a6d770059fd088f39d8d4dfb1d1f/Screenshot_2025-10-01_at_01.46.49_2x.png" alt="Screenshot 2025-10-01 at 01.46.49@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apideck supports 20+ accounting platforms, including QuickBooks, NetSuite, Sage Intacct, MYOB, Microsoft Dynamics 365 Business Central, FreshBooks, and Exact Online. The same unified API works across all of them.&lt;/p&gt;

&lt;p&gt;Different platforms use different formats for bank feed data. While Xero uses its proprietary Bank Feeds API, NetSuite commonly relies on the OFX (Open Financial Exchange) format for importing bank statements. OFX is a standardized format that many banks and financial institutions use to exchange transaction data. If you're building for NetSuite users, understanding OFX formatting becomes important for smooth data imports. Learn more about importing bank statements into NetSuite using OFX format at &lt;a href="https://www.docuclipper.com/blog/how-to-import-bank-statement-into-netsuite/" rel="noopener noreferrer"&gt;DocuClipper's guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Right now, bank feeds are fully implemented for Xero. Support for additional platforms is expanding. The architecture is already there. The API endpoints are the same. As Apideck adds bank feed support to more connectors, your integration automatically gains access to those platforms.&lt;/p&gt;

&lt;p&gt;This matters because you can't predict which accounting system your next big customer will use. Building flexibility into your integration strategy now saves months of work later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line for Decision Makers
&lt;/h2&gt;

&lt;p&gt;Bank feeds separate products that finance teams actually adopt from products that get evaluated and rejected.&lt;/p&gt;

&lt;p&gt;If you're building a fintech product that handles money movement, you need bank feeds. Not eventually. Now.&lt;/p&gt;

&lt;p&gt;The choice is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build direct integrations to 5-10 accounting platforms over 18-24 months&lt;/li&gt;
&lt;li&gt;Use a unified API and ship bank feeds in a few weeks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Development teams can implement accounting integrations in weeks rather than months using unified APIs with standardized webhooks, consistent error handling, and unified documentation that eliminates the complexity of learning multiple provider-specific APIs.&lt;/p&gt;

&lt;p&gt;Apideck gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One integration for 20+ accounting platforms&lt;/li&gt;
&lt;li&gt;Managed authentication and credential storage&lt;/li&gt;
&lt;li&gt;Automatic handling of API changes and updates&lt;/li&gt;
&lt;li&gt;Production-ready bank feeds for Xero (with more platforms coming)&lt;/li&gt;
&lt;li&gt;Real-time data processing with no batching delays&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Ready to add bank feeds to your product?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Xero Bank Feeds:&lt;/strong&gt; Start with the detailed technical guide at &lt;a href="https://developers.apideck.com/guides/bank-feeds-xero" rel="noopener noreferrer"&gt;developers.apideck.com/guides/bank-feeds-xero&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the Full Accounting API:&lt;/strong&gt; Explore all endpoints and connectors at &lt;a href="https://developers.apideck.com/apis/accounting/reference" rel="noopener noreferrer"&gt;developers.apideck.com/apis/accounting&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Try It Out:&lt;/strong&gt; Sign up for a free Apideck account and get 2,500 API calls to test your integration. No credit card required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Talk Strategy:&lt;/strong&gt; Book a demo if you're evaluating unified APIs for your product roadmap. The Apideck team can walk you through your specific use case and integration requirements.&lt;/p&gt;

&lt;p&gt;The fintech companies winning deals right now are the ones that solved bank feeds months ago. Stop building one-off integrations. Ship bank feeds this quarter instead of next year.&lt;/p&gt;

&lt;p&gt;Your finance team users will thank you.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Connect with the HubSpot API</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Thu, 30 Oct 2025 16:43:28 +0000</pubDate>
      <link>https://dev.to/apideck/how-to-connect-with-the-hubspot-api-8hk</link>
      <guid>https://dev.to/apideck/how-to-connect-with-the-hubspot-api-8hk</guid>
      <description>&lt;p&gt;HubSpot's API looks modern on the surface. REST endpoints, JSON payloads, OAuth 2.0, webhooks. Then you actually build something with it and discover the truth: it's a maze of rate limits, undocumented quirks, and lifecycle stage logic that defies human comprehension.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/2TPol60vMKzXgdQZyL6lb9/b349a2c22026974a0a7ba370e207128a/Screenshot_2025-09-12_at_23.42.05_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/2TPol60vMKzXgdQZyL6lb9/b349a2c22026974a0a7ba370e207128a/Screenshot_2025-09-12_at_23.42.05_2x.png" alt="Screenshot 2025-09-12 at 23.42.05@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've spent the last three years integrating HubSpot for various clients. Here's what the documentation won't tell you and what will save you from the same pain I went through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OAuth Dance Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;HubSpot uses OAuth 2.0, which sounds standard until you realize their implementation has its own special flavor. You need three things before you even start: a developer account (separate from your regular HubSpot account), an app registration, and the patience of a saint.&lt;/p&gt;

&lt;p&gt;First, create your app at &lt;a href="https://developers.hubspot.com" rel="noopener noreferrer"&gt;developers.hubspot.com&lt;/a&gt;. You'll get a Client ID and Client Secret. Guard that secret like your life depends on it, because regenerating it will break every existing integration.&lt;/p&gt;

&lt;p&gt;Here's the authorization URL you need to build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://app.hubspot.com/oauth/authorize?`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`scope=crm.objects.contacts.read%20crm.objects.contacts.write`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But wait, there's a catch nobody mentions: the redirect URI must match EXACTLY what you registered. Not just the domain, not just the path, but every single character, including trailing slashes. Get it wrong and you'll see a generic error that tells you nothing.&lt;/p&gt;

&lt;p&gt;When the user approves, HubSpot redirects back with a code. You have exactly 30 seconds to exchange it for tokens, or it expires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.hubapi.com/oauth/v1/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authCode&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh_token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most access tokens are short-lived. You can check the expires_in parameter when generating an access token to determine its lifetime (in seconds). Practically you have a few minutes before they expire.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/6HdfLyDkKs63LyiUHccWm5/13c29ebb8e281f37d4196a0488da410c/Screenshot_2025-09-12_at_23.42.53_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/6HdfLyDkKs63LyiUHccWm5/13c29ebb8e281f37d4196a0488da410c/Screenshot_2025-09-12_at_23.42.53_2x.png" alt="Screenshot 2025-09-12 at 23.42.53@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the refresh logic you'll need running constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.hubapi.com/oauth/v1/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refresh_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Refresh token is dead, user needs to reauthorize&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token refresh failed - user must reauthorize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read more about the refresh access token logic from the &lt;a href="https://developers.hubspot.com/docs/api-reference/auth-oauth-v1/tokens/post-oauth-v1-token" rel="noopener noreferrer"&gt;HubSpot documentation here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting: The 429 Error Festival
&lt;/h2&gt;

&lt;p&gt;HubSpot's rate limits are documented, but what they don't tell you is how aggressive they are. You get 100 requests per 10 seconds for public apps. That sounds like a lot until you realize that's shared across ALL your users if you're using a single app registration.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/2QqH4pzYEwzbgSBdvVBuk9/823027367fbf6ed0a827c04609753825/Screenshot_2025-09-13_at_22.07.28_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/2QqH4pzYEwzbgSBdvVBuk9/823027367fbf6ed0a827c04609753825/Screenshot_2025-09-13_at_22.07.28_2x.png" alt="Screenshot 2025-09-13 at 22.07.28@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, you can upgrade the number of calls your app can make, which is based on your account subscription in the account it's installed in. Here's the plan details from the &lt;a href="https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines#app-limits" rel="noopener noreferrer"&gt;HubSpot Documentation&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;[IMAGE: Rate limit table by subscription tier]&lt;/p&gt;

&lt;p&gt;Here's what a 429 error looks like when you hit the limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You have reached your secondly limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RATE_LIMIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c033cdaa-2c40-4a64-ae48-b4cec88dad24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEN_SECONDLY_ROLLING"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worst part? HubSpot counts requests that fail against your rate limit. So when you hit the limit and retry immediately, you're making it worse. You need exponential backoff or you'll be stuck in 429 hell forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeHubSpotRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Max retries exceeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Exponential backoff: 1s, 2s, 4s, 8s, 16s&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rate limited. Waiting &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms before retry...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;makeHubSpotRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HubSpot API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the real kicker: if you're using Make.com, Zapier, or any integration platform, you're sharing rate limits with every other customer using their HubSpot connector. I've seen perfectly reasonable workflows fail at 2 AM because someone else's integration went haywire. The only solution is to create your own OAuth app and use their "advanced" connection option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lifecycle Stage Nightmare
&lt;/h2&gt;

&lt;p&gt;HubSpot's lifecycle stages are supposed to track where contacts are in your sales process. In reality, they're a one-way street designed by someone who's never had to fix bad data.&lt;/p&gt;

&lt;p&gt;Lifecycle stages can only move forward by default. Lead to Customer? Fine. Customer back to Lead because they canceled? Nope. You have to clear the field first, then set the new value in a separate API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This won't work - lifecycle stage can't go backwards&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lifecyclestage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Fails silently&lt;/span&gt;

&lt;span class="c1"&gt;// This is what you actually need&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lifecyclestage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Clear it first&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lifecyclestage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Now set it&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;makeHubSpotRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.hubapi.com/crm/v3/objects/contacts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's two API calls for one field update, doubling your rate limit usage. And it gets worse: the batch API doesn't guarantee order, so you can't clear and set in the same batch request. Every backwards lifecycle stage movement costs you two separate API calls.&lt;/p&gt;

&lt;p&gt;Oh, and those "Became a [stage] date" properties? They're being deprecated. HubSpot announced this in 2024, but half of its documentation still references them. You now need to use their "calculated properties," which have their own special quirks and can't be set via API at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Properties: The False Promise
&lt;/h2&gt;

&lt;p&gt;HubSpot lets you create custom properties for anything. Sounds great until you realize that enumeration properties (dropdowns, checkboxes) have their own internal IDs that aren't the same as the labels you see in the UI.&lt;/p&gt;

&lt;p&gt;You think you're setting a property to "Enterprise Customer," but HubSpot wants "enterprise_customer_7821" or something equally ridiculous. To find the internal values, you need to query the properties endpoint first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;propertyResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;makeHubSpotRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.hubapi.com/crm/v3/properties/contacts/industry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Returns something like:&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   "options": [&lt;/span&gt;
&lt;span class="c1"&gt;//     { "label": "Enterprise Customer", "value": "enterprise_customer_7821" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { "label": "SMB Customer", "value": "smb_customer_9183" }&lt;/span&gt;
&lt;span class="c1"&gt;//   ]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And don't even think about changing these values later. Every integration, workflow, and report using that property will break. I learned this when a client wanted to rename "Hot Lead" to "Qualified Lead" and it took three days to fix all the broken automations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks: Death by a Thousand Subscriptions
&lt;/h2&gt;

&lt;p&gt;HubSpot webhooks seem straightforward: subscribe to events, receive notifications. What they don't tell you is that webhooks are tied to your app, not individual accounts. Every customer using your app shares the same webhook URL.&lt;/p&gt;

&lt;p&gt;Setting up webhooks requires a verified domain and HTTPS endpoint that can handle HubSpot's validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// HubSpot sends validation on setup&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-hubspot-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-hubspot-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sourceString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Process webhook events&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Event: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for object &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// But which customer is this for? Good luck figuring that out&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook payload doesn't include which account it's from. You get an object ID and have to make another API call (counting against your rate limit) to figure out whose data changed. With 100 customers, that's 100 extra API calls per webhook event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Undocumented Reality
&lt;/h2&gt;

&lt;p&gt;Here's what HubSpot won't tell you but you need to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API versions are complex.&lt;/strong&gt; There's v1, v2, and v3 running simultaneously. Some endpoints only exist in v1 (looking at you, Engagements API), some features are v3 only, and they're deprecating v1 "soon" (they've been saying this for three years).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private apps are not the same as OAuth apps.&lt;/strong&gt; Private apps use API keys and are simpler but can't be distributed. OAuth apps can be shared but require the whole token dance. Choose wrong and you'll be rebuilding your integration from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search API is basically useless.&lt;/strong&gt; It has a different rate limit (4 requests per second), can't search all properties, and sometimes returns stale data. One client had contacts appearing in search results three hours after deletion. The only reliable way to find data is to pull everything and filter locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error messages lie.&lt;/strong&gt; You'll get "Contact already exists" when the real problem is a malformed email. You'll get "Invalid property value" when the property doesn't exist. Always log the full request and response because the error message alone won't help you debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Associations are their own special hell.&lt;/strong&gt; Want to link a contact to a company? That's a separate API call. Want to see all contacts for a company? Another call. Want to update the association? You can't, you have to delete and recreate it. Each operation counts against your rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making This Bearable with TypeScript
&lt;/h2&gt;

&lt;p&gt;If you're building anything serious, use TypeScript. HubSpot's API responses are inconsistent and TypeScript will save you from runtime explosions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HubSpotContact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;lifecyclestage&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HubSpotError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;errorType&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RATE_LIMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VALIDATION_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HubSpotClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HubSpotContact&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HubSpotContact&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.hubapi.com/crm/v3/objects/contacts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HubSpotError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HubSpot API Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Apideck Escape Hatch
&lt;/h2&gt;

&lt;p&gt;Look, I've built enough HubSpot integrations to know when to quit. If you need HubSpot plus other CRMs (Salesforce, Pipedrive, etc.), stop building separate integrations and use a unified API.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/6zWKVtZxa2nCQrlDdkmscb/3d023f77a4d845ba5c1c345feaf6cf99/Screenshot_2025-09-12_at_23.40.16_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/6zWKVtZxa2nCQrlDdkmscb/3d023f77a4d845ba5c1c345feaf6cf99/Screenshot_2025-09-12_at_23.40.16_2x.png" alt="Screenshot 2025-09-12 at 23.40.16@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apideck's unified CRM API&lt;/strong&gt; handles HubSpot's quirks so you don't have to. One integration instead of five, OAuth complexity handled, rate limiting managed across their infrastructure, and lifecycle stage nonsense abstracted away. Your 6-week HubSpot integration can be done easily within two weeks of actual coding. And not only this, you can configure other CRMs as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apideck's CRM Unified API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Direct HubSpot API: OAuth dance, token refresh, rate limiting, lifecycle stage hell&lt;/span&gt;
&lt;span class="c1"&gt;// Apideck: Just this&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Apideck&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@apideck/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apideck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Apideck&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIDECK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIDECK_APP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;consumerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer-123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Your customer's ID&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a contact in HubSpot (or any CRM they've connected)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createContact&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apideck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contactsAdd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hubspot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Or 'salesforce', 'pipedrive', etc.&lt;/span&gt;
      &lt;span class="na"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;john@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;phoneNumbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+1-555-1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;work&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="c1"&gt;// Lifecycle stage just works - no backwards movement BS&lt;/span&gt;
        &lt;span class="na"&gt;lifecycleStage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;customFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;industry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Technology&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Contact created:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Actual useful error messages&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Get all contacts with pagination handled automatically&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAllContacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apideck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contactsAll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hubspot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;john@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// No manual pagination, no rate limit management&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The same code works for ANY CRM - just change serviceId&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncToMultipleCRMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hubspot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salesforce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipedrive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crm&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;crms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apideck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contactsAdd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contactData&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// That's it. No OAuth per platform, no different APIs, no rate limit juggling&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to the 200 lines of OAuth handling, rate limit retry logic, and lifecycle stage workarounds you need for direct HubSpot integration. One API, consistent data models, errors that actually make sense.&lt;/p&gt;

&lt;p&gt;Tangible benefits that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.apideck.com/blog/what-is-a-unified-api" rel="noopener noreferrer"&gt;Unified API for all CRM platforms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;One integration for HubSpot, Salesforce, Pipedrive, and 50+ others&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.apideck.com/guides/field-mapping" rel="noopener noreferrer"&gt;Field mapping that actually works&lt;/a&gt; - Including HubSpot's lifecycle stages and custom properties&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.apideck.com/apis/crm/hubspot" rel="noopener noreferrer"&gt;No more OAuth gymnastics&lt;/a&gt; - They handle token refresh, expiration, all of it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ship Something That Works
&lt;/h2&gt;

&lt;p&gt;HubSpot's API is powerful but exhausting. You'll spend more time handling edge cases than building features. Every integration starts simple and ends with dozens of workarounds for HubSpot's peculiarities.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/d6o5ai4eeewt/2b0biffxoTWxo6QImU0bQ6/f3585a870c84f4a775b267eff76e1ed4/Screenshot_2025-09-12_at_23.41.19_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/d6o5ai4eeewt/2b0biffxoTWxo6QImU0bQ6/f3585a870c84f4a775b267eff76e1ed4/Screenshot_2025-09-12_at_23.41.19_2x.png" alt="Screenshot 2025-09-12 at 23.41.19@2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My advice? Start with the smallest possible integration. Get OAuth working. Make one API call successfully. Handle rate limits properly. Only then add more complexity. And when you inevitably hit the wall where you're spending more time fighting HubSpot than building your product, consider whether a unified API makes more sense.&lt;/p&gt;

&lt;p&gt;The perfect HubSpot integration doesn't exist. Ship something that works, iterate based on which errors your customers actually hit, and keep a bottle of whiskey handy for when HubSpot changes something without warning.&lt;/p&gt;

&lt;p&gt;Because they will, they always do.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>MCP vs API</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Thu, 16 Oct 2025 11:22:21 +0000</pubDate>
      <link>https://dev.to/apideck/mcp-vs-api-40a8</link>
      <guid>https://dev.to/apideck/mcp-vs-api-40a8</guid>
      <description>&lt;h2&gt;
  
  
  MCP vs API: What’s the Actual Difference and When to Use Each
&lt;/h2&gt;

&lt;p&gt;If you’re building with AI in 2025, you’ve probably heard about the &lt;a href="https://www.apideck.com/blog/a-primer-on-the-model-context-protocol#why-usb-c-though" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;. But here’s what nobody’s telling you straight: &lt;strong&gt;MCP doesn’t replace APIs - it makes them work better with AI&lt;/strong&gt;. Let’s cut through the noise and understand what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem MCP Solves
&lt;/h2&gt;

&lt;p&gt;Remember the USB cable mess before USB-C? That’s where we were with AI integrations. Every AI model requires custom connectors for each data source, an expensive nightmare that Anthropic calls the “M×N problem.”&lt;/p&gt;

&lt;p&gt;Traditional APIs are ideal for developers who read documentation and write integration code. But AI agents? They’re different. They need to understand the existing tools, how to use them, and maintain context across multiple operations. That’s where MCP comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP vs Traditional APIs: The Core Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;APIs are for developers. MCP is for AI agents.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you use a &lt;a href="https://www.apideck.com/blog/rest-vs-soap-apis#the-real-difference-that-matters" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;, you manually check the documentation, understand endpoints, and write code to call /users/123 or POST /orders. It’s explicit, predictable, and stateless - each request stands alone.&lt;/p&gt;

&lt;p&gt;MCP flips this. Instead of you reading docs, the AI asks the MCP server, “What can I do here?” and gets back a list of capabilities it can understand. Instead of stateless requests, MCP maintains sessions that remember context across multiple operations.&lt;/p&gt;

&lt;p&gt;Think of it this way: APIs are like individual Lego blocks. MCP is the instruction manual that helps AI figure out which blocks to use and how to combine them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Reality (Without the Jargon)
&lt;/h2&gt;

&lt;p&gt;Traditional APIs use HTTP methods (GET, POST, etc.) mapped to resources. You call specific endpoints, get responses, done. They’re stateless by design - perfect for scaling but requiring you to manage any context yourself.&lt;/p&gt;

&lt;p&gt;MCP uses JSON-RPC 2.0 - a different approach where you call methods like “tools/call” with parameters. It maintains stateful sessions, so when an AI performs multiple actions, it understands they’re part of the same task. This session management is built in, not bolted on.&lt;/p&gt;

&lt;p&gt;Here’s what makes this practical: When you tell an AI “book my flight and add it to my calendar,” MCP helps it discover both the flight API and calendar API, understand how to use them, and maintain context that these actions are related. The underlying services? Still REST APIs. MCP just adds the AI-friendly layer on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table: MCP vs Traditional APIs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Traditional APIs&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Users&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developers writing code&lt;/td&gt;
&lt;td&gt;AI agents and LLMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Discovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read the documentation manually&lt;/td&gt;
&lt;td&gt;Automatic capability discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateless (each request independent)&lt;/td&gt;
&lt;td&gt;Stateful sessions with context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST, GraphQL, gRPC&lt;/td&gt;
&lt;td&gt;JSON-RPC 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration Approach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Explicit endpoint mapping&lt;/td&gt;
&lt;td&gt;Dynamic tool discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Battle-tested, decades old&lt;/td&gt;
&lt;td&gt;Emerging (launched Nov 2024)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Horizontal scaling built-in&lt;/td&gt;
&lt;td&gt;Session management challenges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct integrations, mobile apps, microservices&lt;/td&gt;
&lt;td&gt;AI orchestration, multi-step automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Established patterns (OAuth2, JWT)&lt;/td&gt;
&lt;td&gt;Still evolving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer manages manually&lt;/td&gt;
&lt;td&gt;Built-in conversation context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Unified API Connection
&lt;/h2&gt;

&lt;p&gt;Here’s where it gets interesting for integration pros. Companies like Apideck, Unified.to, Codat, and Rutter built &lt;a href="https://www.apideck.com/blog/what-is-a-unified-api#the-problem-with-api-integration" rel="noopener noreferrer"&gt;unified APIs&lt;/a&gt; to solve the integration sprawl problem - one API to connect to hundreds of services. Now we’re seeing &lt;strong&gt;Unified MCPs&lt;/strong&gt; emerge.&lt;/p&gt;

&lt;p&gt;A Unified MCP works like a unified API but for AI agents. Instead of your AI needing separate MCP connections to Salesforce, HubSpot, and Pipedrive, it connects to one Unified MCP that handles all CRM systems. Same concept, different consumer.&lt;/p&gt;

&lt;p&gt;For companies like Apideck that already aggregate APIs, adding an MCP layer is the logical next step. You’ve already solved the hard part - normalizing data across providers. Now you’re just making it AI-accessible. This creates a powerful stack: unified APIs for developers, unified MCPs for AI agents, all backed by the same normalized data layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Actually Use Each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use traditional APIs when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Building mobile or web applications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating microservice architectures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Processing payments or high-frequency trades&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You need predictable, deterministic behavior&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance and scaling are critical&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use MCP when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Building AI assistants or agents&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Orchestrating multi-step workflows via natural language&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating development copilots&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exposing existing APIs to LLM-powered applications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Users express intent conversationally, not programmatically&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use both when:&lt;/strong&gt; You’re smart. MCP servers typically wrap REST APIs. Your API handles the actual operations; MCP makes them AI-friendly. It’s not either/or - it’s using the right tool for the right job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;MCP isn’t replacing APIs any more than Uber replaced cars - it’s just a smarter way to use them for AI applications. APIs remain the backbone of digital infrastructure. MCP adds the intelligence layer that lets AI agents work with those APIs effectively.&lt;/p&gt;

&lt;p&gt;For developers, this means your API skills aren’t obsolete - they’re more valuable. You’ll build REST APIs for core services, then potentially add MCP servers to make them AI-accessible. For businesses, it means AI agents can finally integrate with your existing tools without armies of developers writing custom connectors.&lt;/p&gt;

&lt;p&gt;Stop overthinking whether MCP will “disrupt” APIs. Start thinking about how to use both strategically. Your existing APIs aren’t going anywhere. But if you want AI to use them intelligently, MCP is how you make that happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Keep building robust APIs - they’re still the foundation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Experiment with MCP for AI-facing services&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch unified MCP providers - they’ll simplify AI integrations like unified APIs simplified SaaS integrations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Focus on the user need, not the technology hype&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The future isn’t MCP versus APIs. It’s &lt;a href="https://www.apideck.com/blog/apis-mcps-or-both-choosing-the-right-ai-integration-stack#the-api-foundation" rel="noopener noreferrer"&gt;MCP plus APIs&lt;/a&gt;, working together to make AI actually useful instead of just impressive.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>api</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Understanding the security landscape of MCP</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Tue, 12 Aug 2025 09:30:00 +0000</pubDate>
      <link>https://dev.to/apideck/understanding-the-security-landscape-of-mcp-216e</link>
      <guid>https://dev.to/apideck/understanding-the-security-landscape-of-mcp-216e</guid>
      <description>&lt;h2&gt;
  
  
  The State of MCP Security in 2025: Key Challenges and Emerging Defenses
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) is becoming the standard for communication and operation between external applications, LLMs, and AI agents. Think of it like USB-C, a standard and a protocol for communication and power transfer. MCPs are also a standard for communication between large language models (LLMs) and other applications, databases, etc.&lt;/p&gt;

&lt;p&gt;It hasn’t been a year (MCP was introduced in November 2024), and the adoption of the protocol has been great so far. We are seeing a wide adoption from giant corporations like Atlassian, AWS, Google, GitHub, Cloudflare, and Supabase. etc., they all have their MCPs available to try and download. And more enterprises are looking forward to being more AI-ready by building MCPs for their LLM stack. As of January 2025, more than &lt;a href="https://www.pulsemcp.com/servers" rel="noopener noreferrer"&gt;1000 community-made MCPs&lt;/a&gt; are available for end-users to try. Model Context Protocol is becoming the standard for enabling data in apps to communicate with LLMs.&lt;/p&gt;

&lt;p&gt;While the hype and adoption are going great, we also need to consider the security, vulnerability, and other issues associated with MCPs. While APIs have been around for more than a decade, we’ve developed security patterns, rate-limiting, and other measures. MCPs must undergo cycles of improvement on the security front. The current goal is to discuss security issues in MCPs and raise awareness among readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Security Challenge: The "Confused Deputy" Problem
&lt;/h2&gt;

&lt;p&gt;The confused deputy is a classic security flaw that is highly relevant to MCP. An MCP server, acting on behalf of a user, might have more permissions than the user. An attacker could potentially trick the LLM into requesting that the MCP server execute with its elevated privileges, leading to unauthorized actions. For example, an LLM could be manipulated into telling a high-privilege MCP server to delete a file that the user shouldn't have been able to delete. The AI doesn't realize it's being manipulated because the malicious instructions come through normal channels like tool descriptions, user inputs, or data it reads from files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Injection Vulnerabilities
&lt;/h3&gt;

&lt;p&gt;This is a major attack vector for any LLM-based application, and MCP creates new opportunities for it. Attackers can embed malicious instructions in documents, emails, or any data that the MCP processes. When the AI reads this content, it interprets the hidden commands as legitimate instructions. Some of the common tricks for prompt injection are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hijack the AI's behavior&lt;/strong&gt;: Make the model ignore previous instructions or perform unintended actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data exfiltration&lt;/strong&gt;: Trick the AI into revealing sensitive information it has access to through other MCP connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger malicious actions&lt;/strong&gt;: Instruct the AI to use one of its connected tools to send spam, delete data, or purchase items.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Supply Chain Risks and Unverified Third-Party Tools
&lt;/h3&gt;

&lt;p&gt;Since anyone can develop an MCP server, there's a significant risk from unverified or malicious third-party tools. You can discuss the following points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Malicious MCP Servers&lt;/strong&gt;: Attackers could create and distribute MCP servers that appear legitimate but are designed to steal data, credentials, or execute malicious code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typosquatting&lt;/strong&gt;: Similar to domain name typosquatting, an attacker could name their malicious MCP server something very similar to a trusted one, hoping users will mistakenly connect to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lack of a Central Vetting Process&lt;/strong&gt;: Unlike an app store, there isn't a universal, mandatory security review for all MCP servers. This places a heavy burden on users and organizations to vet the tools they use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tool Poisoning Attacks
&lt;/h3&gt;

&lt;p&gt;Attackers embed malicious instructions within tool descriptions that are invisible to users but interpreted by AI models. These attacks can manipulate AI behavior to exfiltrate sensitive data, read SSH keys, or execute unauthorized system commands. The scary part is that these instructions can be hidden in any part of the tool definition, not just descriptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credential and Token Exposure
&lt;/h3&gt;

&lt;p&gt;MCP servers need to connect to various external services, and they often do so using API keys, OAuth tokens, and other credentials. This creates a centralized point of failure. If an MCP server is compromised, an attacker could gain access to all the credentials it stores, potentially giving them the "keys to the kingdom" to a user's or a company's various connected services (like email, cloud storage, code repositories, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Over-Privileged Access and Excessive Permissions
&lt;/h3&gt;

&lt;p&gt;Developers of MCP servers might request broad permissions to make their tools as flexible as possible. This violates the principle of least privilege. An MCP server with read, write, and delete access to a user's entire email account, when it only needs to read subject lines, poses a significant and unnecessary risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lack of Granular Monitoring and Auditing
&lt;/h3&gt;

&lt;p&gt;Since MCP is in the early stage, it does not have robust, built-in mechanisms for detailed logging and auditing of all actions performed through it. This can make it difficult to detect malicious activity, investigate security incidents, and understand what data has been accessed or modified.&lt;/p&gt;

&lt;p&gt;What makes this especially dangerous is that MCP servers often store tokens for multiple services all in one place. Suppose an attacker breaks into one MCP server. In that case, they potentially get access to your Gmail, Google Drive, Calendar, and everything else you've connected, creating what security researchers call a "keys to the kingdom" scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has been done so far?
&lt;/h2&gt;

&lt;p&gt;A new &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/changelog" rel="noopener noreferrer"&gt;specification&lt;/a&gt; was released on June 18, 2025, which introduces numerous features and clarifies that all MCP servers are classified as OAuth 2.0 Resource Servers.&lt;/p&gt;

&lt;p&gt;Here are some of the details shared by the team on &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-discovery" rel="noopener noreferrer"&gt;authorization&lt;/a&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization Flow Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key Roles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP Server: Acts as an OAuth 2.1 resource server&lt;/li&gt;
&lt;li&gt;MCP Client: Acts as an OAuth 2.1 client, making requests on behalf of users&lt;/li&gt;
&lt;li&gt;Authorization Server: Issues access tokens (may be hosted with resource server or separately)&lt;/li&gt;
&lt;li&gt;Implementation Requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization Servers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST implement OAuth 2.1 with security measures for both confidential and public clients&lt;/li&gt;
&lt;li&gt;MUST provide OAuth 2.0 Authorization Server Metadata (RFC8414)&lt;/li&gt;
&lt;li&gt;SHOULD support Dynamic Client Registration Protocol (RFC7591)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MCP Servers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728)&lt;/li&gt;
&lt;li&gt;MUST use WWW-Authenticate header when returning 401 Unauthorized&lt;/li&gt;
&lt;li&gt;MUST validate access tokens and ensure they were issued specifically for them&lt;/li&gt;
&lt;li&gt;MUST NOT accept or transit tokens issued for other resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MCP Clients:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST use OAuth 2.0 Authorization Server Metadata and Protected Resource Metadata&lt;/li&gt;
&lt;li&gt;MUST parse WWW-Authenticate headers and respond to 401 responses&lt;/li&gt;
&lt;li&gt;MUST implement PKCE (Proof Key for Code Exchange)&lt;/li&gt;
&lt;li&gt;MUST include resource parameter in authorization and token requests (RFC 8707)&lt;/li&gt;
&lt;li&gt;MUST NOT send tokens other than those issued by the MCP server's authorization server&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security Requirements
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Token Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST use Authorization: Bearer  header for all requests&lt;/li&gt;
&lt;li&gt;MUST NOT include tokens in URI query strings&lt;/li&gt;
&lt;li&gt;MUST validate token audience binding&lt;/li&gt;
&lt;li&gt;MUST implement secure token storage&lt;/li&gt;
&lt;li&gt;Authorization servers SHOULD issue short-lived access tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Communication Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All authorization server endpoints MUST use HTTPS&lt;/li&gt;
&lt;li&gt;All redirect URIs MUST be either localhost or use HTTPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additional Protections:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST implement exact redirect URI validation&lt;/li&gt;
&lt;li&gt;SHOULD use and verify state parameters&lt;/li&gt;
&lt;li&gt;MUST obtain user consent for each dynamically registered client (for proxy servers)&lt;/li&gt;
&lt;li&gt;MUST follow OAuth 2.1 security best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security Best Practices shared by the MCP team at Anthropic
&lt;/h2&gt;

&lt;p&gt;You can read the full specification for the security best practices shared here. It details how to implement the MCPs to avoid falling&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Security Principles
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Token Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP servers MUST NOT accept tokens that were not explicitly issued for the MCP server (avoid "token passthrough")&lt;/li&gt;
&lt;li&gt;Always validate that tokens have the proper audience and were issued specifically for your MCP server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization and Authentication:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP servers that implement authorization MUST verify all inbound requests&lt;/li&gt;
&lt;li&gt;MCP servers MUST NOT use sessions for authentication&lt;/li&gt;
&lt;li&gt;Always obtain proper user consent before accessing third-party services&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Session Security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Session ID Generation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MUST use secure, non-deterministic session IDs&lt;/li&gt;
&lt;li&gt;SHOULD use secure random number generators (avoid predictable or sequential identifiers)&lt;/li&gt;
&lt;li&gt;Consider rotating or expiring session IDs to reduce risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Session Binding:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SHOULD bind session IDs to user-specific information&lt;/li&gt;
&lt;li&gt;Use key formats like : to prevent impersonation&lt;/li&gt;
&lt;li&gt;Combine session data with information unique to the authorized user&lt;/li&gt;
&lt;li&gt;Proxy Server Security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Consent Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When using static client IDs with third-party services, MUST obtain user consent for each dynamically registered client&lt;/li&gt;
&lt;li&gt;Implement proper consent flows to prevent confused deputy attacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation Guidelines
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Security Controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement proper rate limiting, request validation, and traffic monitoring&lt;/li&gt;
&lt;li&gt;Maintain clear audit trails and accountability mechanisms&lt;/li&gt;
&lt;li&gt;Validate token claims (roles, privileges, audience) and metadata&lt;/li&gt;
&lt;li&gt;Preserve trust boundaries between services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These practices are designed to prevent the three main attack vectors identified: confused deputy problems, token passthrough vulnerabilities, and session hijacking attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recent Security Issues for MCPs
&lt;/h2&gt;

&lt;p&gt;The community has reported numerous security issues affecting top companies like GitHub and Supabase. These issues highlight that, despite its rapid adoption, the protocol's security is still maturing and requires immediate and careful attention from all developers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub MCP Exploit:&lt;/strong&gt; The GitHub MCP exploit was discovered in May, allowing unauthorized access to private repositories and the ability to exfiltrate sensitive information. You can read about it in detail here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase MCP:&lt;/strong&gt; The Supabase MCP was found to leak all of your private SQL database. Read the &lt;a href="https://news.ycombinator.com/item?id=44502318" rel="noopener noreferrer"&gt;Hackernews discussion&lt;/a&gt; and the &lt;a href="https://www.generalanalysis.com/blog/supabase-mcp-blog" rel="noopener noreferrer"&gt;original article&lt;/a&gt;, showcasing the attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asana MCP Workspace Data Leak:&lt;/strong&gt; Asana’s MCP allowed users to look into other users’ workspaces. The company took two weeks to fix the issue, ensuring it doesn’t happen again. You can read about the fix and the incident &lt;a href="https://www.theregister.com/2025/06/18/asana_mcp_server_bug/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Injection &amp;amp; Privilege Escalation:&lt;/strong&gt; A single SQL injection bug in Anthropic’s SQLite MCP server (forked 5,000+ times) has enabled attackers to inject stored prompts, exfiltrate data, and escalate privileges within production agents. Many downstream agents, due to the use of unpatched code, remain vulnerable, amplifying both supply-chain and agent-wide compromise risks. Read about this &lt;a href="https://www.trendmicro.com/en_us/research/25/f/why-a-classic-mcp-server-vulnerability-can-undermine-your-entire-ai-agent.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command Injection in mcp-remote Proxy (CVE-2025-6514):&lt;/strong&gt; JFrog researchers revealed a critical (CVSS 9.6) vulnerability in the popular mcp-remote project, used for integrating LLM hosts like Claude Desktop with remote MCP servers. A malicious MCP server can supply a crafted authorization endpoint to the client, resulting in arbitrary OS command execution on Windows and arbitrary executable run on Linux/macOS. The vulnerability affects mcp-remote versions 0.0.5–0.1.15 and is now fixed in 0.1.16. Users connecting to untrusted or insecure MCP servers are most at risk. Read about this &lt;a href="https://jfrog.com/blog/2025-6514-critical-mcp-remote-rce-vulnerability/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many more instances like these, where MCPs can result in new security vulnerabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While the adoption of Model Context Protocol (MCP) is accelerating, it's crucial to address its inherent security risks. Vulnerabilities like prompt injection and token exposure can undermine the protocol's integrity. The new security specifications, based on OAuth 2.0, provide the necessary tools for defense. Ultimately, the long-term success of MCP hinges on developers consistently implementing these security best practices to create a safe and reliable ecosystem.&lt;/p&gt;

</description>
      <category>security</category>
      <category>mcp</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>API Based RAG using Apideck’s Filestorage API, LangChain, Ollama, and Streamlit</title>
      <dc:creator>𝚂𝚊𝚞𝚛𝚊𝚋𝚑 𝚁𝚊𝚒</dc:creator>
      <pubDate>Mon, 11 Aug 2025 10:57:44 +0000</pubDate>
      <link>https://dev.to/apideck/api-based-rag-using-apidecks-filestorage-api-langchain-ollama-and-streamlit-12d2</link>
      <guid>https://dev.to/apideck/api-based-rag-using-apidecks-filestorage-api-langchain-ollama-and-streamlit-12d2</guid>
      <description>&lt;p&gt;In previous articles, we explored the fundamentals of Retrieval-Augmented Generation (RAG) and demonstrated a practical implementation using FAISS, Hugging Face, and Ollama to chat with local data. In this article, we will take RAG a step further. We'll move beyond static vector databases to show how you can fetch information from diverse sources in real-time. This principle is already in effect in major AI assistants and chatbots. This approach is conceptually similar to how services like ChatGPT or Claude can connect to your Google Drive, OneDrive, or Dropbox, retrieving live data to answer your questions.&lt;/p&gt;

&lt;p&gt;For instance, Claude allows users to connect directly to their live data sources, enabling its RAG system to search for real-time information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fne407kkaxobava31xr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fne407kkaxobava31xr.png" alt="Claude OneDrive Integration" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, ChatGPT provides integrations for apps like Google Drive and Microsoft OneDrive, allowing it to retrieve and reason over your personal or work documents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkzqnaljm0hubgd2nbvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkzqnaljm0hubgd2nbvi.png" alt="ChatGPT OneDrive Integration" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.apideck.com/blog/building-a-local-rag-chat-app-with-reflex-langchain-huggingface-and-ollama" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;, we demonstrated a complete Retrieval-Augmented Generation (RAG) pipeline. First, we sourced a dataset from Hugging Face and used the all-MiniLM-L6-v2 model to generate vector embeddings for the text. Next, we indexed these embeddings in a FAISS vector store. When a user asks a question, this system retrieves the most relevant information from the index through semantic search, providing the necessary context to generate a precise answer. This architecture is a classic example of a RAG system powered by a vector database.&lt;/p&gt;

&lt;p&gt;That "classic" approach, however, is just one of many ways to build a RAG system. While vector search on a static document set is common, the true power of RAG lies in its flexibility and adaptability. The retrieval strategy can be adapted to many different data types and needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector Search&lt;/strong&gt;: The method we used previously. Finds data based on conceptual similarity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lexical Search&lt;/strong&gt;: Traditional keyword-based search (e.g., BM25) that excels at matching specific terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid Search&lt;/strong&gt;: A powerful combination of both vector and lexical search for balanced results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Retrieval&lt;/strong&gt;: Querying structured sources like databases (using SQL) or knowledge graphs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API-based Retrieval&lt;/strong&gt;: A dynamic approach where the system calls an external tool or API to fetch live information, rather than relying on a pre-built, static index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we will focus on the powerful &lt;strong&gt;API-based retrieval&lt;/strong&gt; method. Our goal is to build a system that utilizes the file-storage API to select files directly from Box.com, then feed their content to an LLM to generate summaries in real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Storage API
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.apideck.com/file-storage-api" rel="noopener noreferrer"&gt;file storage API&lt;/a&gt; provides a unified interface for major online storage platforms, including Box, Google Drive, OneDrive, Dropbox, and SharePoint.&lt;/p&gt;

&lt;p&gt;Instead of juggling multiple APIs for different storage providers, the file storage API provides a single endpoint for pushing and pulling data across all these systems. Think of it like how Claude and ChatGPT handle app connections; it uses a secure vault for authorization, then provides seamless access to list and download files from your connected storage. Our objective is to: fetch a list of available files, select the relevant ones, and download them for data summarization. Since all Apideck APIs share the same base: &lt;a href="https://unify.apideck.com" rel="noopener noreferrer"&gt;https://unify.apideck.com&lt;/a&gt;, which then branches into specific unified endpoints like /accounting, /file-storage, and /ats. For our purposes, we'll work with &lt;a href="https://unify.apideck.com/file-storage" rel="noopener noreferrer"&gt;https://unify.apideck.com/file-storage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To keep things simple, we're focusing exclusively on the Box.com connector and its file operations. All our operations will use the base URL: &lt;a href="https://unify.apideck.com/file-storage/" rel="noopener noreferrer"&gt;https://unify.apideck.com/file-storage/&lt;/a&gt;, with Apideck handling the underlying complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's how Apideck’s file-storage API works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficr994o9uxa84eaak8d1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficr994o9uxa84eaak8d1.gif" alt="file-storage API works" width="760" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The File Storage API is a unified API gateway that connects to multiple applications providing document storage. Suppose a developer has to create a service that connects to Google Drive, SharePoint, and Box. In that case, they have to build and maintain three separate connectors, each with different data fields and authentication logic. This is a major overhead. Apideck solves this by providing a simple, unified API structure. It handles maintaining the connectors and mapping the different data models, so for the developer, the API endpoint is always the same.&lt;/p&gt;

&lt;p&gt;Take this example: the base URL is always &lt;a href="https://unify.apideck.com" rel="noopener noreferrer"&gt;https://unify.apideck.com&lt;/a&gt;. For the File Storage API, you add the path, like &lt;code&gt;/file-storage/files/{id}&lt;/code&gt;. This consistency makes managing the APIs much easier.&lt;/p&gt;

&lt;p&gt;All calls, whether to Box, Google Drive, or SharePoint, go through the same Apideck endpoint. Apideck uses a set of simple headers to direct the request to the right place. That’s it. Apideck handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s what a real API call to download a file looks like using curl:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--url&lt;/span&gt; https://unify.apideck.com/file-storage/files/&lt;span class="o"&gt;{&lt;/span&gt;file_id&lt;span class="o"&gt;}&lt;/span&gt;/download &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_API_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-apideck-app-id: YOUR_APP_ID'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-apideck-consumer-id: a_user_id_from_your_app'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-apideck-service-id: box'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Authorization: Your secret Apideck API key.&lt;/li&gt;
&lt;li&gt;x-apideck-app-id: The ID of your application in Apideck.&lt;/li&gt;
&lt;li&gt;x-apideck-consumer-id: The ID of the end-user in your system.&lt;/li&gt;
&lt;li&gt;x-apideck-service-id: This is where you specify which service to use, like Box, Google Drive, or SharePoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let’s talk about authentication, how to get access to the box via APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does our application get permission to access a user's files in the first place?
&lt;/h2&gt;

&lt;p&gt;Handling authentication for services like Box or Google Drive can be complex. Each has its own OAuth 2.0 flow, requiring you to manage redirects, handle callbacks, and securely store sensitive access and refresh tokens. Building this for multiple providers is not just a development challenge; it’s a significant security responsibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fremyue2im7r0cckw9dai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fremyue2im7r0cckw9dai.png" alt="Apideck Box.com connector" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where Apideck Vault comes in. Vault acts as a secure, isolated container that manages the entire authentication process on your behalf. You can direct your users to a pre-built, secure UI called &lt;strong&gt;Hosted Vault&lt;/strong&gt;, where they can safely log in to their Box or Google Drive accounts. Vault handles the entire OAuth handshake and securely encrypts and stores the user's credentials, completely abstracting them away from your application. Your system then only needs to reference the user's consumer_id in your API calls, as shown in the example above. This approach drastically simplifies development and enhances security, as your application never has to handle or store the end-user's sensitive API tokens. You can read about the vault &lt;a href="https://developers.apideck.com/guides/vault" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Functionality Overview
&lt;/h2&gt;

&lt;p&gt;Our system follows a simple workflow: The app first fetches a list of files present on the active connectors (Box in our case). We fetch and display the files available in the Box connector on our dashboard. The user selects a specific file from a drop-down list in the app. The app then downloads the selected file. Once the file is downloaded, the AI generates a summary of its content, which is delivered to the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Requirements
&lt;/h3&gt;

&lt;p&gt;A free Apideck account. Python 3.12+ installed in your system. Basic understanding of how virtual environments, requests, etc. works in Python. Knowledge of LangChain, LLM basics, and how APIs work would help in better understanding the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the Environment
&lt;/h3&gt;

&lt;p&gt;Before we dive into the code, we need to handle our credentials securely. Create a .env file in the root of your project directory. This file will store the API keys that our application needs to connect to Apideck.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="nv"&gt;APIDECK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk_live_..."&lt;/span&gt;
    &lt;span class="nv"&gt;APIDECK_APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_APP_ID..."&lt;/span&gt;
    &lt;span class="nv"&gt;APIDECK_CONSUMER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"test-consumer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can get your API_KEY and APP_ID directly from your Apideck dashboard under the API Keys section. The CONSUMER_ID is a unique identifier for the end-user of your application; for this demo, we can just use a static value like test-consumer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Coding Workflow
&lt;/h3&gt;

&lt;p&gt;Our application logic is split into two main utility files: apideck_utils.py for handling the file storage connection, and llm_utils.py for processing the documents with our AI model. The full code can be found in this GitHub repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting to Apideck (apideck_utils.py)
&lt;/h3&gt;

&lt;p&gt;The code for this file can be found &lt;a href="https://github.com/srbhr/api-powered-rag/blob/main/apideck_utils.py" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apideck_unify&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Apideck&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_file_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;box&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetches a list of files from the specified service.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Apideck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;consumer_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;apideck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apideck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_files_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_files_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_files_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No files found or an issue occurred.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An error occurred while fetching file list: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;box&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Downloads a specific file and saves it locally.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;download_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://unify.apideck.com/file-storage/files/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/download&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-apideck-app-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-apideck-consumer-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;consumer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-apideck-service-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;download_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Successfully downloaded &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An error occurred during download: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An unexpected error occurred: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is our bridge to the Apideck File Storage API. It contains two core functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;fetch_file_list()&lt;/code&gt;: This function initializes the Apideck SDK with our credentials and calls the file_storage.files.list() endpoint. It simply asks Apideck to return a list of all files available in the connected Box account and gives us back a clean list of file objects, including their names and unique IDs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;download_file()&lt;/code&gt;: Once we have a file's ID, this function takes over. As we discovered during development, the most reliable way to handle downloads is to make a direct HTTP request to the download endpoint. The function constructs the specific URL (&lt;a href="https://unify.apideck.com/file-storage/files/%7Bid%7D/download" rel="noopener noreferrer"&gt;https://unify.apideck.com/file-storage/files/{id}/download&lt;/a&gt;) and includes the necessary authentication headers (Authorization, x-apideck-app-id, etc.). It then uses the requests library to fetch the file, automatically handling any redirects from Apideck's servers to Box's content servers. The raw file content is then saved locally.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Summarizing with LangChain and Ollama (llm_utils.py)
&lt;/h3&gt;

&lt;p&gt;The code for this file can be found &lt;a href="https://github.com/srbhr/api-powered-rag/blob/main/llm_utils.py" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.document_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PyPDFLoader&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOllama&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chains.summarize&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_summarize_chain&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PromptTemplate&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Extracts text from a PDF and uses a local Ollama model to generate a detailed summary.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOllama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write a concise summary of the following text.
    Aim for a summary that is about 4-5 sentences long.
    After the summary, provide 2-3 key takeaways as bullet points.

    Text:
    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{text}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

    CONCISE SUMMARY AND KEY TAKEAWAYS:&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt_template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PyPDFLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_summarize_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;map_reduce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combine_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROMPT&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;summary_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;summary_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: The file was not found at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pdf_file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An error occurred during summarization: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Please ensure Ollama is running and the PDF file is valid.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file has one main function, summarize_pdf(), that orchestrates the entire summarization process using LangChain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initialize the LLM&lt;/strong&gt;: First, we connect to our local AI model using langchain_ollama import ChatOllama. We point it to the model we're running locally (gemma3:1b-it-qat).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define a Prompt&lt;/strong&gt;: We create a custom PromptTemplate. This is more than just asking for a summary; it's a specific set of instructions for the AI. We ask it to write a summary of a certain length and then provide key takeaways as bullet points, ensuring the output is structured and consistently useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the Document&lt;/strong&gt;: Using PyPDFLoader from langchain_community, the function loads the PDF file that we just downloaded and splits its content into processable documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the Chain&lt;/strong&gt;: Finally, we use LangChain's powerful load_summarize_chain. We configure it with the map_reduce chain type, which is excellent for documents of any size. It first runs our custom prompt on smaller chunks of the document (the "map" step) and then combines those partial summaries into a final, coherent output (the "reduce" step). The final text is then returned to the main application for display.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Final Application
&lt;/h3&gt;

&lt;p&gt;To bring this all together, we've built a simple but powerful user interface using Streamlit. This application orchestrates the entire workflow, serving as a practical demonstration of API-based RAG. Providing the user with AI-generated summaries of their files from Box. The complete code for the project can be found on GitHub &lt;a href="https://github.com/srbhr/api-powered-rag" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fto92zl9i1vj8rpdncrc6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fto92zl9i1vj8rpdncrc6.jpg" alt="connecting apideck with box and AI RAG" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what the final application looks like in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxnhmfh996rsvo3fhihn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxnhmfh996rsvo3fhihn.gif" alt="Our Final application" width="720" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion &amp;amp; What’s Next?
&lt;/h2&gt;

&lt;p&gt;This article concludes our three-part series on building modern RAG systems. We began by establishing a foundational understanding of RAG with a static vector database using FAISS. In this final piece, we evolved that concept significantly by integrating a live data source through the Apideck File Storage API. By connecting directly to a service like Box, we’ve shown that RAG is not limited to static, pre-indexed documents but can be a dynamic, powerful tool for interacting with real-time information from a vast array of sources.&lt;/p&gt;

&lt;p&gt;This project is just the starting point for API-based RAG. The true potential comes into play when an AI Agent can request data from multiple systems simply and easily. The groundwork we've laid with the File Storage API can be directly extended. Imagine building an agent that not only fetches files but also pulls customer data from a CRM or candidate information from an ATS. Since Apideck also provides unified APIs for those systems, you can create sophisticated agents that reason across your entire business toolkit.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>python</category>
      <category>api</category>
    </item>
  </channel>
</rss>
