Vendor APIs
Request-by-request reference for Apify, Google Document AI and xAI Grok, with code in five languages. Use it to try each service directly.
Overview
Each section below gives the base URL, how to authenticate, and every call with request examples and a sample response. Use the language buttons at the top right to switch all code samples between cURL, Python, JavaScript, PHP and Go.
APIFY_TOKEN, GCP_TOKEN, XAI_API_KEY). Never paste keys into code or commit them.Encoding a file for Document AI
rawDocument.content must be the file as base64. Produce it like this, then put the result in the request body.
base64 -w0 annual-report-2025.pdf # macOS: base64 -i annual-report-2025.pdf
import base64
content = base64.b64encode(open("annual-report-2025.pdf", "rb").read()).decode()
import { readFileSync } from "node:fs";
const content = readFileSync("annual-report-2025.pdf").toString("base64");
<?php
$content = base64_encode(file_get_contents("annual-report-2025.pdf"));
import ("encoding/base64"; "os")
data, _ := os.ReadFile("annual-report-2025.pdf")
content := base64.StdEncoding.EncodeToString(data)
Apify
Runs web crawlers (actors) and returns what they collect. Base URL https://api.apify.com/v2. Auth: Authorization: Bearer $APIFY_TOKEN.
Start an actor run
POST /acts/{actor}/runs
Starts a run and returns at once. Keep data.id (for polling) and data.defaultDatasetId (for the results).
| Field | Type | Required | Description |
|---|---|---|---|
actor | string | yes | Actor id with ~ in place of /: apify~website-content-crawler. |
body | object | yes | The actor input, as defined by the actor. Fields vary per actor. |
curl -X POST "https://api.apify.com/v2/acts/apify~website-content-crawler/runs" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startUrls": [
{
"url": "https://example.com/"
}
],
"maxCrawlPages": 10,
"crawlerType": "cheerio",
"maxPdfs": 5
}'import os, requests
BASE = "https://api.apify.com/v2"
r = requests.post(
f"{BASE}/acts/apify~website-content-crawler/runs",
json={'startUrls': [{'url': 'https://example.com/'}],
'maxCrawlPages': 10,
'crawlerType': 'cheerio',
'maxPdfs': 5},
headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://api.apify.com/v2";
const res = await fetch(`${BASE}/acts/apify~website-content-crawler/runs`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.APIFY_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"startUrls": [
{
"url": "https://example.com/"
}
],
"maxCrawlPages": 10,
"crawlerType": "cheerio",
"maxPdfs": 5
})
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://api.apify.com/v2";
$ch = curl_init("$base/acts/apify~website-content-crawler/runs");
$opts = [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("APIFY_TOKEN"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"startUrls" => [[
"url" => "https://example.com/",
]],
"maxCrawlPages" => 10,
"crawlerType" => "cheerio",
"maxPdfs" => 5,
]),
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
base := "https://api.apify.com/v2"
body := `{
"startUrls": [
{
"url": "https://example.com/"
}
],
"maxCrawlPages": 10,
"crawlerType": "cheerio",
"maxPdfs": 5
}`
req, _ := http.NewRequest("POST", base+"/acts/apify~website-content-crawler/runs", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"data": {
"id": "HG7ML7M8z78YcAPEB",
"actId": "aYG0l9s7dbB7j3gbS",
"status": "RUNNING",
"startedAt": "2026-10-20T08:15:02.000Z",
"finishedAt": null,
"defaultDatasetId": "WkzbQMuFYuamGv3YF",
"defaultKeyValueStoreId": "eJNzqsbPiopwJcgwK"
}
}Get run status
GET /actor-runs/{runId}
Poll every few seconds. The run is over when data.status is SUCCEEDED, FAILED, ABORTED or TIMED-OUT. While it works the status is READY or RUNNING.
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | yes | Path parameter. |
curl -X GET "https://api.apify.com/v2/actor-runs/HG7ML7M8z78YcAPEB" \
-H "Authorization: Bearer $APIFY_TOKEN"import os, requests
BASE = "https://api.apify.com/v2"
r = requests.get(
f"{BASE}/actor-runs/HG7ML7M8z78YcAPEB",
headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://api.apify.com/v2";
const res = await fetch(`${BASE}/actor-runs/HG7ML7M8z78YcAPEB`, {
method: "GET",
headers: { Authorization: `Bearer ${process.env.APIFY_TOKEN}` }
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://api.apify.com/v2";
$ch = curl_init("$base/actor-runs/HG7ML7M8z78YcAPEB");
$opts = [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("APIFY_TOKEN")],
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://api.apify.com/v2"
req, _ := http.NewRequest("GET", base+"/actor-runs/HG7ML7M8z78YcAPEB", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"data": {
"id": "HG7ML7M8z78YcAPEB",
"actId": "aYG0l9s7dbB7j3gbS",
"status": "SUCCEEDED",
"startedAt": "2026-10-20T08:15:02.000Z",
"finishedAt": "2026-10-20T08:17:40.000Z",
"defaultDatasetId": "WkzbQMuFYuamGv3YF",
"defaultKeyValueStoreId": "eJNzqsbPiopwJcgwK"
}
}Get dataset items
GET /datasets/{datasetId}/items
The crawled pages and PDFs. Unlike the other two Apify calls, the response is a bare array. Fields depend on the actor. Example for the website content crawler:
| Field | Type | Required | Description |
|---|---|---|---|
datasetId | string | yes | Path parameter. |
| Field | Type | Required | Description |
|---|---|---|---|
format | string | no | json returns an array. Also csv, xlsx, jsonl. |
clean | boolean | no | Drops hidden and empty fields. |
limit | integer | no | Maximum items returned. |
curl -X GET "https://api.apify.com/v2/datasets/WkzbQMuFYuamGv3YF/items?format=json&clean=true&limit=500" \
-H "Authorization: Bearer $APIFY_TOKEN"import os, requests
BASE = "https://api.apify.com/v2"
r = requests.get(
f"{BASE}/datasets/WkzbQMuFYuamGv3YF/items",
headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
params={'format': 'json', 'clean': 'true', 'limit': 500},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://api.apify.com/v2";
const res = await fetch(`${BASE}/datasets/WkzbQMuFYuamGv3YF/items?format=json&clean=true&limit=500`, {
method: "GET",
headers: { Authorization: `Bearer ${process.env.APIFY_TOKEN}` }
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://api.apify.com/v2";
$ch = curl_init("$base/datasets/WkzbQMuFYuamGv3YF/items?format=json&clean=true&limit=500");
$opts = [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("APIFY_TOKEN")],
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://api.apify.com/v2"
req, _ := http.NewRequest("GET", base+"/datasets/WkzbQMuFYuamGv3YF/items?format=json&clean=true&limit=500", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
[
{
"url": "https://example.com/notices/dividend-2026",
"metadata": {
"title": "Dividend notice"
},
"text": "Notice is hereby given that ..."
}
]Choosing a crawler type
The Website Content Crawler fetches each page in one of several ways. You choose with the crawlerType field of the actor input. It decides whether pages are loaded with a plain HTTP request or a real browser, which affects speed and whether JavaScript-built content and bot protection are handled.
| Value | How it loads pages | Runs JavaScript | Speed |
|---|---|---|---|
cheerio | Raw HTTP request, then parses the returned HTML. No browser. | No | Fastest. |
playwright:adaptive (default) | Starts with raw HTTP and switches to a browser for pages that need it. | Only when needed | Fast on simple pages, slower on JavaScript pages. Apify recommends it as the default. |
playwright:firefox | Always a headless Firefox browser. | Yes | Slowest. Best at avoiding blocking. |
jsdom, playwright:chrome | Older options. | n/a | Deprecated. Do not use for new work. |
Confirm the value names on the actor's input page before relying on them.
Which one to use
| Scenario | Use | Why |
|---|---|---|
| Regulator, issuer or news sites with ordinary pages and links to PDF files (notices, circulars, annual reports) | cheerio | The text is already in the HTML. This is the fastest option. |
| You have not inspected the site, or a source mixes simple and dynamic pages | playwright:adaptive | It uses a browser only on the pages that need one. |
| Content appears only after the page loads: single-page apps, "Loading..." placeholders, tables or lists filled by scripts, "load more" buttons | playwright:adaptive, or playwright:firefox if adaptive still misses content | cheerio would return an empty or partial page. |
| The site blocks you: HTTP 403 or 429, captcha pages, "access denied" text in the results | playwright:firefox, with residential proxies in the actor's proxy settings | A real browser looks like a normal visitor. Proxies help when the site blocks the crawler's IP address. |
| Large crawl where speed matters | cheerio, after a small test | Browser crawling is much slower per page. |
How to decide
- Start a test run with
cheerioandmaxCrawlPagesset to 5. - Read the dataset items. If
textholds the real page content, keepcheerio. - If
textis empty, very short, or only menu text, switch toplaywright:adaptiveand test again. - If you now see blocking (403, 429, captcha), switch to
playwright:firefoxand add residential proxies. - Raise
maxCrawlPagesonly after a small test looks right.
Example actor inputs
Static site with PDFs
{
"startUrls": [{"url": "https://example.com/notices/"}],
"crawlerType": "cheerio",
"maxCrawlPages": 50,
"maxCrawlDepth": 2,
"saveContentTypes": ["application/pdf"]
}
Unknown or JavaScript-heavy site
{
"startUrls": [{"url": "https://example.com/"}],
"crawlerType": "playwright:adaptive",
"maxCrawlPages": 50
}
Site that blocks crawlers
{
"startUrls": [{"url": "https://example.com/"}],
"crawlerType": "playwright:firefox",
"maxCrawlPages": 20,
"proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}
}
Other settings that control a crawl
| Field | What it does |
|---|---|
maxCrawlPages | Stops after this many pages. Counts start URLs and pagination pages. The default is effectively unlimited, so always set it. |
maxCrawlDepth | How many links deep to follow from a start URL. Start URLs are depth 0, so 0 crawls only the start URLs. |
includeUrlGlobs | Extra URL patterns the crawler may follow, beyond the start URLs and their subpages. |
excludeUrlGlobs | URL patterns to skip. Start URLs are always crawled. |
maxResults | Caps stored pages and files. Downloaded files count toward it. |
saveMarkdown | Also returns each page as Markdown in the markdown field. |
saveContentTypes | Downloads linked files by content type, for example application/pdf. |
Google Document AI
OCR for PDFs and images using the online process method on an OCR processor. Base URL https://{location}-documentai.googleapis.com, for example us. Auth: Authorization: Bearer $GCP_TOKEN. Get a token with gcloud auth application-default print-access-token; it lasts about an hour. Online requests accept at most 15 pages, so split longer PDFs first.
Process a document (OCR)
POST /v1/projects/{project}/locations/{location}/processors/{processor}:process
Returns the full text and one entry per page. A page's text is the slice of document.text named by its textAnchor.textSegments (indexes are int64 values, returned as strings).
| Field | Type | Required | Description |
|---|---|---|---|
project, location, processor | string | yes | Your Google Cloud project id, the processor location (for example us) and the processor id. |
rawDocument.content | string | yes | The file, base64 encoded (see below). |
rawDocument.mimeType | string | yes | application/pdf, image/png, image/jpeg, image/tiff, image/gif, image/bmp or image/webp. |
processOptions.ocrConfig.enableImageQualityScores | boolean | no | Adds imageQualityScores.qualityScore per page. |
curl -X POST "https://us-documentai.googleapis.com/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6:process" \
-H "Authorization: Bearer $GCP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rawDocument": {
"content": "<BASE64_FILE_CONTENT>",
"mimeType": "application/pdf"
},
"processOptions": {
"ocrConfig": {
"enableImageQualityScores": true
}
}
}'import os, requests
BASE = "https://us-documentai.googleapis.com"
r = requests.post(
f"{BASE}/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6:process",
json={'rawDocument': {'content': '<BASE64_FILE_CONTENT>',
'mimeType': 'application/pdf'},
'processOptions': {'ocrConfig': {'enableImageQualityScores': True}}},
headers={"Authorization": f"Bearer {os.environ['GCP_TOKEN']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://us-documentai.googleapis.com";
const res = await fetch(`${BASE}/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6:process`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.GCP_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"rawDocument": {
"content": "<BASE64_FILE_CONTENT>",
"mimeType": "application/pdf"
},
"processOptions": {
"ocrConfig": {
"enableImageQualityScores": true
}
}
})
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://us-documentai.googleapis.com";
$ch = curl_init("$base/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6:process");
$opts = [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("GCP_TOKEN"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"rawDocument" => [
"content" => "<BASE64_FILE_CONTENT>",
"mimeType" => "application/pdf",
],
"processOptions" => [
"ocrConfig" => [
"enableImageQualityScores" => true,
],
],
]),
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
base := "https://us-documentai.googleapis.com"
body := `{
"rawDocument": {
"content": "<BASE64_FILE_CONTENT>",
"mimeType": "application/pdf"
},
"processOptions": {
"ocrConfig": {
"enableImageQualityScores": true
}
}
}`
req, _ := http.NewRequest("POST", base+"/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6:process", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("GCP_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"document": {
"mimeType": "application/pdf",
"text": "Final dividend notice ...",
"pages": [
{
"pageNumber": 1,
"layout": {
"confidence": 0.98,
"textAnchor": {
"textSegments": [
{
"startIndex": "0",
"endIndex": "812"
}
]
}
},
"imageQualityScores": {
"qualityScore": 0.93
}
}
]
}
}Get processor
GET /v1/projects/{project}/locations/{location}/processors/{processor}
Quick way to confirm the project, location, processor id and token are right before sending a document. state must be ENABLED.
| Field | Type | Required | Description |
|---|---|---|---|
project | string | yes | Path parameter. |
location | string | yes | Path parameter. |
processor | string | yes | Path parameter. |
curl -X GET "https://us-documentai.googleapis.com/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6" \
-H "Authorization: Bearer $GCP_TOKEN"import os, requests
BASE = "https://us-documentai.googleapis.com"
r = requests.get(
f"{BASE}/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6",
headers={"Authorization": f"Bearer {os.environ['GCP_TOKEN']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://us-documentai.googleapis.com";
const res = await fetch(`${BASE}/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6`, {
method: "GET",
headers: { Authorization: `Bearer ${process.env.GCP_TOKEN}` }
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://us-documentai.googleapis.com";
$ch = curl_init("$base/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6");
$opts = [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("GCP_TOKEN")],
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://us-documentai.googleapis.com"
req, _ := http.NewRequest("GET", base+"/v1/projects/my-gcp-project/locations/us/processors/a1b2c3d4e5f6", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("GCP_TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"name": "projects/123456789/locations/us/processors/a1b2c3d4e5f6",
"type": "OCR_PROCESSOR",
"displayName": "idp-ocr",
"state": "ENABLED"
}xAI Grok
Chat completions with structured JSON output. The API is OpenAI compatible. Base URL https://api.x.ai/v1. Auth: Authorization: Bearer $XAI_API_KEY.
List models
GET /models
Models your key can use. Pick the id to send as model.
curl -X GET "https://api.x.ai/v1/models" \
-H "Authorization: Bearer $XAI_API_KEY"import os, requests
BASE = "https://api.x.ai/v1"
r = requests.get(
f"{BASE}/models",
headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://api.x.ai/v1";
const res = await fetch(`${BASE}/models`, {
method: "GET",
headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}` }
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://api.x.ai/v1";
$ch = curl_init("$base/models");
$opts = [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("XAI_API_KEY")],
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://api.x.ai/v1"
req, _ := http.NewRequest("GET", base+"/models", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("XAI_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"data": [
{
"id": "grok-4.3",
"object": "model",
"owned_by": "xai"
}
]
}Chat completion with structured output
POST /chat/completions
The reply text is a JSON string in choices[0].message.content. Parse it. The REST call below is the same request the OpenAI SDK sends for beta.chat.completions.parse.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | Model id, for example grok-4.3. |
messages | object[] | yes | System instructions, then the document text. |
response_format | object | no | json_schema forces the reply to match your schema. Set strict to true and list every property in required. Value rules such as ranges or patterns are not enforced by the schema, so check them in your own code. |
curl -X POST "https://api.x.ai/v1/chat/completions" \
-H "Authorization: Bearer $XAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "grok-4.3",
"messages": [
{
"role": "system",
"content": "Classify the document and extract the requested fields. Use null when a value is absent."
},
{
"role": "user",
"content": "Notice is hereby given of a final dividend of KES 1.50 per share ..."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"document_type": {
"type": "string"
},
"summary": {
"type": "string"
},
"dividend_per_share": {
"type": [
"number",
"null"
]
}
},
"required": [
"document_type",
"summary",
"dividend_per_share"
],
"additionalProperties": false
}
}
}
}'import os, requests
BASE = "https://api.x.ai/v1"
r = requests.post(
f"{BASE}/chat/completions",
json={'model': 'grok-4.3',
'messages': [{'role': 'system',
'content': 'Classify the document and extract the '
'requested fields. Use null when a value '
'is absent.'},
{'role': 'user',
'content': 'Notice is hereby given of a final '
'dividend of KES 1.50 per share ...'}],
'response_format': {'type': 'json_schema',
'json_schema': {'name': 'extraction',
'strict': True,
'schema': {'type': 'object',
'properties': {'document_type': {'type': 'string'},
'summary': {'type': 'string'},
'dividend_per_share': {'type': ['number',
'null']}},
'required': ['document_type',
'summary',
'dividend_per_share'],
'additionalProperties': False}}}},
headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
timeout=60,
)
r.raise_for_status()
print(r.json())
const BASE = "https://api.x.ai/v1";
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"model": "grok-4.3",
"messages": [
{
"role": "system",
"content": "Classify the document and extract the requested fields. Use null when a value is absent."
},
{
"role": "user",
"content": "Notice is hereby given of a final dividend of KES 1.50 per share ..."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"document_type": {
"type": "string"
},
"summary": {
"type": "string"
},
"dividend_per_share": {
"type": [
"number",
"null"
]
}
},
"required": [
"document_type",
"summary",
"dividend_per_share"
],
"additionalProperties": false
}
}
}
})
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());
<?php
$base = "https://api.x.ai/v1";
$ch = curl_init("$base/chat/completions");
$opts = [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("XAI_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"model" => "grok-4.3",
"messages" => [[
"role" => "system",
"content" => "Classify the document and extract the requested fields. Use null when a value is absent.",
], [
"role" => "user",
"content" => "Notice is hereby given of a final dividend of KES 1.50 per share ...",
]],
"response_format" => [
"type" => "json_schema",
"json_schema" => [
"name" => "extraction",
"strict" => true,
"schema" => [
"type" => "object",
"properties" => [
"document_type" => [
"type" => "string",
],
"summary" => [
"type" => "string",
],
"dividend_per_share" => [
"type" => ["number", "null"],
],
],
"required" => ["document_type", "summary", "dividend_per_share"],
"additionalProperties" => false,
],
],
],
]),
];
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) { die("Request failed: " . ($response ?: curl_error($ch))); }
print_r(json_decode($response, true));
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
base := "https://api.x.ai/v1"
body := `{
"model": "grok-4.3",
"messages": [
{
"role": "system",
"content": "Classify the document and extract the requested fields. Use null when a value is absent."
},
{
"role": "user",
"content": "Notice is hereby given of a final dividend of KES 1.50 per share ..."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"document_type": {
"type": "string"
},
"summary": {
"type": "string"
},
"dividend_per_share": {
"type": [
"number",
"null"
]
}
},
"required": [
"document_type",
"summary",
"dividend_per_share"
],
"additionalProperties": false
}
}
}
}`
req, _ := http.NewRequest("POST", base+"/chat/completions", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("XAI_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Response 200
{
"id": "b2c1a7f0-...",
"object": "chat.completion",
"model": "grok-4.3",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "{\"document_type\":\"dividend_announcement\",\"summary\":\"Final dividend of KES 1.50 per share.\",\"dividend_per_share\":1.5}"
}
}
],
"usage": {
"prompt_tokens": 412,
"completion_tokens": 58,
"total_tokens": 470
}
}Grok with the OpenAI SDK
The xAI API is OpenAI compatible, so the OpenAI Python SDK works with a different base URL. This returns a parsed Pydantic object.
import os
from openai import OpenAI
from pydantic import BaseModel
class Extraction(BaseModel):
document_type: str
summary: str
dividend_per_share: float | None
client = OpenAI(api_key=os.environ["XAI_API_KEY"], base_url="https://api.x.ai/v1")
r = client.beta.chat.completions.parse(
model="grok-4.3",
response_format=Extraction,
messages=[{"role": "system", "content": "Classify the document and extract the requested fields."},
{"role": "user", "content": "Notice is hereby given of a final dividend of KES 1.50 per share ..."}],
)
print(r.choices[0].message.parsed)
Postman collections
Import one collection per vendor from the postman/ folder: apify.postman_collection.json, google_document_ai.postman_collection.json and xai_grok.postman_collection.json. Set the token variable at collection level (apifyToken, gcpToken, xaiApiKey). The Document AI one also needs project, location and processorId.