# Calling external REST APIs

Invoke any REST API from Python services - JSON parsed into Python objects, with headers, parameters and status checks.

Python services call external REST APIs through [outgoing connections](https://zato.io/docs/dev/rest/outconns.html). You create a connection once in the Dashboard and every service invokes it by name - Zato attaches the credentials, pools the network connections and parses JSON responses into Python objects.

## Call an API {#call-an-api}

The `self.rest` dictionary returns the connection whose name you pass - the name must match the Dashboard entry exactly. The service below reads a customer from an external API and returns it to its own caller:

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

# Zato
from zato.server.service import Service

class GetCustomer(Service):
    """ Reads a customer from an external API.
    """
    name = 'demo.rest.get-customer'

    def handle(self) -> 'None':

        # The connection configured in the Dashboard ..
        conn = self.rest['Customer API']

        # .. one GET call ..
        response = conn.get()

        # .. and the JSON response, already parsed into a Python object.
        self.response.payload = response.data
```

To expose this service, [create a REST channel](https://zato.io/docs/dev/rest/channels.html) pointing to `/api/customer` and call it:

```bash
curl http://localhost:11223/api/customer
```

The service calls the external Customer API and returns the customer it received.

## Send data with POST {#send-data-with-post}

Pass a Python dict as the second argument and Zato serializes it to JSON:

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

# Zato
from zato.server.service import Service

class CreateOrder(Service):
    name = 'demo.rest.create-order'

    def handle(self) -> 'None':

        # The order to create in the external system ..
        payload = {
            'customer_id': '12345',
            'items': [
                {'sku': 'ABC-001', 'quantity': 2},
                {'sku': 'XYZ-999', 'quantity': 1}
            ]
        }

        # .. the connection serializes the dict to JSON on the way out ..
        conn = self.rest['Orders API']
        response = conn.post(payload)

        # .. and the endpoint's answer goes back to our own caller.
        self.response.payload = response.data
```

If the service is exposed on `/api/orders`, you invoke it with:

```bash
curl -X POST http://localhost:11223/api/orders \
  -d '{"customer_id": "12345", "items": [{"sku": "ABC-001", "quantity": 2}]}'
```

## Path and query parameters {#path-and-query-parameters}

If the outgoing connection's URL path contains placeholders like `/customers/{customer_id}/orders`, Zato substitutes matching keys from `params` into the path. Remaining keys become query string parameters:

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

# Zato
from zato.server.service import Service

class GetCustomerOrders(Service):
    name = 'demo.rest.get-customer-orders'

    def handle(self) -> 'None':

        # customer_id fills the {customer_id} placeholder in the path,
        # status becomes ?status=pending in the query string
        params = {
            'customer_id': '12345',
            'status': 'pending'
        }

        conn = self.rest['Orders API']
        response = conn.get(params=params)

        self.response.payload = response.data
```

A path parameter always fills exactly one path segment - a slash inside a value is sent encoded as `%2F`, so values like `abc/def` or `../admin` never change which resource the path names. A path with placeholders also means `params` is required - invoking such a connection without the placeholder's value raises an error.

## Custom headers {#custom-headers}

Pass a `headers` dict to include headers in the outgoing request:

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

# Zato
from zato.server.service import Service

class CallWithHeaders(Service):
    name = 'demo.rest.call-with-headers'

    def handle(self) -> 'None':

        headers = {
            'X-Request-ID': self.cid,
            'X-Client-Version': '2.0',
            'Accept-Language': 'en-US'
        }

        conn = self.rest['Customer API']
        response = conn.get(headers=headers)

        self.response.payload = response.data
```

Sending `self.cid` as a request ID gives you distributed tracing - the same correlation ID appears in your logs and in the external system's logs.

## Correlation IDs {#correlation-ids}

Every call is made under the calling service's correlation ID, `self.cid`, without you passing it. It is what the connection's audit log and the server's own log file record for the request, so a call made from a service is traceable back to the request that service was handling.

To make a call under a different ID, such as one taken from an incoming request's header, pass it as `cid`:

```python
response = conn.get(params=params, cid=request_id)
```

Older code that passes `self.cid` as the first positional argument, `conn.get(self.cid, params=params)`, continues to work unchanged.

## The response object {#the-response-object}

Every call returns the same response object, whatever the HTTP method:

| Attribute | What it holds |
| --- | --- |
| `response.status_code` | The HTTP status code the endpoint answered with, e.g. 200 or 404 |
| `response.ok` | True for any 2xx status |
| `response.data` | The body parsed into Python objects when it is JSON, or into a [data model](https://zato.io/docs/dev/rest/calling-apis.html#deserialize-into-a-data-model) if you asked for one |
| `response.text` | The body as text, for XML, CSV, HTML and anything else that is not JSON |
| `response.content` | The body as bytes, for binary content |
| `response.headers` | The response headers as a dictionary, case-insensitive - `response.headers['Content-Type']` and `response.headers['content-type']` are the same header |

## Check the response status {#check-the-response-status}

The response includes the endpoint's HTTP status code - check it before using the data:

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

# stdlib
from http import HTTPStatus

# Zato
from zato.server.service import Service

class CheckResponseStatus(Service):
    name = 'demo.rest.check-status'

    def handle(self) -> 'None':

        conn = self.rest['Customer API']
        response = conn.get()

        # The status code determines what our own caller receives
        if response.status_code == HTTPStatus.OK:
            self.response.payload = response.data

        elif response.status_code == HTTPStatus.NOT_FOUND:
            self.response.payload = {'error': 'Not found'}
            self.response.status_code = HTTPStatus.NOT_FOUND

        else:
            raise Exception(f'API error: {response.status_code}')
```

When a service raises an exception, the caller receives HTTP 500. To return a different status, set `self.response.status_code` explicitly, as the 404 case above does.

For a plain success check, `response.ok` is True for any 2xx status:

```python
response = conn.get()

if response.ok:
    self.response.payload = response.data
```

To learn how to handle failures in both directions, see [error handling](https://zato.io/docs/dev/rest/error-handling.html).

## XML and other non-JSON responses {#xml-and-other-non-json-responses}

`response.data` holds the parsed object when the response is JSON. For everything else, the raw body is always available:

```python
response = conn.get()

# The body as text - use this for XML, CSV, HTML and other text formats
body = response.text

# The body as bytes - use this for binary content
raw = response.content
```

What is parsed is decided by the connection's data format setting together with the response's own `Content-Type` header:

- With the data format set to JSON, every response is parsed as JSON - and a response that is not valid JSON, e.g. an XML error page, raises an exception rather than degrading to text
- With the data format left blank, `response.data` is the response's text, unless the endpoint itself declares `application/json`, in which case it is parsed anyway

For an endpoint that serves XML, leave the connection's data format blank and read `response.text`.

## Deserialize into a data model {#deserialize-into-a-data-model}

When your service declares [data models](https://zato.io/docs/dev/model/index.html), a response can be deserialized into a model instance by passing the model class in the call:

```python
response = conn.get(model=CustomerDetails)

# response.data is now a CustomerDetails instance
customer = response.data
self.logger.info(customer.name)
```

## Paginated responses {#paginated-responses}

An API that returns a long list in pages tells you how to get the next one - most often as a cursor or a page number in the body, which goes into `params` of the next call. The loop below collects every page of orders:

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

# Zato
from zato.server.service import Service

class GetAllOrders(Service):
    name = 'demo.rest.get-all-orders'

    def handle(self) -> 'None':

        conn = self.rest['Orders API']
        orders = []
        params = {'page': 1}

        while True:
            response = conn.get(params=params)
            orders.extend(response.data['items'])

            # The endpoint says which page comes next, and no page means this was the last one
            next_page = response.data['next_page']
            if not next_page:
                break

            params = {'page': next_page}

        self.response.payload = orders
```

Some APIs give the next page as a whole URL in a `Link` header instead, as `<https://api.example.com/orders?page=2>; rel="next"`. You take the query string from that URL - here, `page=2` - and pass it in `params` as in the loop above.

## All HTTP methods {#all-http-methods}

The connection object supports these HTTP methods:

```python
conn.get(params)     # GET
conn.post(payload)   # POST
conn.put(payload)    # PUT
conn.patch(payload)  # PATCH
conn.delete(params)  # DELETE
conn.options()       # OPTIONS
```

There is also `conn.ping()` for connectivity checks, and `self.out.rest['Customer API']` is another name for the same connection that `self.rest['Customer API']` returns - both forms work with every method on this page.

An inactive connection - one whose Active checkbox is unticked in the Dashboard, or whose `is_active` is false in enmasse - raises an `Inactive` exception when invoked, which is how traffic to an endpoint is switched off without deleting its configuration.

## A full example {#a-full-example}

The service below combines a payload, path and query parameters and custom headers in one call:

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

# Zato
from zato.server.service import Service

class SetBillingInfo(Service):
    """ Updates billing information in an external system.
    """
    name = 'demo.rest.set-billing-info'

    def handle(self) -> 'None':

        # The request body, serialized to JSON ..
        payload = {'billing':'395.7', 'currency':'EUR'}

        # .. cust_id fills the path placeholder, the rest becomes the query string ..
        params = {'cust_id':'39175', 'phone_no':'271637517', 'priority':'normal'}

        # .. headers the endpoint expects ..
        headers = {'X-App-Name': 'Zato', 'X-Environment':'Production'}

        # .. and one POST call sends all of it.
        conn = self.rest['Set Billing Info']
        response = conn.post(payload, params, headers=headers)
```

The response object gives you [access](https://zato.io/docs/dev/request-response/index.html) to the body, headers and other metadata that the endpoint returns. For declarative API calls with response mapping and no boilerplate, see the [REST adapter](https://zato.io/docs/dev/rest/rest-adapter.html).

To configure timeouts for the connections these services invoke, see [outgoing connections](https://zato.io/docs/dev/rest/outconns.html), and for retries and guaranteed delivery, [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html).

## See also {#see-also}

- [Outgoing connections](https://zato.io/docs/dev/rest/outconns.html) - Pools, timeouts and security for external APIs
- [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html) - Retries, queues and dead-letter queues for requests that could not be delivered
- [REST adapter](https://zato.io/docs/dev/rest/rest-adapter.html) - Declarative API calls with response mapping and no boilerplate
- [Error handling](https://zato.io/docs/dev/rest/error-handling.html) - Handling the errors external APIs return to you
- [Orchestration](https://zato.io/docs/dev/rest/orchestration.html) - Calling multiple APIs in one service and combining their responses

## 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
