Iteration Layer

Building Multi-Tenant Document Pipelines: Architecture Guide for Agencies

15 min read

The Five-Client Problem

You run an agency. You have five active client projects, each with a document processing pipeline. Client A processes supplier invoices for a fleet management company. Client B extracts data from insurance claims. Client C generates branded PDF reports for a real estate platform. Client D transforms product images for an e-commerce marketplace. Client E does all of the above for a logistics company.

Each pipeline is different. Each client has different volume, different budgets, and different compliance requirements. But every pipeline uses the same categories of operations: extract, transform, generate.

The operational question is: how do you structure this so it scales to ten clients, then twenty, without the management overhead growing linearly with every new engagement?

Most agencies solve this one of two ways, and both are wrong.

Approach 1: One shared API key for everything. Fast to set up, impossible to track. When the invoice arrives, you cannot tell how much of the usage came from Client A versus Client C. You cannot set spending limits per client. If one client’s pipeline has a bug that burns through credits, every client is affected. And if you need to revoke access for one client (because the engagement ended or the key was compromised), you have to rotate the key for everyone.

Approach 2: Separate vendor accounts per client. Clean isolation, but management overhead scales linearly. Five accounts means five logins, five invoices, five credential sets, five separate credit pools. You cannot move unused credits from a quiet client to a busy one. Onboarding a new client means creating a new vendor account, configuring billing, setting up credentials, and adding another line to your monthly reconciliation spreadsheet.

There is a better architecture. This guide covers how to structure a multi-tenant document processing setup from a single organization account, with scoped isolation between projects and unified management overhead.

The Multi-Project Architecture

Iteration Layer’s project system is built for exactly this use case. One organization account, multiple projects. Each project represents a client engagement. Each project has its own scoped API keys, its own usage tracking, and its own optional budget cap. All projects share the organization’s credit pool.

Organization Account (your agency)
├── Project: Client A (Fleet Management)
│   ├── API Key: il_proj_clienta_prod_...
│   ├── API Key: il_proj_clienta_dev_...
│   ├── Budget Cap: 2,000 credits/month
│   └── Usage: tracked separately
├── Project: Client B (Insurance Claims)
│   ├── API Key: il_proj_clientb_prod_...
│   ├── Budget Cap: 5,000 credits/month
│   └── Usage: tracked separately
├── Project: Client C (Real Estate Reports)
│   ├── API Key: il_proj_clientc_prod_...
│   ├── Budget Cap: 1,000 credits/month
│   └── Usage: tracked separately
└── Shared Credit Pool: Organization subscription

This gives you the isolation of separate accounts with the operational simplicity of one account.

Scoped API Keys: Isolation Without Overhead

Each project gets its own API keys. These keys are scoped to the project — they can only be used to make API calls that are tracked against that project’s usage. They cannot see or affect other projects.

Why Scoped Keys Matter

Security isolation. If a client project’s key is compromised (leaked in a git commit, exposed in a client’s frontend code, discovered in a log file), you revoke that one key. Other projects are unaffected. No organization-wide key rotation, no downtime for other clients.

Usage attribution. Every API call made with a project-scoped key is automatically attributed to that project. When you check usage at the end of the month, you see exactly how many credits each project consumed, broken down by API. No manual tagging, no query parameters to track project IDs, no post-hoc log analysis.

Access control. You can give a client’s development team a key scoped to their project. They can integrate and test against the API without seeing your other clients’ usage, keys, or configuration. This is useful when a client wants to build on top of the pipeline you set up, or when you are handing off a project at the end of an engagement.

Setting Up Project Keys

From your organization dashboard, creating a project and generating scoped keys is straightforward. Each project can have multiple keys (production, staging, development), and each key is independently revocable.

The scoped key works exactly like an organization-level key — same API endpoints, same request format, same response format. The only difference is that usage is tracked against the project:

