Iteration Layer

Building a Multi-Client Document Pipeline with One API

7 min read

The Setup: Three Clients, Three Pipelines, One Account

You’re an agency with three active clients. Client A is a logistics company that needs invoice extraction. Client B is a law firm that needs contract parsing. Client C is a real estate agency that needs property listing extraction and brochure generation. Each client has different document types, different fields, and different output requirements.

The typical approach: three vendor accounts, three sets of credentials, three billing relationships. You’ve done this before and you know where it leads — a tangle of API keys, inconsistent error handling, and a billing reconciliation process that eats an afternoon every month.

This tutorial walks through setting up all three pipelines on Iteration Layer with project-scoped API keys, per-client extraction schemas, a shared credit pool, and budget caps to keep costs predictable.

Step 1: Create Projects

In the Iteration Layer dashboard, create a project for each client. Projects are organizational containers — they scope API keys and track usage separately while sharing your account’s credit pool.

Navigate to Projects in the sidebar and create three projects:

  • acme-logistics — invoice extraction
  • sterling-legal — contract parsing
  • summit-realty — property listing extraction + brochure generation

Each project gets its own usage tracking. You can see exactly how many credits each client’s pipeline consumes per billing period.

Step 2: Generate Project-Scoped API Keys

For each project, generate an API key. Navigate to API Keys, select the project from the dropdown, and create a new key. The key is scoped to that project — all requests made with it are tracked under that project’s usage.

You’ll end up with three API keys:

  • il_acme1234_... — scoped to acme-logistics
  • il_sterl567_... — scoped to sterling-legal
  • il_summt890_... — scoped to summit-realty

Store these in your deployment environment. Each client’s pipeline uses its own key. If you need to revoke access for one client, you disable their key without affecting the others.

Step 3: Define Extraction Schemas Per Client

Each client needs different fields extracted from different document types. The extraction API uses the same endpoint for all of them — the schema defines what to extract.

Client A: Invoice Extraction

curl -X POST \
  https://api.iterationlayer.com/document-extraction/v1/extract \
  -H "Authorization: Bearer il_acme1234_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "type": "url",
        "name": "invoice-2026-0451.pdf",
        "url": "https://acme-docs.example.com/invoice-2026-0451.pdf"
      }
    ],
    "schema": {
      "fields": [
        {
          "name": "invoice_number",
          "type": "TEXT",
          "description": "Invoice number or reference code"
        },
        {
          "name": "vendor_name",
          "type": "TEXT",
          "description": "Name of the company that issued the invoice"
        },
        {
          "name": "total_amount",
          "type": "CURRENCY_AMOUNT",
          "description": "Total amount due",
          "decimal_points": 2
        },
        {
          "name": "currency",
          "type": "CURRENCY_CODE",
          "description": "Currency of the invoice"
        },
        {
          "name": "due_date",
          "type": "DATE",
          "description": "Payment due date"
        },
        {
          "name": "line_items",
          "type": "ARRAY",
          "description": "Individual line items on the invoice",
          "item_schema": {
            "fields": [
              {
                "name": "description",
                "type": "TEXT",
                "description": "Line item description"
              },
              {
                "name": "quantity",
                "type": "INTEGER",
                "description": "Quantity"
              },
              {
                "name": "unit_price",
                "type": "CURRENCY_AMOUNT",
                "description": "Price per unit",
                "decimal_points": 2
              }
            ]
          }
        }
      ]
    }
  }'
import { IterationLayer } from "iterationlayer";

const acmeClient = new IterationLayer({
  apiKey: "il_acme1234_YOUR_KEY",
});

