# Fieldloop Batches and Receipts
Generated: 2026-11-02T06:34:27.525Z
Source build: local
Canonical docs: https://teammately.ai/docs
---
id: public-api.fieldloop
title: Fieldloop Batches and Receipts
summary: Submit captured logs, preserve retry identity and inspect admission and row processing separately.
kind: api
product_area: public_api
status: stable
updated: 2026-10-31
canonical: /docs/public-api/fieldloop
---
# Fieldloop Batches and Receipts
## Definition
Submit captured application records to an existing source with a Fieldloop Submission key from **Admin → API Keys**. New keys carry that capability across the workspace. Configure the intended Project/source; older key restrictions and current permissions remain enforced. Receipt reads belong to the original submitter.
## Submit a batch
```http
POST /v1/organizations/{organizationKey}/projects/{projectId}/fieldloop/sources/{sourceId}/batches
Authorization: Bearer
Content-Type: application/json
Idempotency-Key:
```
### Example: Captured log body
```json
{
"logs": [{
"source_event_id": "support-ticket-1042:reply",
"occurred_at": "2026-10-31T09:30:00Z",
"input": {"question": "Has my bicycle shipped?"},
"output": {"reply": "Your bicycle is being packed."},
"context": {"application": "Bicycle Shop"}
}]
}
```
Replace the fictional record with selected, redacted data in `batch.json`. Your application owns `source_event_id`. Each log needs RFC3339 `occurred_at` and at least one of `input`, `output`, `messages`, `trajectory` or `action`. Optional feedback is nested in its log; source relationships use `provider_ref`, `session_ref` and `trace_ref`.
### Example: Submit a batch
```sh
SOURCE_URL="${PROJECT_URL}/fieldloop/sources/${TEAMMATELY_FIELDLOOP_SOURCE}"
curl --fail-with-body \
--header "Authorization: Bearer ${TEAMMATELY_API_KEY}" \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: ${BATCH_OPERATION_KEY}" \
--data-binary @batch.json \
"${SOURCE_URL}/batches"
```
```python
import os
from pathlib import Path
from urllib.parse import quote
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = build_opener(NoRedirect())
api_key = os.environ["TEAMMATELY_API_KEY"]
url = os.environ["PROJECT_URL"] + "/fieldloop/sources/" + quote(os.environ["TEAMMATELY_FIELDLOOP_SOURCE"], safe="") + "/batches"
headers = {"Authorization": "Bearer " + api_key}
headers["Idempotency-Key"] = os.environ["BATCH_OPERATION_KEY"]
headers["Content-Type"] = "application/json"
body = Path("batch.json").read_bytes()
request = Request(url, data=body, headers=headers, method="POST")
with opener.open(request) as response:
print(response.read().decode("utf-8"))
```
```typescript
import { readFile } from "node:fs/promises";
const apiKey = process.env.TEAMMATELY_API_KEY!;
{
const url = new URL(process.env.PROJECT_URL! + "/fieldloop/sources/" + encodeURIComponent(process.env.TEAMMATELY_FIELDLOOP_SOURCE!) + "/batches");
const response = await fetch(url, {
method: "POST",
redirect: "error",
headers: {
Authorization: `Bearer ${apiKey}`,
"Idempotency-Key": process.env.BATCH_OPERATION_KEY!,
"Content-Type": "application/json",
},
body: new Uint8Array(await readFile("batch.json")).buffer,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.text());
}
```
```go
package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
)
func main() {
if err := run(context.Background()); err != nil { log.Fatal(err) }
}
func run(ctx context.Context) error {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
{
target := os.Getenv("PROJECT_URL") + "/fieldloop/sources/" + url.PathEscape(os.Getenv("TEAMMATELY_FIELDLOOP_SOURCE")) + "/batches"
payload, err := os.ReadFile("batch.json")
if err != nil { return err }
request, err := http.NewRequestWithContext(ctx, "POST", target, bytes.NewReader(payload))
if err != nil { return err }
request.Header.Set("Authorization", "Bearer " + os.Getenv("TEAMMATELY_API_KEY"))
request.Header.Set("Idempotency-Key", os.Getenv("BATCH_OPERATION_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil { return err }
payloadBytes, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil { return readErr }
fmt.Println(string(payloadBytes))
if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("HTTP %d", response.StatusCode) }
}
return nil
}
```
Use the Project URL from the [quickstart](/docs/public-api/quickstart), an existing issued source ID and one logical batch key. Preserve the exact file/key for uncertain retries.
## Read the receipt
`202` acknowledges durable receipt of the bytes. Retain `data.batch_id`:
### Example: Read a receipt
```sh
curl --fail-with-body \
--header "Authorization: Bearer ${TEAMMATELY_API_KEY}" \
"${SOURCE_URL}/batches/${BATCH_ID}"
```
```python
import os
from urllib.parse import quote
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = build_opener(NoRedirect())
api_key = os.environ["TEAMMATELY_API_KEY"]
url = os.environ["PROJECT_URL"] + "/fieldloop/sources/" + quote(os.environ["TEAMMATELY_FIELDLOOP_SOURCE"], safe="") + "/batches/" + quote(os.environ["BATCH_ID"], safe="")
headers = {"Authorization": "Bearer " + api_key}
request = Request(url, headers=headers, method="GET")
with opener.open(request) as response:
print(response.read().decode("utf-8"))
```
```typescript
const apiKey = process.env.TEAMMATELY_API_KEY!;
{
const url = new URL(process.env.PROJECT_URL! + "/fieldloop/sources/" + encodeURIComponent(process.env.TEAMMATELY_FIELDLOOP_SOURCE!) + "/batches/" + encodeURIComponent(process.env.BATCH_ID!));
const response = await fetch(url, {
method: "GET",
redirect: "error",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.text());
}
```
```go
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
)
func main() {
if err := run(context.Background()); err != nil { log.Fatal(err) }
}
func run(ctx context.Context) error {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
{
target := os.Getenv("PROJECT_URL") + "/fieldloop/sources/" + url.PathEscape(os.Getenv("TEAMMATELY_FIELDLOOP_SOURCE")) + "/batches/" + url.PathEscape(os.Getenv("BATCH_ID"))
request, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil { return err }
request.Header.Set("Authorization", "Bearer " + os.Getenv("TEAMMATELY_API_KEY"))
response, err := client.Do(request)
if err != nil { return err }
payloadBytes, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil { return readErr }
fmt.Println(string(payloadBytes))
if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("HTTP %d", response.StatusCode) }
}
return nil
}
```
| Field | Interpretation |
| --- | --- |
| `batch_id`, `source_id`, `sequence` | Issued identity and destination order |
| `status` | `uploading`, `received`, `processing`, `processed`, `failed`, `cancelled` or `deleted` |
| `row_count` | Submitted population |
| `accepted_count`, `duplicate_count` | Accepted records and recognized duplicates |
| `conflict_count`, `rejected_count` | Rows requiring inspection |
| `rows`, `safe_error_code` | Per-row outcomes and error code when returned |
`processed` can include rejected/conflicting rows. Polling reads an existing batch rather than submitting another one.
## Recovery and inspection
Replay a lost response with identical bytes/key. Retain source records until admission is known. Inspect **Fieldloop → Logs** for original content and processing state. Source consent, Threads and Case creation are separate workflows.
[SDK guides](/docs/sdks) cover continuous ingestion. `FieldloopBatch`, `FieldloopLog` and `FieldloopReceiptEnvelope` in the [OpenAPI](https://api.teammately.ai/v1/openapi.yaml) define request/receipt fields.
## Related reference pages
{% related-card-grid %}
- [API keys](/docs/admin-console/api-keys)
- [Fieldloop troubleshooting](/docs/troubleshooting/fieldloop)
{% /related-card-grid %}
---
id: fieldloop.ingest
title: Capture and Inspect Logs
summary: Configure a source destination, submit captured application records, and distinguish durable admission from processing completion.
kind: task
product_area: fieldloop
status: stable
updated: 2026-10-30
canonical: /docs/fieldloop/ingest-logs
---
# Capture and Inspect Logs
Capture the application facts that explain a behavior question. Keep the original records until their admission is known.
## Prerequisites
- The intended workspace, Project and Fieldloop source.
- Authorization to submit to that destination.
- Actual captured content and stable application event identities.
- Explicit source processing consent where screening is enabled.
## Submit captured records
Use the source's supported submission path with the configured destination. Application-library calls can submit batches and inspect receipts without installing a CLI or signing in through a browser.
Follow the [Python](/docs/sdks/python), [TypeScript](/docs/sdks/typescript) or [Go](/docs/sdks/go) quickstart for a complete batch example, or use the [Fieldloop REST guide](/docs/public-api/fieldloop).
Provide a Fieldloop submission credential through your application's approved secret mechanism. A newly created Fieldloop key grants that capability across its workspace; the sender still identifies the Project and source. Existing more restricted keys keep their restrictions.
Preserve the exact batch content and idempotency key when recovering an uncertain request. A successful admission does not mean every row has completed processing.
## Inspect Logs
Open Logs, choose the intended time range and source, then inspect the original record, relationships and processing state. Captured messages, context, trajectories and feedback remain distinct fields rather than a synthesized replacement for what happened.
Read receipt counts for accepted, duplicate, conflicting and rejected rows. A processed batch can still contain row-level outcomes that need attention.
## Object and state changes
Submission records durable intake and admits processing work. Processing publishes the captured log and its attributable snapshot. Further analysis can create or update Threads separately.
SDK buffering can acknowledge memory before server admission. It is not a disk outbox or a guarantee of crash delivery. Caller-owned cancellation and recovery should preserve the exact source records and batch identity.
## Success criteria
- Records reached the intended Project and source.
- Original content and event identities are preserved.
- Admission and processing outcomes are understood separately.
- Credentials do not appear in logs, reports or copied tasks.
## Common failure modes
- Using a key for another workspace.
- Re-encoding changed content under the same retry identity.
- Treating receipt polling as a new submission.
- Assuming a captured record was analyzed without source consent.
- Treating temporary client buffering as durable admission.
## Related reference pages
{% related-card-grid title="Related reference pages" %}
- [Fieldloop](/docs/fieldloop)
- [Read Fieldloop Threads](/docs/fieldloop/threads)
- [API Keys and Delegated Access](/docs/admin-console/api-keys)
{% /related-card-grid %}
## Related troubleshooting pages
{% related-card-grid title="Related troubleshooting pages" %}
- [Fieldloop Intake and Processing](/docs/troubleshooting/fieldloop)
- [Permissions troubleshooting](/docs/troubleshooting/permissions)
{% /related-card-grid %}
---
id: public-api.authentication
title: API Authentication
summary: Distinguish API-key capability, delegated External Agents login, current destination access, and browser authentication.
kind: api
product_area: public_api
status: stable
updated: 2026-10-31
canonical: /docs/public-api/authentication
---
# API Authentication
## Definition
The gateway accepts API keys and delegated native-login access tokens on their supported routes. Browser session cookies remain the interactive app path; these credential types are not interchangeable.
## API keys
Keys belong to a workspace. Base capability can be read-only, full access or Fieldloop submission. Project restrictions, allowed actions and expiry narrow that capability when supplied.
A new Fieldloop submission key carries that workspace capability. The caller still supplies the Project and source destination, and current access is checked at intake. Previously issued restricted keys retain their restrictions.
Changing access requires issuing the intended credential; renaming a key does not broaden it. Never place values in URLs, source, logs or copied agent tasks.
## Delegated login
Native login is scoped to supported External Agents benchmark routes and organization login status. Delegation uses `external-agents:read` and `external-agents:write`, plus current project read/manage authorization. It does not provision a user, select a fallback organization or grant every endpoint.
The native client uses authorization-code login with PKCE and an OS credential store. Auth discovery/session routes are distinct from customer operations.
## Request headers
Supply the credential as a bearer token. `TEAMMATELY_API_KEY` is injected by your secret mechanism; configure `PROJECT_URL` with the [quickstart](/docs/public-api/quickstart).
### Example: Read a Case page
```sh
curl --fail-with-body \
--header "Authorization: Bearer ${TEAMMATELY_API_KEY}" \
"${PROJECT_URL}/cases?limit=1&offset=0"
```
```python
import os
from urllib.parse import quote, urlencode
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = build_opener(NoRedirect())
api_key = os.environ["TEAMMATELY_API_KEY"]
url = os.environ["PROJECT_URL"] + "/cases"
url += "?" + urlencode({"limit": "1", "offset": "0"})
headers = {"Authorization": "Bearer " + api_key}
request = Request(url, headers=headers, method="GET")
with opener.open(request) as response:
print(response.read().decode("utf-8"))
```
```typescript
const apiKey = process.env.TEAMMATELY_API_KEY!;
{
const url = new URL(process.env.PROJECT_URL! + "/cases");
url.searchParams.set("limit", "1");
url.searchParams.set("offset", "0");
const response = await fetch(url, {
method: "GET",
redirect: "error",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.text());
}
```
```go
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
)
func main() {
if err := run(context.Background()); err != nil { log.Fatal(err) }
}
func run(ctx context.Context) error {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
{
target := os.Getenv("PROJECT_URL") + "/cases"
query := url.Values{}
query.Set("limit", "1")
query.Set("offset", "0")
target += "?" + query.Encode()
request, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil { return err }
request.Header.Set("Authorization", "Bearer " + os.Getenv("TEAMMATELY_API_KEY"))
response, err := client.Do(request)
if err != nil { return err }
payloadBytes, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil { return readErr }
fmt.Println(string(payloadBytes))
if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("HTTP %d", response.StatusCode) }
}
return nil
}
```
## Choose the credential
| Operation | Credential boundary |
| --- | --- |
| Case reads | API key with Project read access |
| Case imports/reference writes | API key with the required write access |
| Fieldloop submission/receipts | Fieldloop capability, destination permission and original-submitter receipt ownership |
| External Agents reads | Authorized API key, or delegated `external-agents:read` plus Project read access |
| External Agents writes | Authorized API key, or delegated `external-agents:write` plus Project management access |
For delegated terminal access, follow [CLI Authentication](/docs/cli/authentication). Application ingestion uses an API key without browser login. A read-only MCP tool does not narrow the credential's permission elsewhere.
## Access failures
For `401`, check the credential's presence, validity and API origin. For `403`, check the workspace, Project, capability, restrictions and delegated scope. Organization IP restrictions can apply. Correct access through normal administration.
## Related reference pages
{% related-card-grid title="Related reference pages" %}
- [Public API](/docs/public-api)
- [API Conventions](/docs/public-api/conventions)
- [API Keys and Delegated Access](/docs/admin-console/api-keys)
{% /related-card-grid %}
---
id: public-api.conventions
title: API Conventions
summary: Use organization-scoped paths, operation-specific envelopes, issued IDs, exact retry identity, and explicit completion.
kind: api
product_area: public_api
status: stable
updated: 2026-10-31
canonical: /docs/public-api/conventions
---
# API Conventions
## Definition
The resource prefix is `/v1/organizations/{organizationKey}/projects/{projectId}`. Benchmark operations continue beneath the selected Benchmark and immutable Version. Paths and IDs are scoped; a readable title is not a join key.
## Envelopes and pagination
Case lists return `message` and `data: {columns, rows}`; detail returns `message` and `result`. Fieldloop and benchmark operations use their documented `data` payloads. Parse the called operation's envelope.
Case reads use limit/offset pagination, defaulting to 10 rows and accepting a positive limit through 1,000. Other readers have their own pagination. Preserve returned continuation information instead of assuming a cursor shape universally.
Case reads emit ETag and support If-None-Match. Concurrency and mutation preconditions belong to the particular operation.
## Writes and retries
Use an Idempotency-Key where the operation requires it. Preserve exact request content and logical identity, but follow that operation's recovery contract. Changed content is a different command.
External results append only to the issued Trial. Completion freezes accepted responses and admits evaluation separately. A failed admission retry does not replace those responses.
## IDs and errors
Use database/API-issued opaque IDs. Example placeholders are not usable IDs. Keep the original HTTP status, structured code and actionable reason; no global error envelope is promised across legacy and forwarded families.
## HTTP outcomes
| Outcome | Next step |
| --- | --- |
| `200` or `201` | Inspect the resource or per-item outcomes |
| `202` | Retain the issued identity and inspect processing state |
| `304` | Use the representation associated with the matching ETag |
| `400` or `422` | Correct the request using its code and reason |
| `401` or `403` | Correct credentials or scope |
| `409` or `412` | Inspect immutable state, idempotency or preconditions |
| `429` | Honor `Retry-After` when supplied |
| `502`, `503` or `504` | Preserve command identity before recovering an uncertain write |
Not every endpoint emits every status. The [OpenAPI](https://api.teammately.ai/v1/openapi.yaml) lists operation-specific responses; one rate-limit response does not define a universal quota.
## Recover without duplicating work
Fieldloop and benchmark submission workflows support their documented exact-content recovery. Preserve original bytes/key and inspect the returned receipt. A client timeout does not undo admitted work.
Case imports and batch Case reference writes use duplicate-key rejection: repeating a key returns `409`, not a replay of the original response. Retain the issued import identity when available and read its status. If a response is lost before its identity is known, reconcile through your administrator or support before submitting with a new key; a fresh key can create another operation. Do not assume duplicate rejection proves whether the first handler completed.
See [SDK Delivery and Recovery](/docs/sdks/delivery-and-recovery) for the Fieldloop client path.
## Related reference pages
{% related-card-grid title="Related reference pages" %}
- [Public API](/docs/public-api)
- [API Authentication](/docs/public-api/authentication)
- [Submit External Evaluation Outputs](/docs/public-api/evaluation-outputs)
{% /related-card-grid %}