curl -X POST https://api.iterationlayer.com/document-extraction/v1/extract \
  -H "Authorization: Bearer il_proj_fleet_mgmt_prod_key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "type": "url",
        "name": "traffic-fine.pdf",
        "url": "https://storage.example.com/fleet-mgmt/fines/FINE-2026-0472.pdf"
      }
    ],
    "schema": {
      "fields": [
        {
          "name": "vehicle_plate",
          "description": "License plate number of the vehicle",
          "type": "TEXT"
        },
        {
          "name": "violation_date",
          "description": "Date the traffic violation occurred",
          "type": "DATE"
        },
        {
          "name": "fine_amount",
          "description": "Amount of the fine including fees",
          "type": "CURRENCY_AMOUNT"
        },
        {
          "name": "violation_country",
          "description": "Country where the violation occurred",
          "type": "COUNTRY"
        }
      ]
    }
  }'
const fleetClient = new IterationLayer({
  apiKey: "il_proj_fleet_mgmt_prod_key",
});

const result = await fleetClient.extract({
  files: [
    {
      type: "url",
      name: "traffic-fine.pdf",
      url: "https://storage.example.com/fleet-mgmt/fines/FINE-2026-0472.pdf",
    },
  ],
  schema: {
    fields: [
      {
        name: "vehicle_plate",
        description: "License plate number of the vehicle",
        type: "TEXT",
      },
      {
        name: "violation_date",
        description: "Date the traffic violation occurred",
        type: "DATE",
      },
      {
        name: "fine_amount",
        description: "Amount of the fine including fees",
        type: "CURRENCY_AMOUNT",
      },
      {
        name: "violation_country",
        description: "Country where the violation occurred",
        type: "COUNTRY",
      },
    ],
  },
});
fleet_client = IterationLayer(api_key="il_proj_fleet_mgmt_prod_key")

result = fleet_client.extract(
    files=[
        {
            "type": "url",
            "name": "traffic-fine.pdf",
            "url": "https://storage.example.com/fleet-mgmt/fines/FINE-2026-0472.pdf",
        }
    ],
    schema={
        "fields": [
            {
                "name": "vehicle_plate",
                "description": "License plate number of the vehicle",
                "type": "TEXT",
            },
            {
                "name": "violation_date",
                "description": "Date the traffic violation occurred",
                "type": "DATE",
            },
            {
                "name": "fine_amount",
                "description": "Amount of the fine including fees",
                "type": "CURRENCY_AMOUNT",
            },
            {
                "name": "violation_country",
                "description": "Country where the violation occurred",
                "type": "COUNTRY",
            },
        ]
    },
)
fleetClient := iterationlayer.NewClient("il_proj_fleet_mgmt_prod_key")

result, err := fleetClient.Extract(iterationlayer.ExtractRequest{
    Files: []iterationlayer.FileInput{
        iterationlayer.NewFileFromURL("traffic-fine.pdf",
            "https://storage.example.com/fleet-mgmt/fines/FINE-2026-0472.pdf"),
    },
    Schema: iterationlayer.ExtractionSchema{
        Fields: []iterationlayer.FieldConfig{
            iterationlayer.TextFieldConfig{
                Name:        "vehicle_plate",
                Description: "License plate number of the vehicle",
            },
            iterationlayer.DateFieldConfig{
                Name:        "violation_date",
                Description: "Date the traffic violation occurred",
            },
            iterationlayer.CurrencyAmountFieldConfig{
                Name:        "fine_amount",
                Description: "Amount of the fine including fees",
            },
            iterationlayer.CountryFieldConfig{
                Name:        "violation_country",
                Description: "Country where the violation occurred",
            },
        },
    },
})

This request is identical to any other extraction call. The scoping happens at the authentication layer — the API key determines which project the usage is attributed to. No additional headers, no project ID parameters, no routing configuration.

Per-Project Budget Caps: Preventing Runaway Costs

Budget caps are the safety mechanism that lets you sleep at night when five automated pipelines are running unattended.

The Problem Budget Caps Solve

Automated pipelines fail in two directions: they stop working (which you notice immediately) or they start working too much (which you notice on the invoice).