const invoiceResult = await acmeClient.extract({
  files: [
    {
      type: "url",
      name: "invoice-2026-0451.pdf",
      url: "https://acme-docs.example.com/invoice-2026-0451.pdf",
    },
  ],
  schema: {
    fields: [
      {
        name: "invoice_number",
        type: "TEXT",
        description: "Invoice number or reference code",
      },
      {
        name: "vendor_name",
        type: "TEXT",
        description: "Name of the company that issued the invoice",
      },
      {
        name: "total_amount",
        type: "CURRENCY_AMOUNT",
        description: "Total amount due",
        decimal_points: 2,
      },
      {
        name: "currency",
        type: "CURRENCY_CODE",
        description: "Currency of the invoice",
      },
      {
        name: "due_date",
        type: "DATE",
        description: "Payment due date",
      },
      {
        name: "line_items",
        type: "ARRAY",
        description: "Individual line items on the invoice",
        item_schema: {
          fields: [
            {
              name: "description",
              type: "TEXT",
              description: "Line item description",
            },
            {
              name: "quantity",
              type: "INTEGER",
              description: "Quantity",
            },
            {
              name: "unit_price",
              type: "CURRENCY_AMOUNT",
              description: "Price per unit",
              decimal_points: 2,
            },
          ],
        },
      },
    ],
  },
});
from iterationlayer import IterationLayer

acme_client = IterationLayer(api_key="il_acme1234_YOUR_KEY")

invoice_result = acme_client.extract(
    files=[
        {
            "type": "url",
            "name": "invoice-2026-0451.pdf",
            "url": "https://acme-docs.example.com/invoice-2026-0451.pdf",
        }
    ],
    schema={
        "fields": [
            {
                "name": "invoice_number",
                "type": "TEXT",
                "description": "Invoice number or reference code",
            },
            {
                "name": "vendor_name",
                "type": "TEXT",
                "description": "Name of the company that issued the invoice",
            },
            {
                "name": "total_amount",
                "type": "CURRENCY_AMOUNT",
                "description": "Total amount due",
                "decimal_points": 2,
            },
            {
                "name": "currency",
                "type": "CURRENCY_CODE",
                "description": "Currency of the invoice",
            },
            {
                "name": "due_date",
                "type": "DATE",
                "description": "Payment due date",
            },
            {
                "name": "line_items",
                "type": "ARRAY",
                "description": "Individual line items on the invoice",
                "item_schema": {
                    "fields": [
                        {
                            "name": "description",
                            "type": "TEXT",
                            "description": "Line item description",
                        },
                        {
                            "name": "quantity",
                            "type": "INTEGER",
                            "description": "Quantity",
                        },
                        {
                            "name": "unit_price",
                            "type": "CURRENCY_AMOUNT",
                            "description": "Price per unit",
                            "decimal_points": 2,
                        },
                    ]
                },
            },
        ]
    },
)
package main

import il "github.com/iterationlayer/sdk-go"

acmeClient := il.NewClient("il_acme1234_YOUR_KEY")

invoiceResult, err := acmeClient.Extract(il.ExtractRequest{
    Files: []il.FileInput{
        il.NewFileFromURL(
            "invoice-2026-0451.pdf",
            "https://acme-docs.example.com/invoice-2026-0451.pdf",
        ),
    },
    Schema: il.ExtractionSchema{
        "invoice_number": il.NewTextFieldConfig(
            "invoice_number", "Invoice number or reference code",
        ),
        "vendor_name": il.NewTextFieldConfig(
            "vendor_name", "Name of the company that issued the invoice",
        ),
        "total_amount": il.NewCurrencyAmountFieldConfig(
            "total_amount", "Total amount due",
        ),
        "currency": il.NewCurrencyCodeFieldConfig(
            "currency", "Currency of the invoice",
        ),
        "due_date": il.NewDateFieldConfig(
            "due_date", "Payment due date",
        ),
    },
})

Client B: Contract Parsing

The same endpoint, different schema. Client B’s law firm needs party names, effective dates, termination clauses, and governing law extracted from contracts.

curl -X POST \
  https://api.iterationlayer.com/document-extraction/v1/extract \
  -H "Authorization: Bearer il_sterl567_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "type": "url",
        "name": "service-agreement-2026.pdf",
        "url": "https://sterling-docs.example.com/service-agreement-2026.pdf"
      }
    ],
    "schema": {
      "fields": [
        {
          "name": "party_a",
          "type": "TEXT",
          "description": "First contracting party (the service provider)"
        },
        {
          "name": "party_b",
          "type": "TEXT",
          "description": "Second contracting party (the client)"
        },
        {
          "name": "effective_date",
          "type": "DATE",
          "description": "Date the contract takes effect"
        },
        {
          "name": "termination_date",
          "type": "DATE",
          "description": "Date the contract expires or can be terminated"
        },
        {
          "name": "governing_law",
          "type": "TEXT",
          "description": "Jurisdiction whose law governs the contract"
        },
        {
          "name": "total_value",
          "type": "CURRENCY_AMOUNT",
          "description": "Total contract value",
          "decimal_points": 2
        }
      ]
    }
  }'
