> ## Documentation Index
> Fetch the complete documentation index at: https://x-preview-mintlify-6d8eadf6.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration guide

> Key concepts, URL lifecycle, error handling, and best practices for integrating the X API v2 Batch Compliance endpoints.

export const Button = ({href, children}) => {
  return <div className="not-prose">
    <a href={href}>
      <button className="x-btn">
        <span>{children}</span>
        <svg width="3" height="24" viewBox="0 -9 3 24" class="h-6 rotate-0 overflow-visible"><path d="M0 0L3 3L0 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>
      </button>
    </a>
  </div>;
};

The Batch Compliance endpoints let you upload large datasets of Post IDs or
user IDs and learn what action is needed to keep your datasets aligned with
user intent and the current state of content on X. This guide covers the
concepts the [quickstart](/x-api/compliance/batch-compliance/quickstart)
glosses over: URL lifecycle, job lifecycle, error handling, and operational
best practices.

## Signed URLs

Creating a job returns two signed, single-purpose URLs on `api.x.com`:

| URL            | Method | Purpose                          | Lifetime                       |
| :------------- | :----- | :------------------------------- | :----------------------------- |
| `upload_url`   | PUT    | Submit your ID file for this job | \~15 minutes from job creation |
| `download_url` | GET    | Retrieve this job's results      | \~7 days from job creation     |

Each URL embeds a token that binds it to exactly one operation on exactly one
job until its expiry. Requests to these URLs must also carry your
`Authorization: Bearer` header — the same App credential that created the job.
This differs from the previous Google Cloud Storage-based flow, where
pre-signed URLs were used without an Authorization header; if you are
migrating an existing integration, add the header to your upload and download
requests. Still treat the URLs as secrets: do not log them or embed them in
client-side code.

The exact expiry instants are returned as `upload_expires_at` and
`download_expires_at` on the job object. If the upload window lapses before
you use it, cancel the job (`DELETE /2/compliance/jobs/:id`) and create a
new one — upload URLs are not refreshable.

## Job lifecycle

```
created ──upload──▶ in_progress ──processing──▶ complete
   │                     │                          │
   └──── cancel ─────────┘                      download
   (or expiry)                                  (~7 days)
```

* **One active job per type.** While a `tweets` job is `created` or
  `in_progress`, creating another `tweets` job returns `409 Conflict`. A
  `users` job can run concurrently with a `tweets` job.
* **Processing is asynchronous.** Poll `GET /2/compliance/jobs/:id` with
  backoff (e.g. every 30–60 seconds). Processing time scales with dataset
  size.
* **Jobs expire.** A job left in `created` without an upload expires. Jobs
  can also end `failed`; both free the active-job slot.

## Uploads

The upload is a single `PUT` of a plain-text body, one numeric ID per line,
up to 1 GiB. There is no resumable/chunked mode: if a transfer is
interrupted, retry the whole PUT while the upload URL is valid, or cancel
the job and start over. For very large datasets, split IDs across multiple
sequential jobs — results are per-ID, so partitioning is safe.

Invalid lines (non-numeric, malformed) don't fail the job; each comes back
in the results as an error record:

```json theme={null}
{"id": "not_a_valid_id", "error": "invalid_id"}
```

## Interpreting results

Results are newline-delimited JSON. Three rules cover the contract:

1. **Absence means clean.** An ID missing from the results has no
   compliance event; your stored copy requires no action.
2. **`action` tells you what to do**: `delete` (remove your stored copy),
   `rehydrate` (re-fetch the Post — it was edited and your copy is stale),
   or `scrub_geo` (remove stored location data for the Post).
3. **One event per ID.** When several events apply, you receive the
   highest-impact one: `delete` outranks `rehydrate`, which outranks
   `scrub_geo`.

Deletion events additionally carry `deleted_at` (an alias of `redacted_at`)
for compatibility with consumers of the legacy results format. `redacted_at`
can be `null` for events that predate timestamp tracking; the obligation to
act is the same.

## Error handling

Errors use RFC 7807 problem+json bodies.

| Status | When                                                                                  | Handling                                        |
| :----- | :------------------------------------------------------------------------------------ | :---------------------------------------------- |
| `400`  | Malformed request (bad `type`, bad body)                                              | Fix the request                                 |
| `401`  | Missing/invalid Bearer on the JSON endpoints, or unusable byte-path token             | Check credentials / recreate the job            |
| `403`  | Valid but expired or wrong-purpose byte-path token; App not enrolled for the endpoint | Recreate the job; check your App's access level |
| `409`  | A job of this type is already active                                                  | Wait for it, or cancel it                       |
| `413`  | Upload exceeds 1 GiB                                                                  | Split the dataset across jobs                   |

## Best practices

* **Run compliance jobs on a schedule.** X requires stored content to
  reflect current compliance state; a recurring (e.g. daily or weekly) job
  over your stored IDs, acting on every returned event, is the intended
  integration pattern. Pair batch jobs with the
  [compliance streams](/x-api/compliance/streams/introduction) if you need
  real-time signals between runs.
* **Upload immediately after creating a job.** The \~15-minute upload window
  is comfortably enough for automation but not for manual workflows with
  long pauses.
* **Store job IDs with your submissions.** `GET /2/compliance/jobs` lists
  recent jobs, but correlating submissions to results is your side's
  responsibility.
* **Download results promptly** and persist them; the download URL lapses
  after \~7 days and results are not retrievable afterward.
* **Act on `rehydrate` via the Posts lookup endpoints** — re-fetching
  returns the current revision of an edited Post along with its edit
  history.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="https://mintcdn.com/x-preview-mintlify-6d8eadf6/c5pmxlylrgjZbc7K/icons/xds/icon-rocket.svg?fit=max&auto=format&n=c5pmxlylrgjZbc7K&q=85&s=0082b4d402bc452feb6fb046011c99d7" href="/x-api/compliance/batch-compliance/quickstart" width="24" height="24" data-path="icons/xds/icon-rocket.svg">
    Create your first compliance job
  </Card>

  <Card title="Compliance streams" icon="stream" href="/x-api/compliance/streams/introduction">
    Real-time compliance events
  </Card>

  <Card title="API Reference" icon="https://mintcdn.com/x-preview-mintlify-6d8eadf6/n02yn7ukCNWwbbxL/icons/xds/icon-code.svg?fit=max&auto=format&n=n02yn7ukCNWwbbxL&q=85&s=6ecdd528a6292ed5e268cb1c9369a567" href="/x-api/compliance/create-compliance-job" width="24" height="24" data-path="icons/xds/icon-code.svg">
    Full endpoint documentation
  </Card>
</CardGroup>