A bug in a retry loop that resubmits the same 50-page document 200 times. A client who uploads a batch of 10,000 documents when you scoped the project for 500/month. A webhook configuration that creates a feedback loop between two services. Each of these scenarios can consume your entire credit pool in hours.

Without per-project budget caps, one runaway project affects every project. The shared credit pool hits zero, and all five clients’ pipelines stop processing.

With per-project budget caps, the runaway project hits its cap and returns a 402 error. The other four projects continue operating normally. You get alerted, investigate the cause, and fix it — without a fire drill across your entire client portfolio.

Setting Budget Caps

Budget caps are set per project in your organization dashboard. You specify the maximum number of credits the project can consume per billing period.

When a project exceeds its cap, API requests using that project’s keys return a 402 status code. The response body tells you the cap has been reached. Other projects on your account are completely unaffected — each budget cap is independent.

You can adjust the cap at any time. If a client’s volume increases mid-month, you raise the cap from the dashboard. If you need to temporarily pause a project’s processing, set the cap to zero.

Sizing Budget Caps

Budget caps should be set with headroom above expected usage, but tight enough to catch anomalies. A good rule of thumb:

Expected monthly usage Budget cap
500 credits 750-1,000 credits
2,000 credits 3,000-4,000 credits
10,000 credits 12,000-15,000 credits

The cap should be high enough that normal usage variation does not trigger it, but low enough that a runaway pipeline gets caught before it consumes a meaningful portion of your credit pool.

Monitoring and Alerts

Budget caps are a safety net, not a monitoring strategy. You should track per-project usage proactively, not just wait for caps to trigger.

A healthy monitoring setup tracks:

  • Daily credit consumption per project (compare to historical average)
  • Credit consumption rate (credits per hour — sudden spikes indicate anomalies)
  • Remaining credits in the organization pool (to forecast when you need to top up)
  • Per-API breakdown per project (to understand which operations drive costs)

Billing Client Projects Accurately

Accurate client billing is the difference between an agency that makes money on processing and one that subsidizes it unknowingly.

The Cost-Plus Model

The most straightforward billing model for agencies: calculate the actual processing cost for each client project, add your margin, and invoice.

Step 1: Determine your per-credit cost. This depends on your plan — see Credits & Pricing for the current rates per plan and pay-as-you-go tiers.

Step 2: Pull per-project usage from the dashboard. Each project’s usage is tracked separately — total credits consumed, broken down by API and by time period.

Step 3: Calculate the client cost. Multiply the project’s credit consumption by your per-credit cost, then add your margin.

Example calculation for Client A (Fleet Management):

Operation Monthly volume Credits per request Total credits
Traffic fine extraction (avg. 2 pages) 800 documents 2 per document 1,600
Summary report generation 4 monthly reports 2 per report 8
Fine notice image processing 200 images 1 per image 200
Total 1,808

Look up your per-credit rate in Credits & Pricing, multiply by total credits, and you have your processing cost. Add a 3x markup (covering your integration work, support, and margin) to get the client-facing price.

The Fixed-Fee Model

Many agencies prefer fixed-fee pricing for client predictability. The unified credit system makes this straightforward to forecast.

Step 1: Estimate monthly volume during project scoping. “Client B expects approximately 300 insurance claims per month, each averaging 5 pages.”

Step 2: Calculate expected credit consumption.

Operation Monthly volume Credits
Claim extraction (5 pages avg.) 300 claims 1,500
Claim summary generation 300 summaries 600
Total 2,100

Step 3: Set a fixed monthly fee that covers the expected consumption, a volume buffer (20-30%), your margin, and your integration/support time.

Multiply 2,100 credits by your per-credit rate, add a 25% volume buffer, then apply your margin. Round to a clean number for the client proposal.

Step 4: Set the budget cap slightly above the buffered volume (e.g., 2,800 credits) to protect against overages without cutting off normal processing.

The Per-Unit Model

For clients with highly variable volume, charge per unit processed:

  • “$0.15 per invoice processed” (covering extraction + report generation credits + margin)
  • “$0.05 per image transformed” (covering transformation credits + margin)

