Schedule a demo

Python API Integrations In-Depth Tutorial

In this tutorial you will build a working API integration service in Python that orchestrates two remote systems, secure it with an API key, expose it as a REST endpoint, schedule it for background execution, and more. It takes about 1-2 hours to complete.

Here's what you'll have at the end:

  • A complete, working environment that you can use for development, testing, and production.
  • A reusable integration service that orchestrates and integrates remote APIs.
  • A REST API channel for external API clients to invoke your service.
  • A job scheduled in the platform's scheduler to invoke the service periodically.

Prerequisite: Docker installed (Desktop or command line).

In this tutorial

  1. Installing Zato
  2. Invoking API services
  3. Connecting to REST endpoints
  4. Your first API service
  5. API security
  6. Creating and invoking REST channels
  7. Python task scheduler
  8. Beyond REST - what else can you build
  9. Unit testing
  10. DevOps and CI/CD automation
  11. Connect your AI copilot
  12. More capabilities

Remember: you can connect your AI copilot to Zato documentation for real-time, accurate answers throughout this tutorial.

Installing Zato

The recommended way to install Zato is via Docker. Both Desktop and Docker command line can be used.

Our quick-start image will auto-create an entire environment and various pieces of configuration for you, all set up and ready to work, and all in under 5 minutes.

You use the same Docker image for development, testing and production, so it's a great time saver that helps you to focus on things that are immediately useful, like the actual API integrations.

So, go to the Docker installation page, select Docker Desktop if you're under Windows or Mac, or select Docker command line if you're under Linux, fill out the details and we can resume the tutorial once you're back.

Zato architecture

  • There's one or more servers running in an environment. Servers are where your services are deployed to.
  • The browser-based Dashboard is where you configure your services and integrations.
  • The built-in scheduler is where background jobs run. You create the jobs in the Dashboard, the scheduler keeps track of which services to invoke when, and then it invokes them according to their schedule. We'll see how it works later in the tutorial.

Zato offers connectors to all the popular technologies and vendors, such as REST, GraphQL, task scheduling, Kafka, Azure, Microsoft 365, AWS, Google Cloud, Salesforce, Atlassian, SAP, Odoo, SQL, HL7, FHIR, AMQP, IBM MQ, LDAP, Redis, MongoDB, SOAP, Caching and many more.

Running in the cloud, on premises, or under Docker, Kubernetes and other container technologies, Zato services are optimized for high performance and security - it's easily possible to run hundreds and thousands of services on typical server instances as offered by AWS, Azure, Google Cloud or other cloud providers.

Built-in security options include API keys, Basic Auth, JWT, NTLM, OAuth and SSL/TLS. It's always possible to secure services using other, non-built in, means.

In terms of its implementation, an individual Zato service is a Python class implementing a specific method called self.handle. The service receives input, processes it according to its business requirements, which may involve communicating with other systems, applications or services, and then some output is produced. Note that both input and output are optional, e.g. a background service transferring files between applications will usually have neither whereas a typical CRUD service will have both.

Because a service is merely a Python class, it means that each one consumes very little resources and it's possible to deploy hundreds or thousands of services on a single Zato server.

Services accept their input through channels - a channel tells Zato that it should make a particular service available to the outside world using such and such protocol, data format and security definition. For instance, a service can be mounted on independent REST channels, sometimes using API keys and sometimes using Basic Auth. Additionally, each channel type has its own specific pieces of configuration, such as caching, timeouts or other options.

Services can invoke other Zato services too - this is just a regular Python method call, within the same Python process. It means that it's very efficient to invoke them - it's simply like invoking another Python function.

Services are hot-deployed to Zato servers without server restarts and a service may be made available to its consumers immediately after deployment.

During development, the built-in Dashboard is usually used to create and manage channels or other Zato objects. As soon as a solution is ready for DevOps automation and CI/CD pipelines, its can be deployed automatically from the command line or directly from a git clone, which makes it easy to use Zato with tools such as Terraform, Nomad or Ansible.

Useful shortcuts

