---
title: "AI Content Drop GraphQL API | AI Content Drop"
description: "The AI Content Drop GraphQL schema: Relay connections, typed UserError payloads, asynchronous generation jobs, open introspection, and the deprecation policy."
canonical: "https://aicontentdrop.com/docs/graphql"
source: "https://aicontentdrop.com/docs/graphql"
---

# AI Content Drop GraphQL API

Endpoint: `POST https://aicontentdrop.com/graphql` · SDL: [`/graphql/schema.graphql`](https://aicontentdrop.com/graphql/schema.graphql) · Introspection: **open, no credential**.

The same data as the [REST API](https://aicontentdrop.com/docs/api), in the shape GraphQL clients expect. Public fields answer without a key; account fields and `generateVideo` need `Authorization: Bearer acd_live_…`.

```bash
curl -sX POST https://aicontentdrop.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ models(first:3){ nodes{ id creditCost } pageInfo{ hasNextPage endCursor } } }"}'
```

## Pagination — Relay connections

Every list is a Connection. `edges` carry `cursor` and `node`; `pageInfo` carries `hasNextPage`, `hasPreviousPage`, `startCursor`, and `endCursor`; `totalCount` ignores pagination. Both directions work: `first`/`after` pages forward, `last`/`before` pages backward. Cursors are opaque — pass them back, do not parse them.

```graphql
{
  articles(first: 10, after: "YWNkOjk") {
    totalCount
    edges { cursor node { slug title publishedAt } }
    pageInfo { hasNextPage endCursor }
  }
}
```

## Errors — typed, in the payload

Request-level failures (bad syntax, missing credential, rate limit) come back in the standard top-level `errors` array with a machine code in `extensions.code`.

Failures a caller can act on come back **inside the payload** as values implementing the `UserError` interface, so you branch on a type instead of matching a message:

```graphql
mutation {
  generateVideo(input: { prompt: "a cat surfing", model: "kling_3_0" }) {
    job { id status pollIntervalSeconds }
    errors {
      __typename
      code
      message
      ... on InsufficientCreditsError { required available topUpUrl }
      ... on AuthenticationError { createKeyUrl }
      ... on ValidationError { path received }
    }
  }
}
```

Concrete types: `ValidationError`, `AuthenticationError`, `InsufficientCreditsError`, `SafetyError`, `NotFoundError`, `ProviderError`. Every one carries an `ErrorCode` enum value, which stays stable even when the message is reworded.

## Long-running work — async jobs

A generation takes one to four minutes, so the mutation does not wait for it. `generateVideo` returns a `GenerationJob` immediately with `status: GENERATING` and a `pollIntervalSeconds`; you re-query `job(id:)` until the status is terminal.

```graphql
query Poll($id: ID!) {
  job(id: $id) {
    status
    creditsUsed
    videoUrl
    errorMessage
    pollIntervalSeconds
  }
}
```

`pollUrl` on the job is the REST equivalent, for clients mixing protocols.

### Or subscribe instead of polling

`generationProgress(id:)` streams the same record over **Server-Sent Events**, in
graphql-sse *distinct-connections* mode: one request carries one operation, the
server writes `event: next` frames, and closes with `event: complete`. There is
no WebSocket endpoint.

```bash
curl -N -X POST https://aicontentdrop.com/graphql \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Authorization: Bearer acd_live_YOUR_KEY" \
  -d '{"query":"subscription { generationProgress(id: \"VIDEO_ID\") { status creditsUsed videoUrl } }"}'
```

```
event: next
data: {"data":{"generationProgress":{"status":"GENERATING","creditsUsed":0,"videoUrl":null}}}

event: next
data: {"data":{"generationProgress":{"status":"COMPLETED","creditsUsed":27,"videoUrl":"https://…"}}}

event: complete
data:
```

An `EventSource` cannot set a request body, so the same operation is reachable as
a GET: `https://aicontentdrop.com/graphql/stream?query=…`. Queries and mutations are legal on the
stream transport too and produce exactly one `next` — a client that only speaks
SSE never needs a second code path.

Nothing is more current on the stream than in a poll: the subscription **is** the
poll, run on our side, and it completes on its own after ten minutes so a
forgotten client cannot hold a connection open. Polling `job(id:)` is the simpler
contract and is equally supported.

## Versioning and deprecation policy

The current major schema is **additive-only**. A field is never removed or retyped in place:

1. A field being retired is marked `@deprecated` with a reason naming its replacement.
2. It receives **at least 180 days** between its announcement and the earliest date it may be omitted from a future major schema.
3. The retirement is announced on this page before it happens.
4. A change that cannot be made additively ships as a new endpoint (`/graphql/v2`), never as a silent change to this one.

A deprecated element remains available on `/graphql` while this major endpoint is supported. The date in the register is therefore not an in-place deletion date: it is the earliest that a later major schema may omit the element. If the `/graphql` endpoint itself is ever retired, its HTTP responses will carry `Deprecation`, `Sunset`, and `Link: rel="deprecation"` headers with the same minimum 180-day notice.

**Introspection hides what it deprecates.** `__Type.fields` takes
`includeDeprecated` and it defaults to `false`, so a standard introspection
response simply omits `Model.credits` — and a client reading that response cannot
tell a retiring field from one that never existed. Two ways round it:

```graphql
# 1. ask introspection for them explicitly
{ __type(name: "Model") { fields(includeDeprecated: true) { name isDeprecated deprecationReason } } }

# 2. or read the register, which is derived from the live schema
{ apiInfo { deprecations { path kind reason } } }
```

Currently deprecated (all remain operational on this major schema):

| Element | Replacement | Announced | Earliest future-major removal | Why |
|---|---|---|---|---|
| `Model.credits` | `Model.creditCost` | 2026-08-26 | 2027-02-22 | Renamed for clarity. |
| `JobStatus.PENDING` | `JobStatus.GENERATING` | 2026-08-26 | 2027-02-22 | Merged into GENERATING; a job is never reported as PENDING. |
| `Query.generation` | `Query.job` | 2026-08-26 | 2027-02-22 | Renamed to make the asynchronous polling contract explicit. |

## Cost and limits

The standard read tier is 120 requests per minute per IP. A valid `acd_agent_` read token selects 600 requests per minute for that IP without granting account access; a user `acd_live_` API key authorizes account operations but does not change the read tier. `RateLimit-*` headers appear on every response and a 429 carries `Retry-After`. Query `rateLimit { limit standardLimit registeredAgentLimit tier bucket windowSeconds }` for the current caller and both ceilings. Queries are capped at 8,000 characters and batched operations are refused — send one operation per request.
