""" AI Content Drop — AI video and image generation across 35+ models on one credit balance. VERSIONING AND DEPRECATION POLICY This endpoint evolves additively. An element being retired is marked @deprecated with a reason, replacement, announcement date, and at least 180 days of notice. It remains available on this major endpoint while that endpoint is supported; the stated date is the earliest it may be absent from a future major schema. Breaking changes ship at a new endpoint (/graphql/v2), never as a silent change to this one. Retirements are also announced at https://aicontentdrop.com/docs/graphql. RATE LIMITS AND COST 120 requests per minute per IP on the standard tier; 600 per minute for the same IP when a valid `acd_agent_` read token is presented. A user `acd_live_` API key authorizes account operations but does not change this read tier. Every response carries RateLimit-* headers; a 429 carries Retry-After. Query `rateLimit` for the current caller and both tier ceilings, and read `@rateLimit` on the schema and on individual fields for what is enforced where. Separately, each request may spend 1000 complexity points. Every root field carries `@cost`, charged per returned item, and a request over budget is refused before execution with `extensions.code: COMPLEXITY_LIMIT_EXCEEDED` naming both numbers. SUBSCRIPTIONS `generationProgress(id:)` streams one running job over Server-Sent Events. Polling `job(id:)` does the same thing with less machinery and is equally supported. ERRORS Two kinds, deliberately. 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`. Operation-level failures a caller can act on come back INSIDE the payload as typed `UserError` values, so a partial success is still a success. """ schema @rateLimit( limit: 120 registeredAgentLimit: 600 windowSeconds: 60 scope: "ip" ) { query: Query mutation: Mutation subscription: Subscription } """ What resolving this field costs, in complexity points. Points are a budget, not money: every request may spend 1000, and a field that returns a page is charged its complexity times the page size you asked for. A request over budget is refused before it executes, with `extensions.code: COMPLEXITY_LIMIT_EXCEEDED` naming the cost and the ceiling, so you can shrink the page rather than guess. Credits — the thing that costs money — are separate and only ever charged by a mutation that succeeds. """ directive @cost("Points charged per returned item." complexity: Int!) on FIELD_DEFINITION """ The request budget enforced on this field, as actually configured on the server. `scope` says what the counter is keyed on. `ip` counts every caller from one address together; `ip-or-key` counts per API key when one is present and falls back to the address when it is not. Responses carry RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset; a 429 carries Retry-After in seconds. These are the numbers the server enforces, not a policy statement. The schema-level limit covers every request to this endpoint; a field-level one is charged on top of it. """ directive @rateLimit( "Standard-tier requests allowed per window." limit: Int! "Higher public-read ceiling selected by a valid acd_agent_ token, when available." registeredAgentLimit: Int "Window length in seconds." windowSeconds: Int! "What the counter is keyed on." scope: String! ) on FIELD_DEFINITION | SCHEMA "How a value failed, as a machine-readable code." enum ErrorCode { "The input did not satisfy the schema or a business rule." VALIDATION_FAILED "No API key, or a key that is revoked or malformed." UNAUTHENTICATED "Authenticated, but not allowed to touch this object." FORBIDDEN "The object does not exist, or does not belong to this account." NOT_FOUND "The account does not hold enough credits for this generation." INSUFFICIENT_CREDITS "The prompt was refused by the content-safety gate." SAFETY_REJECTED "Too many requests. Retry after the interval in the message." RATE_LIMITED "The upstream model provider failed. Retrying is reasonable." PROVIDER_ERROR "Something broke on our side. Retrying is not." INTERNAL } """ Every error a mutation can hand back as a value. Branch on __typename for the specific shape; read `code` if you only need to categorise. """ interface UserError { "Machine-readable category. Stable across message rewordings." code: ErrorCode! "Human-readable explanation. Safe to show a user." message: String! "Path to the offending input field, when one field is at fault." path: [String!] } "An input value was rejected." type ValidationError implements UserError { code: ErrorCode! message: String! path: [String!] "The value that was rejected, as sent." received: String } "The request carried no usable credential." type AuthenticationError implements UserError { code: ErrorCode! message: String! path: [String!] "Where a human creates a key for this account." createKeyUrl: String! } "The account cannot pay for this generation." type InsufficientCreditsError implements UserError { code: ErrorCode! message: String! path: [String!] "Credits the operation needs." required: Int! "Credits the account holds." available: Int! "Where to top up." topUpUrl: String! } "The content-safety gate refused the prompt. No credits were charged." type SafetyError implements UserError { code: ErrorCode! message: String! path: [String!] } "The named object does not exist on this account." type NotFoundError implements UserError { code: ErrorCode! message: String! path: [String!] "The id or slug that missed." identifier: String! } "The model provider failed. The generation was not charged." type ProviderError implements UserError { code: ErrorCode! message: String! path: [String!] "Whether retrying the same request is likely to succeed." retryable: Boolean! } "Standard Relay page metadata. Present on every connection." type PageInfo { "Whether another page exists after `endCursor`." hasNextPage: Boolean! "Whether another page exists before `startCursor`." hasPreviousPage: Boolean! "Cursor of the first edge in this page, or null when the page is empty." startCursor: String "Cursor of the last edge in this page, or null when the page is empty." endCursor: String } "Which family of model to list." enum ModelType { "Text-to-video and image-to-video models." VIDEO "Text-to-image and image-editing models." IMAGE } """ One generation model and what it costs to run. DEPRECATION IN THIS TYPE: the field "credits" is deprecated in favour of "creditCost". It was deprecated 2026-08-26 and cannot be omitted from a future major schema before 2027-02-22. It is stated here because introspection hides deprecated fields unless a query passes includeDeprecated: true, so a default introspection of this type would not otherwise show it. The full machine-readable register is apiInfo { deprecations }. """ type Model { "Stable model id, underscore form. Dashes are accepted on input and normalised." id: ID! "Display name, e.g. \"Kling 3.0\"." name: String! "Flat credits charged per successful generation. Replaces the deprecated credits field." creditCost: Int! "Whether this is a video or image model." type: ModelType! "DEPRECATED - use creditCost. This compatibility alias remains available on the current major schema." credits: Int @deprecated(reason: "Renamed for clarity. Use Model.creditCost instead. Deprecated 2026-08-26; earliest removal in a future major schema is 2027-02-22.") "Marketplace page for this model." url: String! } "An edge in a model connection." type ModelEdge { "Opaque cursor for this position. Pass to `after` or `before`." cursor: String! "The model at this position." node: Model! } "A page of models." type ModelConnection { "The models in this page, with their cursors." edges: [ModelEdge!]! "The models in this page, without cursors, for clients that do not paginate." nodes: [Model!]! "Where this page sits in the full list." pageInfo: PageInfo! "Total models matching the filter, ignoring pagination." totalCount: Int! } "A published guide or model comparison." type Article { "URL slug, unique." slug: ID! "Article title." title: String! "One-sentence summary." description: String! "Editorial category." category: String! "Topic tags." tags: [String!]! "ISO-8601 publication date." publishedAt: String! "Estimated reading time in minutes." readingTimeMinutes: Int "Canonical HTML URL." url: String! "Markdown twin of the same page, for retrieval." markdownUrl: String! } "An edge in an article connection." type ArticleEdge { "Opaque cursor for this position." cursor: String! "The article at this position." node: Article! } "A page of articles." type ArticleConnection { "The articles in this page, with their cursors." edges: [ArticleEdge!]! "The articles in this page, without cursors." nodes: [Article!]! "Where this page sits in the full list." pageInfo: PageInfo! "Total articles matching the query, ignoring pagination." totalCount: Int! } """ Lifecycle of a generation job. DEPRECATION IN THIS ENUM: the value PENDING is deprecated and is never emitted; it was merged into GENERATING. It cannot be omitted from a future major schema before 2027-02-22. Stated here because enumValues(includeDeprecated:) also defaults to false, so a default introspection does not return it. """ enum JobStatus { "Accepted and running at the provider. Keep polling. Absorbs the deprecated PENDING value." GENERATING "Finished. The asset URL is populated." COMPLETED "Failed. No credits were charged; read errorMessage." FAILED "Ran past the ceiling and was abandoned. No credits were charged." TIMEOUT "DEPRECATED - never emitted; PENDING and GENERATING were merged." PENDING @deprecated(reason: "Merged into GENERATING; a job is never reported as PENDING. Use JobStatus.GENERATING instead. Deprecated 2026-08-26; earliest removal in a future major schema is 2027-02-22.") } """ An asynchronous generation. Created by a mutation, resolved by polling. The default contract is submit-and-poll: re-query `job(id:)` every `pollIntervalSeconds` until status leaves GENERATING. Clients that prefer a push channel can subscribe to `generationProgress(id:)` instead, which streams the same record over SSE. Neither is more current than the other — the subscription is this poll, run on our side. """ type GenerationJob { "Job id. Poll with job(id:)." id: ID! "Where the job is in its lifecycle." status: JobStatus! "Prompt-derived title." title: String "Model that ran, or was chosen for you." model: String "Credits actually charged. Zero until the job succeeds — billing is post-deduct." creditsUsed: Int! "Finished video, when status is COMPLETED." videoUrl: String "Poster frame, when one exists." thumbnailUrl: String "Why the job failed, when status is FAILED." errorMessage: String "ISO-8601 creation time." createdAt: String "How long to wait before polling again. Null once the job is terminal." pollIntervalSeconds: Int "REST URL for the same job, for clients mixing protocols." pollUrl: String! } "An edge in a generation connection." type GenerationEdge { "Opaque cursor for this position." cursor: String! "The generation at this position." node: GenerationJob! } "A page of generations belonging to the authenticated account." type GenerationConnection { "The generations in this page, with their cursors." edges: [GenerationEdge!]! "The generations in this page, without cursors." nodes: [GenerationJob!]! "Where this page sits in the full list." pageInfo: PageInfo! } "The authenticated account." type Viewer { "Account id." id: ID! "Display name on the account." username: String "Plan name: free, starter, professional, ultra, or enterprise_max." plan: String! "Credits remaining. Charged only on successful generation." credits: Int! } "A published subscription plan." type Plan { "Plan id, e.g. \"professional\"." id: ID! "Display name." name: String! "Monthly price in US dollars." monthlyUsd: Float! "Credits granted each billing period." creditsPerMonth: Int! "Headline features included." features: [String!]! } "What one generation costs, before committing to it." type CostEstimate { "Normalised model id the quote applies to." modelId: ID! "Display name." name: String! "Credits per generation." creditsEach: Int! "How many generations were quoted." quantity: Int! "Credits for the whole batch." creditsTotal: Int! } "Current rate-limit policy for this caller." type RateLimitInfo { "Requests allowed for this caller per window." limit: Int! "Standard public-read ceiling." standardLimit: Int! "Public-read ceiling selected by a valid registered-agent token." registeredAgentLimit: Int! "Tier selected for this caller: standard or registered_agent." tier: String! "Counter key used by this endpoint." bucket: String! "Window length in seconds." windowSeconds: Int! "Response header carrying the remaining allowance." remainingHeader: String! "How a 429 tells you when to come back." retryAfterHeader: String! } """ One schema element on its way out. The register exists because introspection hides what it deprecates: a client that asks for a type's fields without `includeDeprecated: true` is handed a schema in which `Model.credits` simply does not appear, and cannot tell a retiring field from one that was never there. This list is derived from the live schema at request time, so it cannot drift from the @deprecated markers. """ type SchemaDeprecation { "Where it lives, e.g. Model.credits or JobStatus.PENDING." path: String! "FIELD, ENUM_VALUE, ARGUMENT, or INPUT_FIELD." kind: String! "Why it is retiring, and what replaces it." reason: String! } "Where to find everything else." type ApiInfo { "Product name." name: String! "Schema version. Additive changes do not bump it." version: String! "The REST twin of this API." restEndpoint: String! "OpenAPI 3.1 document describing the REST twin." openapiUrl: String! "Human documentation for this schema." documentationUrl: String! "How auth works, in agent-readable form." authDocumentationUrl: String! "The versioning and deprecation policy, in prose." deprecationPolicy: String! "MCP endpoint, for clients that would rather speak MCP." mcpEndpoint: String! "Whether a sandbox mode exists, and how to enter it." sandbox: String! "How to subscribe to job progress, and over what transport." subscriptions: String! "Every @deprecated element in the live schema, whether or not introspection shows it." deprecations: [SchemaDeprecation!]! } """ Read operations over public catalogue data and authenticated account data. DEPRECATION IN THIS TYPE: generation(id:) is the compatibility name for job(id:). Use job(id:) in new clients. The alias remains available on this major schema and cannot be omitted from a future major schema before 2027-02-22. """ type Query { "Endpoints, policy, and where the human docs are. Needs no credential." apiInfo: ApiInfo! @cost(complexity: 1) "Paginated model catalogue with flat credit costs. Needs no credential." models( "Forward page size, 1-100. Defaults to 50." first: Int "Return models after this cursor." after: String "Backward page size, 1-100." last: Int "Return models before this cursor." before: String "Video or image. Defaults to VIDEO." type: ModelType "Only models at or below this credit cost." maxCredits: Int ): ModelConnection! @cost(complexity: 2) "One model by id, or null when no such model exists. Needs no credential." model("Model id, underscore or dash form." id: ID!): Model @cost(complexity: 1) "Quote a batch before committing to it. Needs no credential." costEstimate( "Model id from the catalogue." modelId: ID! "How many generations to quote. Defaults to 1." quantity: Int = 1 ): CostEstimate! @cost(complexity: 1) "Paginated published guides, optionally filtered by keyword. Needs no credential." articles( "Forward page size, 1-100. Defaults to 20." first: Int "Return articles after this cursor." after: String "Backward page size, 1-100." last: Int "Return articles before this cursor." before: String "Keyword filter over title, summary, and tags." query: String ): ArticleConnection! @cost(complexity: 2) "One article by slug, or null. Needs no credential." article("Article slug." slug: String!): Article @cost(complexity: 1) "Published subscription plans and what each includes. Needs no credential." plans: [Plan!]! @cost(complexity: 2) "Rate-limit policy that applies to this caller. Needs no credential." rateLimit: RateLimitInfo! @cost(complexity: 1) "The authenticated account. Requires an API key." viewer: Viewer @cost(complexity: 2) "Deprecated compatibility alias for job(id:). Requires an API key." generation("Job id returned by generateVideo." id: ID!): GenerationJob @deprecated(reason: "Renamed to make the asynchronous polling contract explicit. Use Query.job instead. Deprecated 2026-08-26; earliest removal in a future major schema is 2027-02-22.") @cost(complexity: 2) """ Status of one asynchronous job. Replaces the deprecated generation(id:) alias with the name a polling client reaches for first. Requires an API key. """ job("Job id returned by generateVideo." id: ID!): GenerationJob @cost(complexity: 2) "Generations on the authenticated account, newest first. Requires an API key." generations( "Forward page size, 1-50. Defaults to 10." first: Int "Return generations after this cursor." after: String ): GenerationConnection! @cost(complexity: 5) } "What generateVideo needs." input GenerateVideoInput { "What the video should show. Detail helps." prompt: String! "Model id from the catalogue. Omit to let the platform choose." model: String "Length in seconds. Defaults to 5." duration: Int "16:9, 9:16, or 1:1. Defaults to 16:9." aspectRatio: String "Public image URL to animate, for image-to-video." imageUrl: String "Rehearse without spending credits: returns a synthetic completed job." sandbox: Boolean "Retry-safe key. The same key returns the first job instead of starting a second." idempotencyKey: String } """ The result of asking for a video: an ASYNC JOB, not a video. Both fields are always present. A submitted job populates `job` and leaves `errors` empty; a refused one populates `errors` and leaves `job` null. Nothing here throws for a reason the caller can fix. The job identifiers are mirrored onto this payload so a client can start polling from the mutation response alone, without traversing into `job`. """ type GenerateVideoPayload { "The accepted job, when one was created." job: GenerationJob "Id of the accepted job. Poll it with job(id:)." jobId: ID "Lifecycle state at the moment of submission. Always GENERATING on success." status: JobStatus "Seconds to wait before the first poll." pollIntervalSeconds: Int "Why nothing was created, when nothing was." errors: [UserError!]! } type Mutation { """ Start a video generation on the authenticated account and return immediately. Requires an API key. Credits are charged only when the job succeeds, so a failure costs nothing. Poll job(id:) until status leaves GENERATING. """ generateVideo("Prompt, model, and options." input: GenerateVideoInput!): GenerateVideoPayload! @cost(complexity: 50) @rateLimit(limit: 20, windowSeconds: 60, scope: "ip-or-key") } """ Push updates for work that is already running. Transport is Server-Sent Events in distinct-connections mode: send the subscription to POST https://aicontentdrop.com/graphql with `Accept: text/event-stream`, or GET https://aicontentdrop.com/graphql/stream?query=..., and read `event: next` frames until `event: complete`. There is no WebSocket endpoint. Polling `job(id:)` remains supported and is the simpler contract; this exists for clients that would rather be told than ask. """ type Subscription { """ Emit the state of one generation every few seconds until it stops being GENERATING, then complete. Requires an API key, and the job must belong to it. The stream also completes on its own after 10 minutes so a forgotten client cannot hold a connection forever; re-subscribe, or fall back to polling, if a job outlives it. """ generationProgress("Job id returned by generateVideo." id: ID!): GenerationJob! @cost(complexity: 5) @rateLimit(limit: 20, windowSeconds: 60, scope: "ip-or-key") }