# Dynamic Endpoints ⚡

Dynamic endpoints allow you to programmatically generate multiple endpoint configurations based on runtime data. This is powerful for APIs where the list of available endpoints or resources isn't known until you query the API itself, or when you need to create many similar endpoints without repeating configuration.

Learn more: [Dynamic Endpoints](https://docs.slingdata.io/concepts/api-specs/dynamic-endpoints) " [State Variables](https://docs.slingdata.io/concepts/api-specs/structure#state-variables) " [Processors](https://docs.slingdata.io/concepts/api-specs/advanced#data-processors)

## How Dynamic Endpoints Work

Dynamic endpoints use the `dynamic_endpoints` key to generate multiple endpoint configurations:

1. **Setup Phase** (optional): Execute API calls to fetch data needed for iteration
2. **Iteration Phase**: Loop over a list (from setup or predefined)
3. **Generation Phase**: Create one endpoint configuration per iteration item
4. **Execution Phase**: Run the generated endpoints like normal static endpoints

The generated endpoints are combined with any static endpoints defined in the `endpoints` section.

## Simple Iteration Over Static List

The simplest use case - iterate over a predefined list to create multiple similar endpoints.

**Spec File** (`resources_api.yaml`)

{% code title="resources\_api.yaml" overflow="wrap" %}

```yaml
name: "Resources API"

defaults:
  state:
    base_url: https://api.example.com/v1
  request:
    headers:
      Authorization: "Bearer {secrets.api_key}"

dynamic_endpoints:
  # Iterate over a static list of resource types
  - iterate: '["users", "orders", "products"]'
    into: "state.resource_type"

    endpoint:
      # Generate endpoint name from resource type
      name: "{state.resource_type}"
      description: "Fetch {state.resource_type} data"

      request:
        url: "{state.base_url}/{state.resource_type}"
        parameters:
          limit: 100
          offset: 0

      pagination:
        next_state:
          offset: "{state.offset + 100}"
        stop_condition: length(response.records) < 100

      response:
        records:
          jmespath: "data[]"
          primary_key: ["id"]

        processors:
          # Add resource type to each record for tracking
          - expression: "state.resource_type"
            output: "record._resource_type"
```

{% endcode %}

***

**Using Replication**

Running with Sling: `sling run -r /path/to/replication.yaml`

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

streams:
  # The dynamic endpoints generate: users, orders, products
  '*':
    object: public.{stream_name}
```

{% endcode %}

This creates three endpoints from a single configuration, avoiding repetition.

***

**Using Python**

{% code title="api\_to\_database.py" overflow="wrap" %}

```python
from sling import Replication, ReplicationStream

replication = Replication(
    source='MY_API',
    target='MY_TARGET_DB',
    streams={
        'users': ReplicationStream(object='public.users'),
        'orders': ReplicationStream(object='public.orders'),
        'products': ReplicationStream(object='public.products')
    }
)

replication.run()
```

{% endcode %}

## Dynamic Discovery from API

Fetch the list of available resources from the API itself, then create endpoints dynamically.

**Spec File** (`database_api.yaml`)

{% code title="database\_api.yaml" overflow="wrap" %}

```yaml
name: "Database API"

defaults:
  state:
    base_url: https://api.database.com/v2
  request:
    headers:
      X-API-Key: "{secrets.api_key}"

dynamic_endpoints:
  - setup:
      # Fetch list of available tables from the API
      - request:
          url: "{state.base_url}/metadata/tables"
        response:
          processors:
            # Extract table names from response
            - expression: 'jmespath(response.json, "tables[].name")'
              output: "state.available_tables"
              aggregation: last

    # Create one endpoint per table discovered
    iterate: "state.available_tables"
    into: "state.table_name"

    endpoint:
      name: "table_{state.table_name}"
      description: "Data from {state.table_name} table"

      request:
        url: "{state.base_url}/tables/{state.table_name}/rows"
        parameters:
          limit: 1000
          offset: 0

      pagination:
        next_state:
          offset: "{state.offset + 1000}"
        stop_condition: length(response.records) < 1000

      response:
        records:
          jmespath: "rows[]"
          primary_key: ["id"]

        processors:
          # Tag each record with source table name
          - expression: "state.table_name"
            output: "record._source_table"
```

{% endcode %}

***

**Using Replication**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# If API returns tables: ["customers", "invoices", "payments"]
# Then these endpoints are generated: table_customers, table_invoices, table_payments

streams:
  table_customers:
    object: staging.customers

  table_invoices:
    object: staging.invoices

  table_payments:
    object: staging.payments
```

{% endcode %}

***

**Using Python**

{% code title="api\_to\_database.py" overflow="wrap" %}

```python
from sling import Replication, ReplicationStream

# The API will discover available tables automatically
replication = Replication(
    source='MY_API',
    target='MY_TARGET_DB',
    streams={
        'table_customers': ReplicationStream(object='staging.customers'),
        'table_invoices': ReplicationStream(object='staging.invoices'),
        'table_payments': ReplicationStream(object='staging.payments')
    }
)

replication.run()
```

{% endcode %}

## Multi-Organization Endpoints

Create separate endpoints for each organization the authenticated user has access to.

**Spec File** (`multi_org_api.yaml`)

{% code title="multi\_org\_api.yaml" overflow="wrap" %}

```yaml
name: "Multi-Organization API"

defaults:
  state:
    base_url: https://api.saas.com/v1
  request:
    headers:
      Authorization: "Bearer {secrets.access_token}"

dynamic_endpoints:
  - setup:
      # Fetch organizations the user can access
      - request:
          url: "{state.base_url}/user/organizations"
        response:
          processors:
            # Store the list of organizations
            - expression: 'jmespath(response.json, "organizations")'
              output: "state.org_list"
              aggregation: last

    # Create one endpoint per organization
    iterate: "state.org_list"
    into: "state.org"

    endpoint:
      # Use organization ID in endpoint name
      name: "org_{state.org.id}_events"
      description: "Events for {state.org.name}"

      request:
        url: "{state.base_url}/organizations/{state.org.id}/events"
        parameters:
          limit: 500
          cursor: "{state.cursor}"

      pagination:
        next_state:
          cursor: '{jmespath(response.json, "pagination.next_cursor")}'
        stop_condition: 'is_null(jmespath(response.json, "pagination.next_cursor"))'

      response:
        records:
          jmespath: "events[]"
          primary_key: ["event_id"]

        processors:
          # Add organization context to each record
          - expression: "state.org.id"
            output: "record.organization_id"
          - expression: "state.org.name"
            output: "record.organization_name"
```

{% endcode %}

***

**Using Replication**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# If user has access to orgs 123 and 456, endpoints generated:
# org_123_events, org_456_events

streams:
  '*':
    object: public.{stream_name}
```

{% endcode %}

## Geographic Regions with Metadata

Create endpoints for predefined geographic regions with additional metadata.

**Spec File** (`regional_api.yaml`)

{% code title="regional\_api.yaml" overflow="wrap" %}

```yaml
name: "Regional Sales API"

defaults:
  state:
    base_url: https://api.global-sales.com

    # Define regions with metadata
    regions:
      - code: "us-east"
        name: "US East Coast"
        timezone: "America/New_York"
      - code: "us-west"
        name: "US West Coast"
        timezone: "America/Los_Angeles"
      - code: "eu-west"
        name: "Europe West"
        timezone: "Europe/London"
      - code: "apac"
        name: "Asia Pacific"
        timezone: "Asia/Tokyo"

  request:
    headers:
      Authorization: "Bearer {secrets.api_key}"

dynamic_endpoints:
  # No setup needed - iterate over predefined regions
  - iterate: "state.regions"
    into: "state.region"

    endpoint:
      name: "sales_{state.region.code}"
      description: "Sales data for {state.region.name}"

      state:
        # Default to last 7 days
        start_date: '{date_format(date_add(now(), -7, "day"), "%Y-%m-%d")}'
        end_date: '{date_format(now(), "%Y-%m-%d")}'

      request:
        url: "{state.base_url}/regions/{state.region.code}/sales"
        parameters:
          date_from: "{state.start_date}"
          date_to: "{state.end_date}"
          page: "{state.page}"
          page_size: 250

      pagination:
        next_state:
          page: "{state.page + 1}"
        stop_condition: 'jmespath(response.json, "has_more") == false'

      response:
        records:
          jmespath: "sales[]"
          primary_key: ["sale_id"]

        processors:
          # Add region metadata to each record
          - expression: "state.region.code"
            output: "record.region_code"
          - expression: "state.region.name"
            output: "record.region_name"
          - expression: "state.region.timezone"
            output: "record.region_timezone"
```

{% endcode %}

***

**Using Replication**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# Dynamic endpoints generated: sales_us-east, sales_us-west, sales_eu-west, sales_apac

streams:
  '*':
    object: public.{stream_name}
```

{% endcode %}

## Combining Static and Dynamic Endpoints

Mix static endpoints (for unique resources) with dynamic endpoints (for similar resources).

**Spec File** (`hybrid_api.yaml`)

{% code title="hybrid\_api.yaml" overflow="wrap" %}

```yaml
name: "Hybrid API"

defaults:
  state:
    base_url: https://api.example.com/v1
  request:
    headers:
      Authorization: "Bearer {secrets.api_key}"

# Static endpoints for unique resources
endpoints:
  metadata:
    description: "API metadata and configuration"
    request:
      url: "{state.base_url}/metadata"
    response:
      records:
        jmespath: "."
        primary_key: ["api_version"]

  user_profile:
    description: "Authenticated user profile"
    request:
      url: "{state.base_url}/user/profile"
    response:
      records:
        jmespath: "."
        primary_key: ["user_id"]

# Dynamic endpoints for similar resources
dynamic_endpoints:
  - iterate: '["transactions", "accounts", "categories"]'
    into: "state.entity"

    endpoint:
      name: "{state.entity}"
      description: "Fetch {state.entity} data"

      request:
        url: "{state.base_url}/{state.entity}"
        parameters:
          limit: 100

      pagination:
        next_state:
          offset: "{state.offset + 100}"
        stop_condition: length(response.records) < 100

      response:
        records:
          jmespath: "data[]"
          primary_key: ["id"]
```

{% endcode %}

***

**Using Replication**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# Total 5 endpoints: 2 static + 3 dynamic

streams:
  # Static endpoints
  metadata:
    object: config.api_metadata

  user_profile:
    object: config.user_profile

  # Dynamic endpoints
  '*':
    object: public.{stream_name}
```

{% endcode %}

## Filtering During Setup

Only generate endpoints for resources matching specific criteria.

**Spec File** (`filtered_api.yaml`)

{% code title="filtered\_api.yaml" overflow="wrap" %}

```yaml
name: "Filtered Tables API"

defaults:
  state:
    base_url: https://api.example.com
  request:
    headers:
      X-API-Key: "{secrets.api_key}"

dynamic_endpoints:
  - setup:
      - request:
          url: "{state.base_url}/schemas/public/tables"
        response:
          processors:
            # Get all tables
            - expression: 'jmespath(response.json, "tables")'
              output: "state.all_tables"
              aggregation: last

            # Filter to only production tables (starting with "prod_")
            - expression: >
                filter(
                  state.all_tables,
                  "starts_with(name, 'prod_')"
                )
              output: "state.filtered_tables"

    # Only create endpoints for filtered tables
    iterate: "state.filtered_tables"
    into: "state.table"

    endpoint:
      name: "{state.table.name}"
      description: "Production table: {state.table.name}"

      request:
        url: "{state.base_url}/tables/{state.table.name}/data"
        parameters:
          limit: 1000

      pagination:
        next_state:
          offset: "{state.offset + 1000}"
        stop_condition: length(response.records) < 1000

      response:
        records:
          jmespath: "rows[]"
          primary_key: ["id"]
```

{% endcode %}

***

**Using Replication**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# Only tables matching "prod_*" pattern are available

streams:
  prod_customers:
    object: public.customers

  prod_orders:
    object: public.orders

  # dev_* and test_* tables are excluded
```

{% endcode %}

## Pattern Matching Dynamic Endpoints

Use wildcards to run multiple dynamic endpoints at once.

**Using Replication with Wildcards**

{% code title="replication.yaml" overflow="wrap" %}

```yaml
source: MY_API
target: MY_TARGET_DB

# Run all organization endpoints for a specific org
streams:
  org_123_*:
    object: analytics.{stream}
    # This matches: org_123_events, org_123_users, etc.

  # Run all regional sales endpoints
  sales_*:
    object: sales.{stream}
    # This matches: sales_us-east, sales_us-west, etc.
```

{% endcode %}

***

**Using Python with Pattern Matching**

{% code title="api\_to\_database.py" overflow="wrap" %}

```python
from sling import Replication, ReplicationStream

# You need to know the actual endpoint names or fetch them
replication = Replication(
    source='MY_API',
    target='MY_TARGET_DB',
    streams={
        # Run all org_123 endpoints
        'org_123_events': ReplicationStream(object='analytics.org_123_events'),
        'org_123_users': ReplicationStream(object='analytics.org_123_users'),
        'org_123_metrics': ReplicationStream(object='analytics.org_123_metrics'),
    }
)

replication.run()
```

{% endcode %}

## Best Practices

### 1. Use Meaningful Endpoint Names

Generate descriptive names that clearly identify what each endpoint does:

```yaml
# Good: Clear and descriptive
name: "region_{state.region.code}_daily_sales"

# Avoid: Generic or unclear
name: "endpoint_{state.index}"
```

### 2. Filter in Setup Phase

Only generate endpoints you'll actually use:

```yaml
setup:
  - request:
      url: "{state.base_url}/resources"
    response:
      processors:
        # Filter to only active resources
        - expression: >
            filter(
              jmespath(response.json, "resources"),
              "status == 'active'"
            )
          output: "state.active_resources"
```

### 3. Add Context to Records

Use processors to tag records with metadata from the iteration:

```yaml
processors:
  - expression: "state.org.id"
    output: "record.organization_id"
  - expression: "state.region.code"
    output: "record.region_code"
```

### 4. Validate Iteration Data

Ensure the data you're iterating over is valid:

```yaml
setup:
  - request:
      url: "{state.base_url}/orgs"
    response:
      processors:
        - expression: 'jmespath(response.json, "organizations")'
          output: "state.orgs"
          aggregation: last

        # Log for debugging
        - expression: log("Found " + string(length(state.orgs)) + " organizations")
          output: ""
```

### 5. Use Static Endpoints for Unique Resources

Don't force everything into dynamic endpoints. Use static endpoints for one-off resources:

```yaml
# Static for unique resources
endpoints:
  metadata:
    request:
      url: "{state.base_url}/metadata"

# Dynamic for similar resources
dynamic_endpoints:
  - iterate: "state.tables"
    into: "state.table"
    endpoint:
      name: "{state.table.name}"
```

### 6. Consider Performance

Be mindful of how many endpoints you generate:

```yaml
# Be cautious with large lists
# Generating 1000+ endpoints can impact performance

setup:
  - request:
      url: "{state.base_url}/all_resources"
    response:
      processors:
        # Consider pagination or filtering large lists
        - expression: 'jmespath(response.json, "resources[0:100]")'
          output: "state.limited_resources"
```

## Combining Dynamic Endpoints with Other Features

### With Incremental Sync

```yaml
dynamic_endpoints:
  - iterate: '["users", "orders", "products"]'
    into: "state.resource"

    endpoint:
      name: "{state.resource}"

      # Enable incremental sync for each endpoint
      sync: [last_updated_at]

      state:
        updated_since: >
          {coalesce(
            sync.last_updated_at,
            date_format(date_add(now(), -30, "day"), "%Y-%m-%dT%H:%M:%S%z")
          )}

      request:
        url: "{state.base_url}/{state.resource}"
        parameters:
          updated_since: "{state.updated_since}"

      response:
        records:
          jmespath: "data[]"
          primary_key: ["id"]

        processors:
          - expression: "record.updated_at"
            output: "state.last_updated_at"
            aggregation: "maximum"
```

### With Backfill

```yaml
dynamic_endpoints:
  - iterate: "state.regions"
    into: "state.region"

    endpoint:
      name: "region_{state.region.code}_events"

      sync: [last_date]

      iterate:
        # Support backfill with context.range_start/end
        over: >
          range(
            coalesce(context.range_start, sync.last_date, "2024-01-01"),
            coalesce(context.range_end, date_format(now(), "%Y-%m-%d")),
            "1d"
          )
        into: "state.current_date"

      request:
        url: "{state.base_url}/regions/{state.region.code}/events"
        parameters:
          date: "{state.current_date}"
```