const sterlingClient = new IterationLayer({
  apiKey: "il_sterl567_YOUR_KEY",
});

const contractResult = await sterlingClient.extract({
  files: [
    {
      type: "url",
      name: "service-agreement-2026.pdf",
      url: "https://sterling-docs.example.com/service-agreement-2026.pdf",
    },
  ],
  schema: {
    fields: [
      {
        name: "party_a",
        type: "TEXT",
        description: "First contracting party (the service provider)",
      },
      {
        name: "party_b",
        type: "TEXT",
        description: "Second contracting party (the client)",
      },
      {
        name: "effective_date",
        type: "DATE",
        description: "Date the contract takes effect",
      },
      {
        name: "termination_date",
        type: "DATE",
        description: "Date the contract expires or can be terminated",
      },
      {
        name: "governing_law",
        type: "TEXT",
        description: "Jurisdiction whose law governs the contract",
      },
      {
        name: "total_value",
        type: "CURRENCY_AMOUNT",
        description: "Total contract value",
        decimal_points: 2,
      },
    ],
  },
});
sterling_client = IterationLayer(api_key="il_sterl567_YOUR_KEY")

contract_result = sterling_client.extract(
    files=[
        {
            "type": "url",
            "name": "service-agreement-2026.pdf",
            "url": "https://sterling-docs.example.com/service-agreement-2026.pdf",
        }
    ],
    schema={
        "fields": [
            {
                "name": "party_a",
                "type": "TEXT",
                "description": "First contracting party (the service provider)",
            },
            {
                "name": "party_b",
                "type": "TEXT",
                "description": "Second contracting party (the client)",
            },
            {
                "name": "effective_date",
                "type": "DATE",
                "description": "Date the contract takes effect",
            },
            {
                "name": "termination_date",
                "type": "DATE",
                "description": "Date the contract expires or can be terminated",
            },
            {
                "name": "governing_law",
                "type": "TEXT",
                "description": "Jurisdiction whose law governs the contract",
            },
            {
                "name": "total_value",
                "type": "CURRENCY_AMOUNT",
                "description": "Total contract value",
                "decimal_points": 2,
            },
        ]
    },
)
sterlingClient := il.NewClient("il_sterl567_YOUR_KEY")

contractResult, err := sterlingClient.Extract(il.ExtractRequest{
    Files: []il.FileInput{
        il.NewFileFromURL(
            "service-agreement-2026.pdf",
            "https://sterling-docs.example.com/service-agreement-2026.pdf",
        ),
    },
    Schema: il.ExtractionSchema{
        "party_a": il.NewTextFieldConfig(
            "party_a", "First contracting party (the service provider)",
        ),
        "party_b": il.NewTextFieldConfig(
            "party_b", "Second contracting party (the client)",
        ),
        "effective_date": il.NewDateFieldConfig(
            "effective_date", "Date the contract takes effect",
        ),
        "termination_date": il.NewDateFieldConfig(
            "termination_date", "Date the contract expires or can be terminated",
        ),
        "governing_law": il.NewTextFieldConfig(
            "governing_law", "Jurisdiction whose law governs the contract",
        ),
        "total_value": il.NewCurrencyAmountFieldConfig(
            "total_value", "Total contract value",
        ),
    },
})

Two clients, two schemas, same API, same error handling, same SDK. The only difference is the API key and the fields.

Step 4: Chain Extraction into Generation

Client C needs more than extraction — they need the extracted property listing data turned into a PDF brochure. This is where composability matters. The extraction result feeds directly into document generation.