Per-project usage tracking makes this easy to reconcile. At the end of the month, pull the project’s usage, calculate the per-unit count, and invoice.

The Agency Playbook: Running Iteration Layer at Scale

Here is the practical workflow for an agency managing multiple client projects on Iteration Layer.

Onboarding a New Client

Step 1: Create the project.

  1. Log into your organization dashboard
  2. Create a new project (name it after the client or engagement)
  3. Generate a production API key and a development/staging key
  4. Set an initial budget cap based on your scope estimate

Step 2: Build the pipeline.

Because every Iteration Layer API follows the same patterns — same auth, same request format, same response format, same SDK methods — you are not learning a new tool. You are configuring an existing one.

For a client that needs invoice extraction and report generation, the pipeline code looks like this:

# Step 1: Extract invoice data
EXTRACTION_RESULT=$(curl -s -X POST \
  https://api.iterationlayer.com/document-extraction/v1/extract \
  -H "Authorization: Bearer il_proj_newclient_prod_key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "type": "url",
        "name": "invoice.pdf",
        "url": "https://storage.example.com/newclient/invoices/INV-001.pdf"
      }
    ],
    "schema": {
      "fields": [
        {
          "name": "vendor_name",
          "description": "Name of the vendor",
          "type": "TEXT"
        },
        {
          "name": "line_items",
          "description": "Invoice line items",
          "type": "ARRAY",
          "item_schema": {
            "fields": [
              {
                "name": "description",
                "description": "Item description",
                "type": "TEXT"
              },
              {
                "name": "amount",
                "description": "Line item amount",
                "type": "CURRENCY_AMOUNT"
              }
            ]
          }
        },
        {
          "name": "total",
          "description": "Invoice total including VAT",
          "type": "CURRENCY_AMOUNT"
        }
      ]
    }
  }')

echo "$EXTRACTION_RESULT"

# Step 2: Generate a summary report from extracted data
# (Use the extracted data to populate the document generation request)
curl -X POST https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer il_proj_newclient_prod_key" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "pdf",
    "document": {
      "metadata": {
        "title": "Invoice Summary Report"
      },
      "content": [
        {
          "type": "headline",
          "level": "h1",
          "text": "Invoice Summary"
        },
        {
          "type": "paragraph",
          "markdown": "**Vendor:** Extracted vendor name\n\n**Total:** Extracted total amount"
        }
      ]
    }
  }' --output summary-report.pdf
const client = new IterationLayer({
  apiKey: "il_proj_newclient_prod_key",
});

// Step 1: Extract invoice data
const extractionResult = await client.extract({
  files: [
    {
      type: "url",
      name: "invoice.pdf",
      url: "https://storage.example.com/newclient/invoices/INV-001.pdf",
    },
  ],
  schema: {
    fields: [
      {
        name: "vendor_name",
        description: "Name of the vendor",
        type: "TEXT",
      },
      {
        name: "line_items",
        description: "Invoice line items",
        type: "ARRAY",
        item_schema: {
          fields: [
            {
              name: "description",
              description: "Item description",
              type: "TEXT",
            },
            {
              name: "amount",
              description: "Line item amount",
              type: "CURRENCY_AMOUNT",
            },
          ],
        },
      },
      {
        name: "total",
        description: "Invoice total including VAT",
        type: "CURRENCY_AMOUNT",
      },
    ],
  },
});

// Step 2: Generate summary report from extracted data
const report = await client.generateDocument({
  format: "pdf",
  document: {
    metadata: {
      title: "Invoice Summary Report",
    },
    content: [
      {
        type: "headline",
        level: "h1",
        text: "Invoice Summary",
      },
      {
        type: "paragraph",
        markdown: `**Vendor:** ${extractionResult.vendor_name.value}\n\n**Total:** ${extractionResult.total.value}`,
      },
    ],
  },
});
client = IterationLayer(api_key="il_proj_newclient_prod_key")