Here's a few useful details to keep in mind.

  • http://localhost:8183 - This is where your Dashboard is. The default username is "admin" and the password is what you set it to when the environment was starting.
  • https://localhost:8183 - Same as 8183, but using SSL.
  • http://localhost:11223 - TCP port 11223 is the default one that servers use, that's where external REST clients connect to by default. There are no default credentials and we'll create some later on.
  • https://localhost:11224 - Same as 11223, but using SSL.
  • http://localhost:11223/zato/ping - a built-in endpoint that will reply with a pong message. You can use it for checking if your firewalls allow connections to Zato servers.

Python cloud IDE for API integrations

Before we start, it makes sense to note that you don't need anything besides Zato to go through the tutorial. In particular, Zato ships with its own, browser-based Python IDE that will be used here.

And by the way, you can certainly use VS Code with Zato too, but it's not really needed for the tutorial, so we'll be using the built-in, cloud IDE.

Invoking API services

Now that you have Zato installed, let's invoke a service and see some action.

  • Go to http://localhost:8183
  • In the top menu, select Services -> IDE, and you'll see the demo service ready to be invoked

  • The screen is divided into several parts:

    • On the left-hand side, you have your code editor that you use for Python programming
    • Parameters that your services expect can be entered in the upper area on the right-hand side
    • All the responses from services, be it in JSON or any other format, are returned in the lower area of the right-hand side
  • Enter "name=Mike" in the parameters field and click Invoke. This will invoke the service on the server and return a response to you.

Try it: Play around with it for a while. Change "Mike" to your own name. Remove the parameter entirely. Can you see in the Python code why "Howdy partner!" was returned to you when no name is given?
  • Speaking of input parameters, it's convenient to use "key=value" to invoke your services from Dashboard but you can also use JSON on input, for instance, {"name":"Mike"} means the same as name=Mike but it's almost always more convenient to enter key=value parameters so that's what the tutorial uses.

  • Clicking "History" will show you a list of all the recent requests and their last responses.

  • You can also browse your server logs directly from the Dashboard. Press F12 to bring up your browser's developer console, and set its logging to "Info" - this is required because otherwise the browser will be showing you many completely unrelated messages, so just set it to "Info" and you'll be getting your server logs straight in the browser.

Connecting to REST endpoints

Design note: In Zato, connections are independent of your code. You don't embed addresses or credentials in Python - you refer to connections by name and the platform handles the rest: connection pooling, OAuth token refresh, failover. This separation means the same service works across dev, test and production without code changes.
  • In Dashboard, go to Connections -> Outgoing -> REST

There are two ways to create outgoing connections:

  • Create individually - Click "Create a new REST outgoing connection" to manually configure each connection
  • Import from OpenAPI - Click "Import OpenAPI" to bulk-create connections from an OpenAPI/Swagger specification

For this tutorial, we'll create connections manually. Click "Create a new REST outgoing connection" and a form will appear. We need to create two connections, to CRM and Billing, so fill it out twice, clicking "OK" each time to save the changes.

Here are the connection details to provide in the form.

HeaderValue
NameCRM
Data formatJSON
Hosthttps://zato.io
URL path/tutorial/api/get-user
SecurityNo security
HeaderValue
NameBilling
Data formatJSON
Hosthttps://zato.io
URL path/tutorial/api/balance/get
SecurityNo security

For instance, here's how the form filled out with the first connection's details looks like. The fields to enter new information in are highlighted in yellow. The rest can stay with the default values.

Pinging REST connections

Having created REST connections, we can check if they can access the systems they point to by pinging them - there is a Ping link for each connection to do that.

Click it and confirm that the response is similar to the one below - as long as it is in green, the connection works fine.

The connection is pinged not from your localhost but from one the server - in this way you can confirm that it truly is your servers, rather than your local system, the ones that have access to a remote endpoint.

Try it: Edit one of the connections and change the URL path to something invalid, like "/does-not-exist". Ping it again. What does the error look like? Change it back when you're done.

Your first API service

  • In the IDE, click File -> New file, enter api.py as the file name and wait for a confirmation that a new service is ready to be invoked.

  • You'll note that the default contents of new services is the same demo code as previously. That's on purpose. Let's now build your integration service step by step.

