-
SEO Learning Center
Broaden your SEO knowledge with resources for all skill levels.
-
The Beginner's Guide to SEO
If you're brand new to SEO, start here.
-
SEO Q&A
Get answers from the Moz Community.
-
Free Downloads and More
Quick access to whitepapers, reports, guides, webinars, and case studies.
-
Help Hub
Learn how to use Moz products.
-
Community & Events
Connect with over 600k online marketers.
-
SEO Training
Instructor-led classes and seminars
Asynchronous API Calls
Most Moz API methods return their data directly in the response to your request. Some methods instead do their work in the background: they immediately hand back a receipt called an async_call, and you collect the data shortly afterwards by looking that receipt up. This guide explains why those methods exist, what an async_call contains, and how to poll one until it produces a result.
Why Some Methods Are Asynchronous
A few of the operations exposed by the Moz API take long enough that holding an HTTP connection open until they finish would be unreliable. Rather than risk proxy and client timeouts on a request that is still perfectly healthy, these methods accept your request, acknowledge it, and do the work outside of the request that started it.
The trade-off is that getting data from an asynchronous method takes at least two requests instead of one. The method documentation states whether a method is asynchronous.
The Async Call Object
Every asynchronous method returns the same envelope, whatever the method does and whether or not the work eventually succeeds:
{ "id": "01a0458f-a14f-7254-aa11-7c6cb734e3f2", "jsonrpc": "2.0", "result": { "async_call": { "id": "c3397e5a-ce31-42fb-8607-ed40487aea18", "action": "test.ping.async", "status": "pending", "ttl": 120, "result": null, "error": null } } }
An async_call contains the following fields:
- id: The identifier for this call. Pass it to async.call.lookup to check on the request and collect its data.
- action: The name of the method that is being run.
- status: How far along the request is, described below.
- ttl: The number of seconds remaining before this call is discarded.
- result: The data the method produced, or null if it has not finished.
- error: The error the method produced, or null if it has not failed.
An asynchronous method always returns an async_call. In some instances an asynchronous method could return an async_call that is already complete on the first call.
Call Statuses
A call progresses through the following statuses:
- pending: The request is queued and waiting for relevant data.
- processing: The result is being generated.
- complete: The request has finished, successfully or otherwise.
While the status is anything other than complete, both result and error will be null and you should keep polling. Once the status is complete, exactly one of result or error will be non-null.
The Request Lifecycle
The examples below use test.ping.async, a method that exists purely so you can exercise this flow. It costs no quota, and its completion is deliberately held back for about five seconds. The delay is provided so that your code can test verifying a call moving from pending to complete status on subsequent polls.
First, call the asynchronous method as you would any other Moz API method:
curl -X POST https://api.moz.com/jsonrpc \ -H "x-moz-token: <YOUR_MOZ_TOKEN>" \ -H "Content-Type: application/json" \ -d ' { "jsonrpc": "2.0", "id": "01a0458f-a14f-7254-aa11-7c6cb734e3f2", "method": "test.ping.async", "params": { "data": { "message": "Hello, world." } } }'
The response acknowledges the request and gives you the call to poll:
{ "id": "01a0458f-a14f-7254-aa11-7c6cb734e3f2", "jsonrpc": "2.0", "result": { "async_call": { "id": "c3397e5a-ce31-42fb-8607-ed40487aea18", "action": "test.ping.async", "status": "pending", "ttl": 120, "result": null, "error": null } } }
Next, look the call up by its id using async.call.lookup:
curl -X POST https://api.moz.com/jsonrpc \ -H "x-moz-token: <YOUR_MOZ_TOKEN>" \ -H "Content-Type: application/json" \ -d ' { "jsonrpc": "2.0", "id": "01a0458f-a94f-7c8b-b3d2-4f0e9a71c6d5", "method": "async.call.lookup", "params": { "data": { "id": "c3397e5a-ce31-42fb-8607-ed40487aea18" } } }'
If the work is still in flight, you get the same object back with a lower ttl, and you should wait and try again:
{ "id": "01a0458f-a94f-7c8b-b3d2-4f0e9a71c6d5", "jsonrpc": "2.0", "result": { "async_call": { "id": "c3397e5a-ce31-42fb-8607-ed40487aea18", "action": "test.ping.async", "status": "pending", "ttl": 118, "result": null, "error": null } } }
Once the work is done, the same lookup returns the finished call. This is the response you act on.
Polling for a Result
Repeat the async.call.lookup request until the status is complete. We generally suggest not polling more than once every five seconds; the documentation for an individual method may suggest a longer interval if that method typically takes longer than that to run. Polling more aggressively will not return your data any sooner, and may result in your requests being rate limited.
async.call.lookup consumes no quota, so polling costs you nothing beyond the request itself. The quota for the underlying work is accounted for by the asynchronous method that started the call, not by the lookups.
Successful Requests
A request that succeeded has a status of complete, a populated result, and a null error:
{ "id": "01a0459b-32e8-7f41-9c07-2b8d5e6a41f9", "jsonrpc": "2.0", "result": { "async_call": { "id": "3d446f53-aaba-4bdb-9d47-b1d0754bfe90", "action": "test.ping.async", "status": "complete", "ttl": 20, "result": { "message": "Hello, world." }, "error": null } } }
The contents of async_call.result are exactly what the method would have returned in the top-level result had it been synchronous, so the response format documented for the method describes async_call.result verbatim.
Failed Requests
There are two distinct ways an asynchronous request can fail, and they look different.
The first is a request that is rejected before it is ever accepted: an invalid token, a malformed parameter, or insufficient quota. These fail exactly like a synchronous request: you get the appropriate HTTP status code and a top-level error, with no async_call at all. The error handling guide covers these.
The second is a request that was accepted and then failed while being worked on. The lookup succeeds, so the HTTP status is 200 and there is no top-level error. The failure is reported inside the call, with a status of complete, a null result, and a populated error:
{ "id": "01a045a4-7b13-7d90-ae52-3c1f8b0d7264", "jsonrpc": "2.0", "result": { "async_call": { "id": "18e5de71-af54-490d-8eb1-0596a4f68121", "action": "test.ping.async", "status": "complete", "ttl": 20, "result": null, "error": { "code": -32653, "status": 400, "data": {}, "message": "We've thrown an error!" } } } }
async_call.error has the same shape as a top-level error object, and its code and status describe the failure exactly as they would have for a synchronous request, so you can hand it to the same error handling code. Because the HTTP status of the lookup reflects the lookup rather than the work, check async_call.error rather than the HTTP status to decide whether the request succeeded.
Call Time to Live
The ttl on a call is the number of seconds remaining before that call is discarded and its data is no longer retrievable. It counts down as the call ages, so the value drops between polls.
While a call is in progress, the ttl reflects how long the request will be retained while it is being worked on. This defaults to 120 seconds for most asynchronous methods. Read the value from the response rather than assuming a fixed number, as a method expected to take longer may be given a longer window.
Once you fetch a call whose status is complete, its TTL is set to 20 seconds. That short window exists only so that you can retry the lookup if the response is lost in transit; it is not a cache to read from later. When you receive a complete call, use that response’s result or error immediately rather than looking the call up again later.
If a call’s TTL reaches zero it is discarded, and any further lookup of that ID returns a Call Not Found error.
Call Not Found Errors
async.call.lookup returns a Call Not Found error when the ID you asked for does not correspond to a call that is still being retained. It comes back with a 404 status and a -32655 code:
{ "id": "01a045b0-c8d5-7a36-8e14-6f92b7c3ad08", "jsonrpc": "2.0", "error": { "code": -32655, "status": 404, "data": {}, "message": "Call was not found." } }
The common causes are:
- More than 20 seconds have passed since you first retrieved the completed call.
- The call expired before you collected it, because polling stopped or was never started.
A Call Not Found error is not a transient condition and retrying the lookup will not help; the call and any data it held are gone. To recover, call the asynchronous method again to start a new request.
Testing Your Integration
Two methods exist specifically so you can verify your handling of this flow without spending quota or interpreting real data:
- Async Ping (test.ping.async) returns an async_call that stays pending for about five seconds before completing, echoing back the message you sent. Pass error: true instead to have the call complete with an error, which lets you confirm that your client distinguishes a failed asynchronous request from a successful one.
- Ping (test.ping) is the synchronous equivalent, useful for checking your endpoint, headers, and token in isolation before adding polling.
Both methods cost zero rows, but they do require an account with Moz API access.