Sabalink Technologies

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.

Samples read credentials from environment variables (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).

FieldTypeRequiredDescription
actorstringyesActor id with ~ in place of /: apify~website-content-crawler.
bodyobjectyesThe 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.

FieldTypeRequiredDescription
runIdstringyesPath 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:

FieldTypeRequiredDescription
datasetIdstringyesPath parameter.
FieldTypeRequiredDescription
formatstringnojson returns an array. Also csv, xlsx, jsonl.
cleanbooleannoDrops hidden and empty fields.
limitintegernoMaximum 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.

ValueHow it loads pagesRuns JavaScriptSpeed
cheerioRaw HTTP request, then parses the returned HTML. No browser.NoFastest.
playwright:adaptive (default)Starts with raw HTTP and switches to a browser for pages that need it.Only when neededFast on simple pages, slower on JavaScript pages. Apify recommends it as the default.
playwright:firefoxAlways a headless Firefox browser.YesSlowest. Best at avoiding blocking.
jsdom, playwright:chromeOlder options.n/aDeprecated. Do not use for new work.

Confirm the value names on the actor's input page before relying on them.

Which one to use

ScenarioUseWhy
Regulator, issuer or news sites with ordinary pages and links to PDF files (notices, circulars, annual reports)cheerioThe text is already in the HTML. This is the fastest option.
You have not inspected the site, or a source mixes simple and dynamic pagesplaywright:adaptiveIt 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" buttonsplaywright:adaptive, or playwright:firefox if adaptive still misses contentcheerio would return an empty or partial page.
The site blocks you: HTTP 403 or 429, captcha pages, "access denied" text in the resultsplaywright:firefox, with residential proxies in the actor's proxy settingsA real browser looks like a normal visitor. Proxies help when the site blocks the crawler's IP address.
Large crawl where speed matterscheerio, after a small testBrowser crawling is much slower per page.

How to decide

  1. Start a test run with cheerio and maxCrawlPages set to 5.
  2. Read the dataset items. If text holds the real page content, keep cheerio.
  3. If text is empty, very short, or only menu text, switch to playwright:adaptive and test again.
  4. If you now see blocking (403, 429, captcha), switch to playwright:firefox and add residential proxies.
  5. Raise maxCrawlPages only 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

FieldWhat it does
maxCrawlPagesStops after this many pages. Counts start URLs and pagination pages. The default is effectively unlimited, so always set it.
maxCrawlDepthHow many links deep to follow from a start URL. Start URLs are depth 0, so 0 crawls only the start URLs.
includeUrlGlobsExtra URL patterns the crawler may follow, beyond the start URLs and their subpages.
excludeUrlGlobsURL patterns to skip. Start URLs are always crawled.
maxResultsCaps stored pages and files. Downloaded files count toward it.
saveMarkdownAlso returns each page as Markdown in the markdown field.
saveContentTypesDownloads 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).

FieldTypeRequiredDescription
project, location, processorstringyesYour Google Cloud project id, the processor location (for example us) and the processor id.
rawDocument.contentstringyesThe file, base64 encoded (see below).
rawDocument.mimeTypestringyesapplication/pdf, image/png, image/jpeg, image/tiff, image/gif, image/bmp or image/webp.
processOptions.ocrConfig.enableImageQualityScoresbooleannoAdds 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.

FieldTypeRequiredDescription
projectstringyesPath parameter.
locationstringyesPath parameter.
processorstringyesPath 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.

FieldTypeRequiredDescription
modelstringyesModel id, for example grok-4.3.
messagesobject[]yesSystem instructions, then the document text.
response_formatobjectnojson_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.