# Orchestrating multiple APIs

Coordinate multiple APIs in one service - sequential calls, aggregation, fan-out and partial failures.

An orchestration service coordinates multiple API calls in one business task - it fetches data from an HR system, checks an access control database, updates a ticket tracker and combines everything into a single response.

Orchestration happens through `self.invoke` - each call runs another service, which may call an external API through an [adapter](https://zato.io/docs/dev/rest/rest-adapter.html) or do local processing. The orchestration logic stays in one service and the API-specific code stays in the adapters.

## Call APIs sequentially {#call-apis-sequentially}

When the second call needs data from the first, run them in order. The service below reads an employee from HR, then uses the employee's email to look up their access card:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetEmployeeWithAccessCard(Service):
    """ Combines HR data with access control data for one employee.
    """
    name = 'demo.rest.get-employee-with-access-card'

    input = 'employee_id'

    def handle(self) -> 'None':

        employee_id = self.request.input.employee_id

        # The first call returns the employee ..
        employee = self.invoke('hr.employee.get', employee_id=employee_id)

        # .. whose email the second call needs.
        access_card = self.invoke('access.card.get', email=employee.email)

        has_access_card = access_card is not None

        self.response.payload = {
            'employee': employee,
            'has_access_card': has_access_card,
            'access_card': access_card
        }
```

[Expose the service on a channel](https://zato.io/docs/dev/rest/channels.html) at `/api/employee/{employee_id}` and call it:

```bash
curl http://localhost:11223/api/employee/EMP-001
```

The response combines data from two backend systems into one JSON object.

## Aggregate data from multiple sources {#aggregate-data-from-multiple-sources}

When the calls are independent, one service gathers them all and the client makes one request instead of four:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCustomerDashboard(Service):
    """ Builds a customer dashboard from four backend systems.
    """
    name = 'demo.rest.get-customer-dashboard'

    input = 'customer_id'

    def handle(self) -> 'None':

        customer_id = self.request.input.customer_id

        # Each call runs synchronously and returns its system's data
        profile = self.invoke('crm.customer.get', customer_id=customer_id)
        orders = self.invoke('orders.list', customer_id=customer_id)
        tickets = self.invoke('support.tickets.list', customer_id=customer_id)
        billing = self.invoke('billing.get', customer_id=customer_id)

        open_tickets = [item for item in tickets if item.status == 'open']
        open_ticket_count = len(open_tickets)

        customer = {
            'id': customer_id,
            'name': profile.name,
            'email': profile.email,
            'since': profile.created_at
        }

        summary = {
            'total_orders': len(orders),
            'open_tickets': open_ticket_count,
            'account_balance': billing.amount
        }

        self.response.payload = {
            'customer': customer,
            'summary': summary,
            'recent_orders': orders[:5],
            'recent_tickets': tickets[:3]
        }
```

[Expose the service on a channel](https://zato.io/docs/dev/rest/channels.html) at `/api/customer/{customer_id}/dashboard`:

```bash
curl http://localhost:11223/api/customer/CUST-123/dashboard
```

## Conditional orchestration {#conditional-orchestration}

Business rules decide which downstream systems each request reaches:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# The Jira transition that marks a ticket as done
_jira_done_transition = 31

class ProcessTrainingCompletion(Service):
    """ Records a completed training in the systems the course names.
    """
    name = 'demo.rest.process-training-completion'

    input = 'course_id', 'user_id'

    def handle(self) -> 'None':

        course_id = self.request.input.course_id
        user_id = self.request.input.user_id

        course = self.invoke('lms.course.get', id=course_id)
        completion = self.invoke('lms.completion.get', course_id=course_id, user_id=user_id)

        # The course itself says which systems to notify
        notifications = course.notify_on_completion

        if 'HR' in notifications:
            self.invoke('hr.training.record', employee_id=completion.employee_id, course=course)
            self.logger.info('Recorded training in HR for %s', user_id)

        if 'Jira' in notifications:
            ticket_id = completion.access_request_ticket
            if ticket_id:
                self.invoke('jira.transition', ticket_id=ticket_id, transition_id=_jira_done_transition)
                self.logger.info('Transitioned Jira ticket %s', ticket_id)

        self.response.payload = {'status': 'processed'}
```

## Fan-out pattern {#fan-out-pattern}

When several systems receive the same data, fan the calls out from one source:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class SyncAllSystems(Service):
    """ Sends the active employee list to every downstream system.
    """
    name = 'demo.rest.sync-all-systems'

    def handle(self) -> 'None':

        # One source of truth ..
        employees = self.invoke('hr.employees.active')

        # .. and the same data goes to each target system.
        lms_result = self.invoke('lms.sync', employees=employees)
        access_result = self.invoke('access.sync', employees=employees)
        directory_result = self.invoke('directory.sync', employees=employees)

        self.response.payload = {
            'employees_processed': len(employees),
            'lms_synced': lms_result.count,
            'access_synced': access_result.count,
            'directory_synced': directory_result.count
        }
```

## Handle partial failures {#handle-partial-failures}

Decide per source whether a failure ends the request or only removes one part of the response - core data is required, enrichments are optional:

```python
# -*- coding: utf-8 -*-

# stdlib
from traceback import format_exc

# Zato
from zato.server.service import Service

class GetEmployeeFullProfile(Service):
    """ Returns an employee's profile with optional enrichments.
    """
    name = 'demo.rest.get-employee-full-profile'

    input = 'employee_id'

    def handle(self) -> 'None':

        employee_id = self.request.input.employee_id

        # The profile is required - a failure here ends the request
        profile = self.invoke('hr.employee.get', employee_id=employee_id)

        result = {
            'employee_id': employee_id,
            'profile': profile
        }

        # Training data is an enrichment - the response works without it
        try:
            response = self.invoke('lms.courses', email=profile.email)
            result['training'] = response
        except Exception:
            self.logger.warning('Training data unavailable, e:`%s`', format_exc())
            result['training'] = None

        # Access history is an enrichment too
        try:
            response = self.invoke('access.history', employee_id=employee_id)
            result['access_history'] = response
        except Exception:
            self.logger.warning('Access history unavailable, e:`%s`', format_exc())
            result['access_history'] = None

        self.response.payload = result
```

To learn how failures map to the status codes your own callers receive, see [error handling](https://zato.io/docs/dev/rest/error-handling.html).

## See also {#see-also}

- [REST adapter](https://zato.io/docs/dev/rest/rest-adapter.html) - The adapter services orchestrations invoke
- [Error handling](https://zato.io/docs/dev/rest/error-handling.html) - Mapping failures to the right status codes for callers
- [Data mapping](https://zato.io/docs/dev/rest/data-mapping.html) - Translating each system's format before combining them
- [Calling REST APIs](https://zato.io/docs/dev/rest/calling-apis.html) - Invoking external APIs directly from services

## Learn more {#learn-more}

- [REST integration overview](https://zato.io/integrations/rest/) - Start here to see how teams build REST integrations
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - Master architectural patterns for production-ready integrations
- [Modern REST API tutorial](https://zato.io/tutorials/rest-api/python.html) - Design, build and secure REST APIs with Python