Step 1: Service skeleton

First, replace the demo code with this skeleton and click Deploy:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# ##############################################################################

class MyService(Service):
    """ Returns user details by the person's name.
    """
    name = 'api.my-service'

    # I/O definition
    input = '-name'
    output = 'user_type', 'account_no', 'account_balance'

    def handle(self):
        name = self.request.input.name or 'partner'
        self.logger.info(f'cid:{self.cid} Received request for {name}')

        self.response.payload = {
          'user_type': '',
          'account_no': '',
          'account_balance': '',
        }

# ##############################################################################

Enter "name=Mike" and click Invoke. You'll get an empty response - that's expected, the skeleton doesn't talk to any systems yet. The minus sign in input = '-name' means the parameter is optional.

Step 2: Add CRM connection

Now, add the CRM call. Replace the handle method:

    def handle(self):
        name = self.request.input.name or 'partner'

        # Get data from CRM
        crm_conn = self.out.rest['CRM'].conn
        crm_request = {'UserName':name}
        crm_data = crm_conn.get(self.cid, crm_request).data

        user_type = crm_data['UserType']
        account_no = crm_data['AccountNumber']

        self.logger.info(f'cid:{self.cid} Got CRM data for {name}')

        self.response.payload = {
          'user_type': user_type,
          'account_no': account_no,
          'account_balance': '',
        }

Click Deploy, then Invoke with "name=Mike". Now you'll see real data from CRM - user_type and account_no will be populated.

Step 3: Add Billing connection

Add the Billing call to get the complete picture:

    def handle(self):

        # For later use
        name = self.request.input.name or 'partner'

        # REST connections
        crm_conn = self.out.rest['CRM'].conn
        billing_conn = self.out.rest['Billing'].conn

        # Prepare requests
        crm_request = {'UserName':name}
        billing_params = {'USER':name}

        # Get data from CRM
        crm_data = crm_conn.get(self.cid, crm_request).data

        # Get data from Billing
        billing_data = billing_conn.post(self.cid, params=billing_params).data

        # Extract the business information from both systems
        user_type = crm_data['UserType']
        account_no = crm_data['AccountNumber']
        account_balance = billing_data['ACC_BALANCE']

        self.logger.info(f'cid:{self.cid} Returning user details for {name}')

        # Now, produce the response for our caller
        self.response.payload = {
          'user_type': user_type,
          'account_no': account_no,
          'account_balance': account_balance,
        }

Click Deploy, then Invoke. All three fields are now populated - your service orchestrates two independent systems and returns a unified response.

Try it: Add a fourth field to the output, e.g. 'greeting'. Set it to something based on the user's name. Invoke the service again - can you see your new field in the response?

Let's analyze a few key points about the complete service:

  • We refer to the previously created REST connections by their names, CRM and Billing. We don't hardcode any information about the connections inside the Python code. This promotes reusability because it lets us reconfigure the connection without having to redeploy the service.

  • The one to CRM is sent using the GET method but the one to Billing is a POST one. CRM receives a JSON request on input but Billing receives query string parameters ("params") because this is what these hypothetical systems expect.

  • We extract the information from both systems using Python's regular dict notation. Note that CRM and Billing use different data format conventions, e.g. UserType vs. ACC_BALANCE.

  • We return a response to our caller using our own preferred data format, which is "lower_case", e.g. user_type or account_no, even though the source systems were using different naming formats.

At its core, that's how an API service works: accept input, connect to external resources, map data from one format to another and provide a response in your canonical data format. All using simple Python code.

Now, let's let external REST clients invoke this service too. For that, we need security credentials first.

API security

  • In Dashboard, go to Security -> API Keys.

  • Click Create a new API key and enter "My API Key" as the name of the security definition, then OK to create it.

  • Click Change API key in the newly created API key and enter any value for the key, e.g. let's say it will be "abc", then OK to set it. This step is required because, by default, all the passwords and secrets in Zato are random uuid4 strings.

