REST outgoing connections

Reach any external REST API from every service - pools, timeouts, retries and guaranteed delivery included.

An outgoing REST connection holds everything needed to reach one external API - the address, the URL path, the credentials, the connection pool and the timeout. You create a connection once in the Dashboard and every service invokes it by name, with no URLs or credentials in Python code.

For external systems that expose a GraphQL API rather than REST, Zato offers dedicated GraphQL outgoing connections.

Create a connection

To create a connection, go to Connections > Outgoing > REST in the Dashboard, click Create a new REST outgoing connection and fill in the form:

  1. Name: Customer API
  2. Host: the base address of the API, e.g. https://api.example.com
  3. URL path: the endpoint path, which can include {placeholders} for parameters
  4. Data format: JSON
  5. Click OK

The connection is ready the moment you click OK - services invoke it by its name, with no restarts anywhere.

The same form also takes:

  • Security: a Basic Auth, API key or Bearer token definition - Zato attaches the credentials to every request, see authentication
  • Pool size: how many network connections Zato keeps open to the endpoint - requests above that number wait for a connection to free up rather than opening new ones
  • Timeout: how many seconds an invocation waits for the endpoint to answer

Import connections from OpenAPI

When the external system publishes an OpenAPI specification, you can create its connections in bulk instead of one by one:

  1. On the same page, click Import OpenAPI
  2. Paste the OpenAPI YAML or JSON into the editor, or enter a URL to fetch it from
  3. In the next step, select which endpoints to import from the table
  4. Click OK

The import creates one outgoing REST connection for each selected path, with the host and URL path pre-configured. If the specification defines authentication - Basic Auth, Bearer token or API key - the matching security definitions are created too.

An OpenAPI specification describes authentication requirements but carries no actual credentials, so after the import you enter them yourself:

  • Basic Auth: go to Security > HTTP Basic Auth, find the definition and click Change password
  • API key: go to Security > API Keys, find the definition and click Change secret
  • Bearer token: go to Security > OAuth > Outgoing > Client Credentials and enter the client ID, the client secret and the token endpoint URL

Once the credentials are in place, the connections that use these definitions authenticate automatically.

Timeouts

A connection waits for the endpoint as long as its Timeout field says. A single call can override it:

request = {'Hello': 'World'}
response = conn.post(self.cid, request, timeout=30)

Without an explicit timeout anywhere, the wait defaults to the Linux kernel's default of approximately 2 minutes.

Retries

When an external API is temporarily unavailable, the connection can retry the request automatically, with exponential backoff. Retries are triggered on:

  • Connection errors - the remote server is unreachable
  • Timeout errors - the request took longer than the configured timeout
  • HTTP 429 Too Many Requests - the endpoint says the request was made too soon rather than that it was wrong

Everything else, including HTTP error responses such as 500, is returned to your service as-is, without any retries.

The four settings:

SettingDefaultWhat it does
max_retries0How many times a failed invocation is retried. 0 means no retries at all.
retry_sleep_time2How many seconds to sleep before the first retry.
retry_backoff_multiplier2Each retry sleeps this many times longer than the previous one, up to 8 seconds per a single sleep.
retry_backoff_threshold60A cap on the total time spent sleeping between retries, in seconds. Once reached, no more retries take place.

By default, retries are disabled (max_retries=0) - a network error is reported to your service immediately and a 429 response reaches it exactly as it arrived.

Configure retries per connection

The retry settings can be stored on the connection itself, so every invocation of that connection uses them without any code changes. In the Dashboard, open the connection's create or edit form and click Toggle options next to More options - the four retry fields are there.

Retry options on a connection

In enmasse, add the fields to the connection's YAML definition:

outgoing_rest:
  - name: Set Billing Info
    host: https://api.example.com
    url_path: /billing
    max_retries: 5
    retry_sleep_time: 2
    retry_backoff_multiplier: 2
    retry_backoff_threshold: 60

The same settings apply to outgoing SOAP connections, both in the Dashboard and in enmasse, under outgoing_soap.

Configure retries per call

You can also pass the settings directly when invoking a connection - values given in a call always win over what the connection itself is configured with:

# Retry up to 5 times on connection errors or timeouts
response = conn.get(self.cid, max_retries=5)
response = conn.post(
    self.cid,
    payload,
    max_retries=5,               # Maximum number of retry attempts
    retry_sleep_time=2,          # Initial sleep time in seconds
    retry_backoff_multiplier=2,  # Each sleep is twice as long as the previous one, up to 8s
    retry_backoff_threshold=60   # Stop retrying after this many seconds of total sleep time
)

The precedence is always: explicit call arguments win over the connection's own configuration, which wins over the defaults.