# Step 1: Extract invoice data
extraction_result = client.extract(
    files=[
        {
            "type": "url",
            "name": "invoice.pdf",
            "url": "https://storage.example.com/newclient/invoices/INV-001.pdf",
        }
    ],
    schema={
        "fields": [
            {
                "name": "vendor_name",
                "description": "Name of the vendor",
                "type": "TEXT",
            },
            {
                "name": "line_items",
                "description": "Invoice line items",
                "type": "ARRAY",
                "item_schema": {
                    "fields": [
                        {
                            "name": "description",
                            "description": "Item description",
                            "type": "TEXT",
                        },
                        {
                            "name": "amount",
                            "description": "Line item amount",
                            "type": "CURRENCY_AMOUNT",
                        },
                    ]
                },
            },
            {
                "name": "total",
                "description": "Invoice total including VAT",
                "type": "CURRENCY_AMOUNT",
            },
        ]
    },
)

# Step 2: Generate summary report from extracted data
report = client.generate_document(
    format="pdf",
    document={
        "metadata": {
            "title": "Invoice Summary Report",
        },
        "content": [
            {
                "type": "headline",
                "level": "h1",
                "text": "Invoice Summary",
            },
            {
                "type": "paragraph",
                "markdown": f"**Vendor:** {extraction_result['vendor_name']['value']}\n\n**Total:** {extraction_result['total']['value']}",
            },
        ],
    },
)
client := iterationlayer.NewClient("il_proj_newclient_prod_key")

// Step 1: Extract invoice data
extractionResult, err := client.Extract(iterationlayer.ExtractRequest{
    Files: []iterationlayer.FileInput{
        iterationlayer.NewFileFromURL("invoice.pdf",
            "https://storage.example.com/newclient/invoices/INV-001.pdf"),
    },
    Schema: iterationlayer.ExtractionSchema{
        Fields: []iterationlayer.FieldConfig{
            iterationlayer.TextFieldConfig{
                Name:        "vendor_name",
                Description: "Name of the vendor",
            },
            iterationlayer.ArrayFieldConfig{
                Name:        "line_items",
                Description: "Invoice line items",
                ItemSchema: iterationlayer.ExtractionSchema{
                    Fields: []iterationlayer.FieldConfig{
                        iterationlayer.TextFieldConfig{
                            Name:        "description",
                            Description: "Item description",
                        },
                        iterationlayer.CurrencyAmountFieldConfig{
                            Name:        "amount",
                            Description: "Line item amount",
                        },
                    },
                },
            },
            iterationlayer.CurrencyAmountFieldConfig{
                Name:        "total",
                Description: "Invoice total including VAT",
            },
        },
    },
})

// Step 2: Generate summary report from extracted data
report, err := client.GenerateDocument(iterationlayer.GenerateDocumentRequest{
    Format: "pdf",
    Document: iterationlayer.DocumentDefinition{
        Metadata: iterationlayer.DocumentMetadata{
            Title: "Invoice Summary Report",
        },
        Content: []iterationlayer.ContentBlock{
            iterationlayer.HeadlineBlock{
                Level: "h1",
                Text:  "Invoice Summary",
            },
            iterationlayer.ParagraphBlock{
                Markdown: "**Vendor:** extracted vendor name\n\n**Total:** extracted total",
            },
        },
    },
})

Notice the composability: the extraction result feeds directly into the document generation request. Same API key, same client instance, same error handling patterns. The output of one API becomes the input to the next without format conversion or middleware.

Step 3: Test and deploy.

Use the development API key for testing. The development key tracks usage separately from production (if you set up separate dev projects), so test runs do not pollute your production usage metrics or hit your client’s budget cap.

Monthly Operations

Usage review (15 minutes per project):

  1. Check per-project usage in the dashboard
  2. Compare to budget cap and expected volume
  3. Flag any anomalies (sudden spikes or drops)
  4. Export usage data for client invoicing

Client billing (5 minutes per project):

  1. Pull the project’s credit consumption for the billing period
  2. Apply your billing model (cost-plus, fixed-fee, or per-unit)
  3. Generate the client invoice