# Step 1: Extract property listing data
curl -X POST \
  https://api.iterationlayer.com/document-extraction/v1/extract \
  -H "Authorization: Bearer il_summt890_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "type": "url",
        "name": "listing-445.pdf",
        "url": "https://summit-docs.example.com/listing-445.pdf"
      }
    ],
    "schema": {
      "fields": [
        {
          "name": "property_address",
          "type": "ADDRESS",
          "description": "Full property address"
        },
        {
          "name": "asking_price",
          "type": "CURRENCY_AMOUNT",
          "description": "Listed asking price",
          "decimal_points": 0
        },
        {
          "name": "bedrooms",
          "type": "INTEGER",
          "description": "Number of bedrooms"
        },
        {
          "name": "square_meters",
          "type": "DECIMAL",
          "description": "Living area in square meters",
          "decimal_points": 1
        },
        {
          "name": "description",
          "type": "TEXTAREA",
          "description": "Property description text"
        }
      ]
    }
  }'

# Step 2: Generate PDF brochure from extracted data
curl -X POST \
  https://api.iterationlayer.com/document-generation/v1/generate \
  -H "Authorization: Bearer il_summt890_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "pdf",
    "document": {
      "metadata": {
        "title": "Property Listing - Marienstrasse 12, Berlin"
      },
      "content": [
        {
          "type": "headline",
          "level": "h1",
          "text": "Marienstrasse 12, Berlin"
        },
        {
          "type": "paragraph",
          "markdown": "**Asking price:** EUR 485,000 | **Bedrooms:** 3 | **Area:** 112.5 m2"
        },
        {
          "type": "separator"
        },
        {
          "type": "paragraph",
          "markdown": "Bright corner apartment in a renovated Altbau building, featuring original hardwood floors, high ceilings, and a south-facing balcony overlooking the courtyard."
        }
      ]
    }
  }'
const summitClient = new IterationLayer({
  apiKey: "il_summt890_YOUR_KEY",
});

// Step 1: Extract property listing data
const listingResult = await summitClient.extract({
  files: [
    {
      type: "url",
      name: "listing-445.pdf",
      url: "https://summit-docs.example.com/listing-445.pdf",
    },
  ],
  schema: {
    fields: [
      {
        name: "property_address",
        type: "ADDRESS",
        description: "Full property address",
      },
      {
        name: "asking_price",
        type: "CURRENCY_AMOUNT",
        description: "Listed asking price",
        decimal_points: 0,
      },
      {
        name: "bedrooms",
        type: "INTEGER",
        description: "Number of bedrooms",
      },
      {
        name: "square_meters",
        type: "DECIMAL",
        description: "Living area in square meters",
        decimal_points: 1,
      },
      {
        name: "description",
        type: "TEXTAREA",
        description: "Property description text",
      },
    ],
  },
});

// Step 2: Generate PDF brochure from extracted data
const brochure = await summitClient.generateDocument({
  format: "pdf",
  document: {
    metadata: {
      title: `Property Listing - ${listingResult.property_address.value}`,
    },
    content: [
      {
        type: "headline",
        level: "h1",
        text: String(listingResult.property_address.value),
      },
      {
        type: "paragraph",
        markdown: `**Asking price:** EUR ${listingResult.asking_price.value} | **Bedrooms:** ${listingResult.bedrooms.value} | **Area:** ${listingResult.square_meters.value} m2`,
      },
      {
        type: "separator",
      },
      {
        type: "paragraph",
        markdown: String(listingResult.description.value),
      },
    ],
  },
});
summit_client = IterationLayer(api_key="il_summt890_YOUR_KEY")

# Step 1: Extract property listing data
listing_result = summit_client.extract(
    files=[
        {
            "type": "url",
            "name": "listing-445.pdf",
            "url": "https://summit-docs.example.com/listing-445.pdf",
        }
    ],
    schema={
        "fields": [
            {
                "name": "property_address",
                "type": "ADDRESS",
                "description": "Full property address",
            },
            {
                "name": "asking_price",
                "type": "CURRENCY_AMOUNT",
                "description": "Listed asking price",
                "decimal_points": 0,
            },
            {
                "name": "bedrooms",
                "type": "INTEGER",
                "description": "Number of bedrooms",
            },
            {
                "name": "square_meters",
                "type": "DECIMAL",
                "description": "Living area in square meters",
                "decimal_points": 1,
            },
            {
                "name": "description",
                "type": "TEXTAREA",
                "description": "Property description text",
            },
        ]
    },
)