What the settings lead to

  • With max_retries=0 - the default - a network error is reported immediately, there are no retries.

  • With max_retries=5 and retry_sleep_time=2, a call that keeps failing sleeps 2s, 4s, 8s, 8s and 8s between attempts before giving up - six requests and about 30 seconds in total.

  • With max_retries=3, retry_sleep_time=1 and retry_backoff_multiplier=3, the sleeps are 1s, 3s and 8s - the third sleep would be 9s but no single sleep is ever longer than 8 seconds.

  • With max_retries=10, retry_sleep_time=2 and retry_backoff_threshold=4, only two retries take place - after two 2-second sleeps the total sleep time reaches the 4-second threshold and the error is reported, even though max_retries alone would allow more attempts.

Rate-limited responses

A rate-limited endpoint answers with HTTP 429 to say that the request was made too soon, which is the one status code that is retried. It uses the same four settings as a network error does, so with the default max_retries=0 a 429 reaches your service straight away and nothing is retried.

When the retries run out, the 429 response itself is what your service receives, like any other response - it is the endpoint's own answer, not an exception.

An endpoint that answers with 429 may also say how long to wait before the next request, in a Retry-After header. When it does, that is how long the connection waits, instead of the sleep its backoff schedule would have produced. Both forms of the header are understood:

  • A number of seconds, e.g. Retry-After: 30
  • An HTTP date to wait until, e.g. Retry-After: Sat, 25 Jul 2026 18:30:00 GMT - a date that has already passed means there is nothing to wait for

A header that cannot be read as either is treated as one that was never sent, and the backoff schedule applies.

The wait an endpoint asks for still has to fit in what is left of retry_backoff_threshold, that setting being a cap on the total time one invocation may spend sleeping. If it does not fit, no further request is made and the 429 response is returned to your service.

For instance, with max_retries=5, retry_sleep_time=2 and retry_backoff_threshold=60:

  • A 429 with Retry-After: 30 sleeps 30 seconds rather than 2, then retries. A second 429 asking for the same 30 seconds fits the remaining budget exactly, so it is honoured too, and a third one is not.

  • A 429 with Retry-After: 300 is not retried at all - five minutes is more than the whole budget, so the response comes back to your service immediately.

  • A 429 with no Retry-After sleeps 2s, 4s, 8s, 8s and 8s, exactly as a connection error would.

Guaranteed delivery

Synchronous calls make your service wait for the endpoint to answer and handle every outcome, including no answer at all. When the message only needs to reach the endpoint eventually, and your service does not need the response, publish it instead:

conn = self.rest['Orders API']
conn.publish({'order_id': '12345', 'status': 'shipped'})

The call returns as soon as the message is stored, which takes a few milliseconds and does not touch the network. From that point on it is the platform's job to deliver it - the endpoint being down, slow or unreachable is no longer something your service needs to handle.

Each outgoing connection that is published to has a queue of its own, and messages in it are delivered one after another, in the order they were published. Delivery is retried with a growing interval, from 3 seconds up to 10 seconds between attempts, for up to 30 days. A message is removed from the queue only once the endpoint accepted it, so a server restart in the middle of all of this changes nothing - the queue is in a database, and delivery picks up where it left off.

The same publisher is reached both ways of naming a connection, so these two lines publish to one queue:

self.rest['Orders API'].publish(data)
self.out.rest['Orders API'].publish(data)

The message is sent using the connection's own configuration - the host, the URL path, the HTTP method, the headers, the query parameters and the credentials are the ones the connection is configured with, exactly as they would be for a synchronous call. What your service passes to publish is the request body, and a Python dict is serialized to JSON on the way in.

publish returns a PublishResult whose msg_id attribute identifies the message that was queued:

result = conn.publish({'order_id': '12345'})
self.logger.info(f'Queued as {result.msg_id}')

The message's own metadata can be set per publication, the same way as in the pub/sub Python API:

conn.publish(
    payload,
    priority=7,        # 0-9, the default is 5
    expiration=3600,   # Seconds until the message expires, the default is a year
    correl_id=self.cid # Correlation ID to send along
)

A connection keeps its queue for as long as it exists. After you rename a connection, the messages queued under the old name are delivered under the new one, exactly once - there is only ever one queue per connection. Deleting a connection deletes its queue - the remaining messages are dropped and their count is written to the server log.

Audit log

Each outgoing connection records the requests it sends and the responses it receives in its own audit log. This applies to user-defined connections only - internal ones are skipped entirely, and pings are never recorded.

See also

PageWhat it covers
Calling REST APIsInvoking connections from Python - GET, POST, parameters and headers
AuthenticationThe security definitions a connection authenticates with
REST adapterDeclarative API calls with response mapping and no boilerplate
Error handlingWhat your service receives when an endpoint fails

Learn more