Budget cap review (5 minutes per project, quarterly):

  1. Compare actual usage to cap over the last quarter
  2. Adjust caps based on trends (growing clients need higher caps)
  3. Review any cap-triggered incidents and their root causes

Scaling to Twenty Projects

The architecture scales linearly. Each new project is:

  • A new project in the dashboard (2 minutes)
  • Two new API keys — production and development (1 minute)
  • A budget cap configuration (1 minute)
  • Pipeline code that follows the same patterns as every other project

The operational overhead per project is roughly constant. Twenty projects takes the same 15-20 minutes of monthly management per project as five projects. There is no exponential increase in coordination, credential management, or billing complexity.

Handing Off Projects

When a client engagement ends or you are handing off the pipeline to the client’s internal team:

  1. Transfer the project. The client creates their own Iteration Layer account. You help them set up the project on their account and migrate the API keys.
  2. Or keep the project on your account. If you are providing ongoing managed services, the project stays on your account. The client pays you; you pay for the credits.
  3. Revoke old keys. Once the handoff is complete, revoke the old project keys. Other projects are unaffected.

The handoff is clean because the project isolation is real. There are no shared configurations, no cross-project dependencies, no entangled credentials.

Common Pitfalls and How to Avoid Them

Pitfall 1: Sharing API Keys Across Projects

It is tempting to use one API key for “all development” or “all small clients.” Do not do this. Per-project usage tracking only works if each project has its own key. The five minutes saved by sharing a key will cost hours of manual usage attribution at billing time.

Pitfall 2: Setting Budget Caps Too Tight

If the budget cap triggers during normal operation, it disrupts the client’s pipeline and erodes trust. Set caps with at least 50% headroom above expected peak usage. A cap that never triggers is still valuable — it protects against catastrophic failure modes, not normal variation.

Pitfall 3: Not Tracking Development Usage Separately

Development and testing usage should not be mixed with production usage. Create separate development projects (or use separate keys with clearly labeled purposes) so that your test runs do not inflate the usage numbers you use for client billing.

Pitfall 4: Billing Clients Based on Plan Cost Instead of Actual Usage

If some months a client uses very little, do not bill them the proportional plan cost. Bill them based on actual credit consumption (at your marked-up rate) or a minimum monthly fee. This keeps pricing fair and defensible.

Pitfall 5: Not Documenting the Pipeline for Handoff

Every client project should have a brief document that describes: what the pipeline does, which API calls it makes, what the expected monthly volume is, and what the budget cap is set to. When you hand off a project — or when a team member takes over — this document prevents knowledge loss.

Compliance at the Multi-Tenant Level

For agencies serving EU clients, the multi-project architecture also simplifies compliance documentation.

One DPA, All Projects

You sign one Data Processing Agreement with Iteration Layer. That DPA covers all processing across all projects. You do not need a separate vendor DPA per client engagement.

In your DPA with each client, you list Iteration Layer as a sub-processor with the same details:

  • Sub-processor: Iteration Layer
  • Processing location: EU (see Security for details)
  • Data processed: Document content for extraction/generation (zero retention)
  • Retention: None. Files processed in memory, discarded immediately.

This section is identical across client DPAs. It becomes a standard clause in your agency’s DPA template.

Getting Started

If you are an agency evaluating Iteration Layer for multi-client document processing, here is the practical starting point:

  1. Sign up for a free account. Trial credits are available per API — enough to prototype a full pipeline without committing to a subscription.
  2. Create your first project for an existing or upcoming client engagement.
  3. Build one pipeline. Extract data from a client document, generate a report from the extracted data. See how the composability works in practice.
  4. Add a second project for a different client. Notice that the only things that change are the API key and the extraction schema — the integration code, error handling, and response parsing are identical.
  5. Evaluate the operational overhead. Compare the per-project management time to your current multi-vendor setup.

The docs cover every API endpoint with request/response examples. The TypeScript, Python, and Go SDKs handle auth and error parsing.

Build your first workflow in minutes

Chain our APIs together and ship a complete pipeline before lunch. Free trial credits included — no credit card required.