---
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 <credential from your secret mechanism>
Content-Type: application/json
Idempotency-Key: <application-owned logical batch 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 %}