What you've just created is a reusable security definition - you can attach to multiple REST channels, which means that you can secure access to multiple REST endpoints of yours using such definitions. We're not limited to API keys though, the same goes for Basic Auth or SSL/TLS, for instance.

Let's create a REST channel now, that is, let's make it possible for external API clients to invoke your services.

Creating REST channels

  • In Dashboard, go to Connections -> Channels -> REST.

  • Click Create a new REST channel, enter the values as below and click OK.

    HeaderValue
    NameMy REST Channel
    Data formatJSON
    URL path/tutorial/api/get-user-details
    Serviceapi.my-service
    SecurityAPI key/My API Key
  • As previously, the fields to enter new information in are highlighted in yellow. The rest can stay with the default values. You can toggle the options to check what else is possible but we don't need it during the tutorial.

A channel is a definition of an API endpoint. That's how you make your services available to external callers, to apps and systems that want to make use of your services.

But note that an endpoint is not the same as a service, because a single service can be mounted on multiple channels, for instance, each channel with a different security definition or a rate-limiting strategy.

OK, good, we have a channel so let's invoke it now.

Invoking REST channels

Since a REST channel is just a regular REST endpoint, we can use any REST client to invoke it. Let's use Postman and curl - the result will be the same in either case.

Here are our endpoint's details:

  • Address: http://localhost:11223/tutorial/api/get-user-details
  • API Key Header: X-API-Key
  • API Key Value: abc
  • Sample request: {"name":"Mike"}
  • REST method: Any will work because we haven't set any limits in the channel's definition, e.g. you can use GET or POST, it doesn't matter, so let's use GET because we're getting data from our endpoint.

$ curl -XGET -H "X-API-Key:abc" http://localhost:11223/tutorial/api/get-user-details
{"account_balance": "357.9", "account_no": "123456", "user_type": "RGV"}
$
Try it: Remove the X-API-Key header from your request. What HTTP status code do you get back? This is the security layer in action.

Interestingly, since your browser receives all the server log messages, you can check in your browser's developer console what your service and the outgoing connections are doing while you're invoking them from Postman (remember, press F12 and make sure to set the level to "Info" only)

Here's a brief overview of what you will observe in the logs that your browser is receiving:

1) Invocation of the REST Channel: Entries indicating the invocation of the REST channel will show details such as the specific channel invoked and the details of the remote API client initiating the request.

2) Requests to and Responses from CRM and Billing: Following the invocation of the REST channel, you'll see entries corresponding to requests sent to and responses received from the CRM and Billing systems. These entries can be correlated with the same correlation ID (CID) as the initial REST channel invocation, providing a clear traceability of the request flow through the system.

3) Custom Log Messages: Additionally, custom log messages added within the service implementation will also be captured in the log file. These messages can provide additional context or insights into the service's behavior and processing steps.

And there you have it, a reusable API endpoint, implemented in Python and secured with an API key.

Let's check how to use the scheduler now, how to invoke services in background periodically.

Python task scheduler

  • In Dashboard, go to Scheduler, click Create a new job: interval-based

  • A form will show on screen, fill it out as below:

And you're done. You've just scheduled your service to be invoked in background once in 10 seconds, indefinitely, using an interval-based job.

Try it: Change the interval to 5 seconds. Open the browser console (F12, Info level) and watch - can you see the repeated invocations appearing in the logs?

The scheduler supports per-job timezones, jitter (to prevent clustered servers from firing at the same instant), max execution time (to kill hung jobs), one-time jobs, and full YAML export for reproducible deployments:

scheduler:
  - name: my.report.job
    service: api.my-service
    job_type: interval_based
    seconds: 10
    timezone: Europe/Berlin
    jitter_ms: 5000
    max_execution_time_ms: 15000

For the complete guide, see the Python scheduler tutorial.

Beyond REST - what else can you build

The REST service you've just built is only one example of what's possible. Here are other integration patterns that work the same way - you write a service, and the platform handles the wiring.

OpenAPI channels

Group your REST channels into an OpenAPI specification that clients can consume directly. Create an OpenAPI channel in Dashboard under Connections -> Channels -> OpenAPI, assign REST channels to it, and Zato generates a standard OpenAPI 3.1 spec - downloadable as YAML or accessible via HTTP. See OpenAPI docs.

