Collect API Reference
Submit test execution data directly to Obvyr via the /collect endpoint. This is the underlying API used by both the CLI and the Gradle plugin — use it directly to build custom integrations or to support stacks not yet covered by an Obvyr helper.
Prefer a helper?
For most stacks the Obvyr CLI is the easiest integration path. For JVM/Gradle projects, the Gradle plugin requires no wrapper at all. Use this reference when you need a custom integration.
Endpoint
POST https://api.obvyr.com/collectAuthentication
All requests must include an agent token in the Authorization header:
Authorization: Bearer <agt_token>Agent tokens are created in the Obvyr dashboard under your project's agent settings. Tokens are scoped to a single agent and carry the account_id, project_id, and agent_id claims used to route the observation.
Keep your token safe
Treat agent tokens like passwords. Store them as CI secrets or environment variables — never commit them to source control.
Request Format
The endpoint accepts multipart/form-data with a single field:
| Field | Type | Required | Description |
|---|---|---|---|
archive | file | Yes | A zstd-compressed tar archive (.tar.zst) |
Archive Structure
artifacts.tar.zst
├── command.json ← required
├── output.txt ← optional
└── attachment/
├── junit.xml ← optional
└── <filename> ← optional (text-based files only)command.json Schema
The archive must contain a UTF-8 encoded command.json file at the root.
Required fields:
| Field | Type | Description |
|---|---|---|
command | string[] | The command that was executed, as an array of strings (e.g. ["pytest", "-q", "tests/"]) |
user | string | Execution context identifier (e.g. "local-dev", "github-ci") |
return_code | integer | Exit code from the command |
execution_time_ms | integer | Total execution duration in milliseconds |
Optional fields:
| Field | Type | Description |
|---|---|---|
executed | string | ISO 8601 datetime when the command was executed |
env | object | Environment variables at execution time (arbitrary key-value pairs) |
tags | string[] | Labels for filtering and grouping observations |
Example command.json:
json
{
"command": ["pytest", "--junitxml=test-results/junit.xml", "tests/"],
"user": "github-ci",
"return_code": 0,
"execution_time_ms": 4312,
"executed": "2026-04-15T09:23:41.000Z",
"env": {
"CI": "true",
"GITHUB_REF": "refs/heads/main"
},
"tags": ["ci", "unit"]
}user is context, not identity
Use user to describe what is running the command — not who. Values like local-dev, github-ci, or jenkins-prod enable pattern analysis without collecting personal data.
output.txt
Include output.txt to capture the command's stdout/stderr. Obvyr uses this to surface failure output alongside the observation. UTF-8 encoding required.
Attachments
Place additional files under an attachment/ directory in the archive. Only text-based file types are accepted:
- Formats:
xml,json,yaml,yml,csv,txt,html,htm,log - Size limits: 5 MB per file, 10 MB total across all attachments
JUnit XML placed here is parsed for test-level insights (individual pass rates, flaky test detection, execution times).
Response
A successful 200 OK response returns JSON:
json
{
"observation_id": "2026-04-15T09:23:41.000000_a1b2c3d4-...",
"account_id": "11111111-1111-1111-1111-111111111111",
"project_id": "22222222-2222-2222-2222-222222222222",
"agent_id": "33333333-3333-3333-3333-333333333333",
"status": "pending"
}| Field | Type | Description |
|---|---|---|
observation_id | string | Unique observation identifier (datetime + UUID) |
account_id | UUID | Account the observation belongs to |
project_id | UUID | Project the observation belongs to |
agent_id | UUID | Agent the observation was submitted to |
status | string | Processing status — "pending" on initial submission |
Rate Limits
Rate limits are enforced per account (not per agent or project), keyed to your subscription tier:
| Tier | Limit |
|---|---|
| Free | 100 / hour |
| Starter | 500 / hour |
| Team | 2 000 / hour |
| Business | 10 000 / hour |
| Enterprise | 100 000 / hour |
When the limit is exceeded the endpoint returns 429 Too Many Requests. The response includes standard rate-limit headers indicating when the window resets.
Error Codes
| Status | Condition |
|---|---|
400 | Invalid archive format, malformed command.json, or attachment governance violation (disallowed file type or size exceeded) |
402 | Data collection disabled for this account, or observation limit reached for your plan |
415 | Archive is not a valid zstd-compressed file |
422 | Missing required field(s) — check command.json for command, user, return_code, execution_time_ms |
429 | Rate limit exceeded for your account's tier |
503 | Storage service temporarily unavailable — retry with exponential back-off |
Example: Building an Archive
The archive must be a zstd-compressed tar. Here is a minimal shell example:
bash
# Create archive directory
mkdir -p obs/attachment
# Write command.json
cat > obs/command.json <<'EOF'
{
"command": ["npm", "test"],
"user": "github-ci",
"return_code": 0,
"execution_time_ms": 8100,
"tags": ["ci", "unit"]
}
EOF
# Copy output and attachments
cp test-output.txt obs/output.txt
cp test-results/junit.xml obs/attachment/junit.xml
# Compress with zstd
tar --zstd -cf artifacts.tar.zst -C obs .
# Submit
curl -X POST https://api.obvyr.com/collect \
-H "Authorization: Bearer $OBVYR_API_KEY" \
-F "archive=@artifacts.tar.zst"Use a helper instead
The CLI and Gradle plugin handle archive construction, compression, and submission automatically. Roll your own only when a helper is not available for your stack.