# Step 2: Generate PDF brochure from extracted data
brochure = summit_client.generate_document(
    format="pdf",
    document={
        "metadata": {
            "title": f"Property Listing - {listing_result['property_address']['value']}",
        },
        "content": [
            {
                "type": "headline",
                "level": "h1",
                "text": str(listing_result["property_address"]["value"]),
            },
            {
                "type": "paragraph",
                "markdown": f"**Asking price:** EUR {listing_result['asking_price']['value']} | **Bedrooms:** {listing_result['bedrooms']['value']} | **Area:** {listing_result['square_meters']['value']} m2",
            },
            {
                "type": "separator",
            },
            {
                "type": "paragraph",
                "markdown": str(listing_result["description"]["value"]),
            },
        ],
    },
)
summitClient := il.NewClient("il_summt890_YOUR_KEY")

// Step 1: Extract property listing data
listingResult, err := summitClient.Extract(il.ExtractRequest{
    Files: []il.FileInput{
        il.NewFileFromURL(
            "listing-445.pdf",
            "https://summit-docs.example.com/listing-445.pdf",
        ),
    },
    Schema: il.ExtractionSchema{
        "property_address": il.NewAddressFieldConfig(
            "property_address", "Full property address",
        ),
        "asking_price": il.NewCurrencyAmountFieldConfig(
            "asking_price", "Listed asking price",
        ),
        "bedrooms": il.NewIntegerFieldConfig(
            "bedrooms", "Number of bedrooms",
        ),
        "square_meters": il.NewDecimalFieldConfig(
            "square_meters", "Living area in square meters",
        ),
        "description": il.NewTextareaFieldConfig(
            "description", "Property description text",
        ),
    },
})

// Step 2: Generate PDF brochure from extracted data
address := fmt.Sprintf("%v", (*listingResult)["property_address"].Value)

brochure, err := summitClient.GenerateDocument(il.GenerateDocumentRequest{
    Format: "pdf",
    Document: il.DocumentDefinition{
        Metadata: il.DocumentMetadata{
            Title: "Property Listing - " + address,
        },
        Content: []il.ContentBlock{
            il.HeadlineBlock{
                Type:  "headline",
                Level: "h1",
                Text:  address,
            },
            il.ParagraphBlock{
                Type:     "paragraph",
                Markdown: "**Asking price:** EUR 485,000 | **Bedrooms:** 3 | **Area:** 112.5 m2",
            },
            il.SeparatorBlock{
                Type: "separator",
            },
            il.ParagraphBlock{
                Type:     "paragraph",
                Markdown: fmt.Sprintf("%v", (*listingResult)["description"].Value),
            },
        },
    },
})

Same API key, same client instance. The extraction output flows directly into the document generation input. No format conversion, no intermediate storage, no second vendor.

Step 5: Set Budget Caps

Every project in the dashboard has an optional budget cap — a maximum number of credits the project can consume per billing period. This is critical for fixed-fee client work.

Set the cap based on your expected usage plus a safety margin. If client A’s invoice pipeline typically uses 200 credits per month, set the cap at 300. If the pipeline hits the cap, requests will be rejected with a clear error rather than silently burning through your budget.

This gives you an early warning system. A cap hit means something changed — either the client’s volume increased (time to renegotiate) or something in the pipeline is misbehaving (time to debug). Either way, you find out before the bill arrives.

What You Get

Three client pipelines, three API keys, three extraction schemas — all running through one account with one credit pool and one invoice. Adding client D means creating a new project, generating a new key, and writing a new schema. Not evaluating a new vendor, not setting up a new billing relationship, not learning a new SDK.

The per-project usage tracking means you always know what each client costs. The budget caps mean you control what each client can cost. And because every API uses the same auth pattern, error format, and response structure, your team’s integration code is reusable across every client project.

Start with a free account — no credit card required. Create your first project, generate an API key, and run an extraction. The Document Extraction docs and Document Generation docs have the full schema reference.

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.