Event streaming with Kafka

Create a Kafka channel in Dashboard, point it to a topic, and your service is invoked for each message - zero wiring code:

class MyService(Service):
    def handle(self):
        data = self.request.raw_request
        self.logger.info('Kafka message: %s', data)

Publishing is just as simple - self.out.kafka['my-publisher'].send({'event': 'order.created'}). See the full Kafka examples.

GraphQL

Create a GraphQL outgoing connection in Dashboard and query any GraphQL server directly from your services:

class MyService(Service):
    def handle(self):
        conn = self.out.graphql['ms365-graph']
        result = conn.execute('{ users { id displayName mail } }')
        self.logger.info('Users: %s', result)

Variables are passed via the params argument - conn.execute(query, params={'user_id': 'abc-123'}). See the full GraphQL examples.

Publish/subscribe messaging

Zato has a built-in pub/sub broker. Services publish to topics, external applications subscribe and pull messages from their queues via REST. No external broker required:

class MyService(Service):
    def handle(self):
        self.pubsub.publish('orders.completed', {'order_id': 12345})

See pub/sub docs.

AMQP and RabbitMQ

Same pattern as Kafka - create an AMQP channel in Dashboard pointing to a queue, and your service is invoked for each message. Publishing goes through self.outgoing.amqp.send. See AMQP examples.

Rule engine

Express business rules in a way that both technical and business people can read and maintain. Rules are evaluated by the platform and can gate, route, or transform requests without touching service code. See the rule engine tutorial.

Rate limiting

Protect your endpoints with per-channel or per-client rate limits - requests per minute, per hour, with burst allowances, time-of-day windows, and IP-based blocking. All configured in Dashboard, no code changes needed. See rate limiting docs.

Unit testing

Well, we do have an API service but we don't have any tests for it.

Zato ships with a unit testing framework that lets you test your services without running a server. You write tests using Python's standard unittest module, mock external connections, and verify your service logic works correctly.

DevOps and CI/CD automation

Throughout the tutorial, you may have been wondering about one thing.

OK, we have Python services and unit tests, but how am I actually going to provision my new environments? It's good that there's Dashboard but am I supposed to keep clicking and filling out forms each time I have a new environment? If I create a few dozen REST channels and other connections, how do I automate the process of deploying it all? How do I make my builds reproducible?

These are good questions and there's a good answer to it too. You can automate it all very easily.

There's an entire chapter about it but, in short, everything you do in Dashboard can be exported to YAML, stored in git, and imported elsewhere.

Such a file will have entries like these here:

security:
  - name: My API Key
    type: apikey
    username: My API Key
    password: Zato_Enmasse_Env.My_API_Key

channel_rest:
  - name: My REST Channel
    service: api.my-service
    security_name: My API Key
    url_path: /tutorial/api/get-user-details
    data_format: json

You can easily recognize the same configuration that you previously added using Dashboard. It's just in YAML now.

You push files with such configuration to git and that lets you have reproducible builds - you're always able to reproduce the same exact setup in other systems or environments. In other words, this is Infrastructure as Code.

Connect your AI copilot or LLM

If you ever need any live assistance during the tutorial, remember that you can connect your AI copilot or LLM and ask it questions about the tutorial and other parts of the documentation.

Zato exposes its documentation via MCP (Model Context Protocol) at https://zato.io/mcp. You can connect Claude, Cursor, VS Code, or any other MCP-compatible tool:

claude mcp add zato-docs https://zato.io/mcp

More capabilities and features

There are many more features that the platform has and this tutorial only showed you the basics that will already let you integrate systems but it's still just the tip of the iceberg.

If you want to discover on your own what else is possible, it's a good idea to check the individual chapters of the documentation and learn more about what makes the most sense in your own situation, for your own integration and automation purposes.

What next?

If you're building integrations and you'd like a trusted partner to guide you on architecture and design, get in touch and let's see what we can do together.

Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"For me, Zato Source is the only technology partner to help with operational improvements."

- John Adams
Program Manager of Channel Enablement at Keysight