Skip to content

Batch calls with multicall

system.multicall packs many API calls into one HTTP request. Use it for bulk jobs such as adding hundreds of DIDs or reading many accounts: you save one HTTPS round trip and one digest handshake per call. In a test with 1000 addDID calls, one by one took 5 min 20 s and multicall took 1 min 39 s, about three times faster.

All calls in a batch run as the authenticated customer, one after another, in the order you send them. Each call succeeds or fails on its own: a fault in one call does not stop the others and does not undo the calls before it.

system.multicall takes one parameter, an array. Each element is a struct with two members:

Name Type Description
methodName string The method to call, for example getAccountInfo
params array The parameters of that method. API methods take one struct, so this is an array of one struct
<?xml version="1.0"?>
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param>
<value><array><data>
<value><struct>
<member><name>methodName</name><value><string>getAccountInfo</string></value></member>
<member><name>params</name><value><array><data>
<value><struct>
<member><name>i_account</name><value><int>3092</int></value></member>
</struct></value>
</data></array></value></member>
</struct></value>
<value><struct>
<member><name>methodName</name><value><string>getAccountInfo</string></value></member>
<member><name>params</name><value><array><data>
<value><struct>
<member><name>i_account</name><value><int>-1</int></value></member>
</struct></value>
</data></array></value></member>
</struct></value>
</data></array></value>
</param>
</params>
</methodCall>

The response is an array with one element per call, in the same order:

  • a call that succeeded returns an array of one element, the method’s usual result struct;
  • a call that failed returns a struct with faultCode and faultString, the same members as a fault.
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><array><data>
<value><array><data>
<value><struct>
<member><name>result</name><value><string>OK</string></value></member>
<member><name>i_account</name><value><int>3092</int></value></member>
<member><name>username</name><value><string>1001</string></value></member>
</struct></value>
</data></array></value>
<value><struct>
<member><name>faultCode</name><value><int>403</int></value></member>
<member><name>faultString</name><value><string>…</string></value></member>
</struct></value>
</data></array></value>
</param>
</params>
</methodResponse>

If the batch itself is malformed, for example when the parameter is not an array, the whole request returns one fault instead.

The call() helper of the overview wraps one result struct, and xmlrpc.client.MultiCall needs a ServerProxy, which has no digest authentication. This function sends the batch with requests and turns every element of the response into a result or an xmlrpc.client.Fault:

flysip_multicall.py
import xmlrpc.client
import requests
from flysip_api import AUTH, URL # the helper module from the overview
def multicall(calls: list[tuple[str, dict]], timeout: int = 300) -> list:
"""Send (method, params) pairs in one request.
Returns one item per call, in order: the result struct of the call,
or an xmlrpc.client.Fault for a call that failed.
"""
batch = [{"methodName": method, "params": [params]} for method, params in calls]
body = xmlrpc.client.dumps((batch,), "system.multicall", allow_none=True)
resp = requests.post(URL, data=body.encode(), auth=AUTH,
headers={"Content-Type": "text/xml"}, timeout=timeout)
resp.raise_for_status()
# Raises Fault if the batch as a whole was rejected.
results = xmlrpc.client.loads(resp.content, use_builtin_types=True)[0][0]
return [
xmlrpc.client.Fault(r["faultCode"], r["faultString"]) if isinstance(r, dict)
else r[0]
for r in results
]

Read three accounts, one of which does not exist:

request
calls = [
("getAccountInfo", {"i_account": 3092}),
("getAccountInfo", {"i_account": -1}), # fails
("getAccountInfo", {"username": "1001"}),
]
for (method, params), res in zip(calls, multicall(calls)):
if isinstance(res, xmlrpc.client.Fault):
print(method, params, "fault", res.faultCode, res.faultString)
else:
print(method, params, res["username"])

Add 1000 DIDs in batches of 200, so that each request finishes well within the timeout:

request
dids = [f"1206555{n:04d}" for n in range(1000, 2000)]
for start in range(0, len(dids), 200):
chunk = dids[start:start + 200]
results = multicall([("addDID", {"did": d, "incoming_did": d}) for d in chunk])
for did, res in zip(chunk, results):
if isinstance(res, xmlrpc.client.Fault):
print(f"{did}: fault {res.faultCode} {res.faultString}")
else:
print(f"{did}: i_did {res['i_did']}")

The phpxmlrpc library has multicall() on its client. Pass false as the fourth argument so that it sends a real system.multicall instead of falling back to one request per call. It returns one response object per call:

multicall.php
<?php
include 'xmlrpc/xmlrpc.inc'; // the phpxmlrpc library
$cli = new xmlrpc_client('https://panel.example.com/xmlapi/xmlapi');
$cli->setCredentials('acme-web', 'API-PASSWORD', CURLAUTH_DIGEST);
$cli->request_charset_encoding = 'UTF-8';
$cli->return_type = 'phpvals';
$msgs = [];
foreach ([3092, -1, 3093] as $i_account) {
$msgs[] = new xmlrpcmsg('getAccountInfo', [new xmlrpcval([
'i_account' => new xmlrpcval($i_account, 'int'),
], 'struct')]);
}
$responses = $cli->multicall($msgs, 60, '', false);
foreach ($responses as $r) {
if ($r->faultCode()) {
error_log('Fault ' . $r->faultCode() . ': ' . $r->faultString());
} else {
$v = $r->value();
print $v['username'] . "\n";
}
}