extractbrand API reference

Full HTTP reference for the extractbrand API: fetch a brand's logos, colour roles, typography and voice for any domain as JSON, with per-endpoint parameters, status codes and response shapes.

OpenAPI 3.1 · version v1

Start at https://extractbrand.dev/quickstart — sign up, mint a key, make one request.

Design-system-grade brand data for any domain: colours, typography, logos, imagery and voice, extracted from the live site and served from a shared cache.

Every shape on this page is generated from the Zod schemas the runtime validates against, so the reference cannot describe a response the API does not send.

Base URL

https://api.extractbrand.dev

Authentication

Send your key as a bearer token on every request:

curl https://api.extractbrand.dev/v1/brands/stripe.com \
  -H "Authorization: Bearer eb_..."

Keys begin with the eb_ prefix. Create one from your dashboardsign up first if you have no account. Treat a key like a password: it carries the full access of the organization it belongs to. The key also determines which organization you act on — there is no organization_id parameter anywhere in this API.

Cache-first, with a job for the misses

GET /v1/brands/{domain} answers from a shared cache. A domain someone has already extracted answers 200 in milliseconds. A domain nobody has extracted yet answers 202 with a job, and you re-request the same URL to collect the result — the job envelope's poll_url points back at it. Pass ?wait=N to hold the request open for up to 25 seconds so a first-time domain can answer 200 in one call.

An entry lives for 30 days. Past that it is served with cache_status: "stale" while a refresh runs behind it, so you are never blocked on an expiry. cache_status tells you what latency to expect, never what you were charged — the charge is the same either way.

Quota

Every answered call costs one unit — a domain we already hold and a domain nobody has read yet cost the same, because the meter counts what we serve you rather than what we had to run. POST /v1/brands/{domain}/refresh costs more, since it always forces a fresh extraction. Every 200 and 202 carries the organization's standing in its quota block. A spent quota answers 429 with code quota_exceeded.

Errors

Every non-2xx body on every endpoint is the same shape, with a code from one closed set:

{
  "error": {
    "code": "invalid_domain",
    "message": "The provided domain is invalid or could not be resolved."
  }
}

Contract extras live inside that same error object, never beside it — a 429 carries error.retry_after_seconds, a 422 carries error.failure_reason. A 429 also sets the standard Retry-After header, from the same number.

Brands

Read and refresh brand data for a domain. Brand data is derived from the public web and shared across every organization, so a domain another customer has already extracted answers instantly and for free.

GET /v1/brands/{domain}

Get a brand

Returns the cached brand for a domain. A fresh or stale cache entry answers 200 immediately. A domain nobody has extracted yet answers 202 with a job, and the caller re-requests this same URL to collect the result — poll_url in the job envelope points back here. Add ?wait=N to hold the request open for up to N seconds so a first-time domain can answer 200 in a single call. A domain whose owner has proved control of it is served from its record and is never revalidated, so it reports cache_status stale indefinitely once its entry passes its lifetime; the record is maintained by its owner rather than by measurement, and provenance reports which fields they authored.

Parameters

  • wait query · optional — Seconds to hold the request open waiting for an in-flight extraction to finish, so a first-time domain can answer 200 in one call. Clamped to 25; a value that is not a whole number of at least 1 is rejected with 400. Omit it for an immediate 202.
  • domain path · required — The domain to look up, without a scheme. A host that redirects elsewhere is resolved to its landing host, and that host is the cache key the answer is keyed on.

Responses

  • 200 BrandResponse — The cached brand. cache_status is hit within the entry's 30-day lifetime and stale past it — a stale entry is served immediately while a refresh runs behind it.
  • 202 JobResponse — Extraction is under way. Re-request this URL until it answers 200.
  • 400 ApiErrorinvalid_domain when the path segment is not a resolvable domain. invalid_parameter when ?wait is not a whole number of seconds of at least 1.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 404 ApiErrornot_found — this domain is maintained by its owner and we hold no record to serve. Nothing is extracted for it so the answer does not change on retry.
  • 422 ExtractionFailedErrorextraction_failed — this domain failed recently and is inside its negative-result window so it is not re-attempted yet.
  • 429 RateLimitedErrorrate_limited when the API key exceeds its request window. quota_exceeded when the organization's monthly extraction quota is spent.
  • 451 ApiErrorremoved — this brand was removed at the request of its owner and is no longer served. The refusal is final for as long as the removal stands; retrying does not help.
  • 500 ApiErrorinternal_error — the request authenticated but the service failed to answer it.

POST /v1/brands/{domain}/refresh

Refresh a brand

Forces a new extraction for a domain even when a fresh cache entry exists, and always answers 202 — collect the result from GET /v1/brands/{domain}, which poll_url points at. A refresh already under way for this domain is joined rather than duplicated, and costs nothing. Refreshes are capped at one per domain per hour and ten per organization per day, on top of the monthly extraction quota.

Parameters

  • domain path · required — The domain to look up, without a scheme. A host that redirects elsewhere is resolved to its landing host, and that host is the cache key the answer is keyed on.

Responses

  • 202 JobResponse — The refresh was accepted, or an in-flight extraction for this domain was joined.
  • 400 ApiErrorinvalid_domain — the path segment is not a resolvable domain.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 402 ApiErrorrefresh_not_included — this plan's allowance is smaller than what an explicit refresh always costs. Upgrading resolves it.
  • 409 ApiErrorbrand_claimed — this domain is maintained by its owner and is no longer re-extracted on request. The refusal costs neither a refresh window nor a quota unit.
  • 429 RateLimitedErrorrefresh_rate_limited when a refresh window for this domain or organization is spent. quota_exceeded when the monthly extraction quota is spent. rate_limited when the API key exceeds its request window.
  • 451 ApiErrorremoved — this brand was removed at the request of its owner and is no longer extracted. The refusal costs neither a refresh window nor a quota unit.
  • 500 ApiErrorinternal_error — the request authenticated but the service failed to answer it.

Account

Confirm which organization the calling API key acts on.

GET /v1/me

Identify the calling key

Returns the organization the presented API key belongs to. Use it to confirm a key is live and to see which organization it acts on — there is no organization_id parameter anywhere in this API, the key determines the tenant.

Responses

  • 200 MeResponse — The organization the key belongs to.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 429 RateLimitedErrorrate_limited — the key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the key verified but the organization could not be read. Retry rather than rotate the key.

Usage

What your organization has spent, and against what limit.

The one thing to understand before reading any figure here: quota meters answered calls, not the work behind them. A domain already held costs exactly what one extracted on demand costs, because the meter counts what we hand back rather than what it took to get. An explicit refresh costs more than a read. An organization sitting at its ceiling is refused rather than served.

GET /v1/usage

Read this organization's usage

Reports what this organization has spent against its allowance in the current period, and the ceiling actually being enforced. The figures count ANSWERED CALLS: a domain already held costs the same as one nobody has read yet, and an explicit refresh costs more than either because it always forces a fresh extraction. An organization sitting at its ceiling is refused rather than served. There is no organization parameter: the key decides the tenant. An organization that has never called anything reads as a zeroed standing rather than a 404.

Responses

  • 200 UsageStanding — This organization's standing for the current period.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 429 RateLimitedErrorrate_limited — the key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the key verified but the ledger could not be read. Retry rather than rotate the key.

Billing

Buy a plan, manage the subscription, and cap what you can be charged.

Checkout and the Billing Portal both answer with a short-lived Stripe-hosted URL rather than handling card details themselves — no payment information ever passes through this API. The portal manages an existing customer, so an organization that has never checked out has nothing to open and is told so rather than having an empty customer created for it.

PUT /v1/billing/ceiling is the spend guard: it caps billable extractions per period and is the only endpoint here that changes what you can be charged. Zero is a legal ceiling: it does not revoke your key, but every billable call is refused with 429 until the ceiling is raised or the period rolls.

PUT /v1/billing/ceiling

Set this organization's spend ceiling

Caps how many units this organization can be billed for in a period. Send a whole number to set the cap, or null to clear it and fall back to the plan's own allowance. Zero is legal and means no billable calls this period: every read is refused with 429 until the cap is raised or the period rolls. What you send is what gets stored: the clamp against the plan allowance is applied on read, so an organization that raises its plan later gets the ceiling it actually asked for rather than one truncated to the tier it was on at the time. There is no organization parameter: the key decides whose ceiling moves.

Request body

CeilingRequest application/json

Responses

  • 200 Ceiling — The ceiling now in force. clamped says whether the plan allowance is what is actually binding.
  • 400 BillingErrorinvalid_ceiling — the body named no monthly_ceiling or carried something other than null or a whole number of zero or greater. Nothing was written.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 429 RateLimitedErrorrate_limited — the key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the ceiling could not be written. Retry rather than rotate the key.

POST /v1/billing/checkout

Start a checkout for a plan

Opens a Stripe-hosted Checkout session for this organization and returns its URL. The organization's Stripe customer is created on first use and reused thereafter. The plan must be one this API can currently sell — a tier whose price does not yet exist answers 503 plan_unavailable before any Stripe object is created. There is no organization parameter: the key decides who is billed. The session is presented in GBP for callers in the UK and EUR for callers in the euro area, and in USD otherwise; the currency is resolved from the calling connection's country and is not a request parameter. Anywhere outside those three, Stripe presents the USD amount converted to the buyer's local currency on the hosted page.

Request body

CheckoutRequest application/json

Responses

  • 200 CheckoutSession — Redirect the customer to checkout_url.
  • 400 BillingErrorunknown_plan — the body carried no readable plan or named a tier that does not exist.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 429 RateLimitedErrorrate_limited — the key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the checkout could not be created. Retry rather than rotate the key.

POST /v1/billing/portal

Open the billing portal

Returns a Stripe-hosted Billing Portal URL for this organization's existing customer record, where a subscription can be changed, a payment method updated or an invoice read. An organization that has never checked out has no customer to manage and answers 409 rather than having one silently created for it — start a checkout first. There is no organization parameter: the key decides whose portal opens.

Responses

  • 200 PortalSession — Redirect the customer to portal_url.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 409 BillingErrorno_billing_account — this organization has never bought anything and has no Stripe customer to manage.
  • 429 RateLimitedErrorrate_limited — the key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the portal session could not be created. Retry rather than rotate the key.

Webhooks

Stop polling. Register an HTTPS endpoint and every extraction your organization is waiting on is delivered to it when it finishes — including one another organization triggered and you joined, which is the common case on a shared cache.

The endpoints below manage the registry. The delivery itself is an outbound request from us to you, which OpenAPI's operation model cannot express, so its contract is stated here.

What we POST to you

{
  "object": "event",
  "event_id": "evt_9f2c1d0b7a3e4f5061728394a5b6c7d8",
  "event_type": "brand.extraction.completed",
  "created_at": "2026-07-31T09:14:22.104Z",
  "job_id": "5f2c…",
  "data": { "object": "brand", "domain": "stripe.com", "…": "…" }
}

event_type is one of brand.extraction.completed, brand.extraction.failed or webhook.test. Switch on it and ignore anything you do not recognise — the list is additive.

Quality is a field, never an event

A completed delivery reports how much of the brand the extraction actually came back with, in the same two fields the 200 body carries: quality_verdictcomplete, partial or thin — and missing_fields, the field names that read did not find. A null verdict means the entry predates the field and was never assessed; read it as unknown, never as complete, and missing_fields is null with it. Both report what we did not find, never a claim that the company does not have one.

Quality and failure are two axes and never appear on one delivery: a completed event carries a verdict and no failure_reason, a failed event carries a failure_reason and no verdict. A thin extraction is therefore delivered as brand.extraction.completed with a thin verdict — there is no degraded event type, and none is coming. Alert on the field.

Verifying the signature

Every delivery carries x-extractbrand-signature: t=<unix seconds>,v1=<hex>. The digest is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. Verify against the raw bytes, never a re-serialization of the parsed JSON, and compare in constant time. Reject a delivery whose t is more than 300 seconds from your clock — the timestamp is inside the signed material precisely so that check is yours to make.

Other headers: x-extractbrand-event-id, x-extractbrand-event-type and x-extractbrand-delivery-attempt. All three are also inside the signed body.

Delivery and retries

At-least-once. Answer 2xx as soon as you have durably accepted the event and do your work afterwards; anything else is a failure. A 429 or 5xx is retried up to five attempts total with 30s / 2m / 8m / 32m backoff, and every other 4xx is not retried at all — those describe the request, and repeating it changes nothing. Redirects are never followed. **Deduplicate on event_id**: it is stable across every retry of one delivery and distinct for every endpoint.

A slow or failing receiver never affects the extraction itself, and deliveries are never metered.

GET /v1/webhooks

List webhook endpoints

Every webhook endpoint this organization has registered along with the outcome of the last delivery attempted against each one. Signing secrets are never returned by this endpoint or by any other read — the secret is shown once when the endpoint is created.

Responses

  • 200 WebhookEndpointList — The organization's endpoints. The list is empty when none are registered.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 429 RateLimitedErrorrate_limited — the API key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the registry could not be read.

POST /v1/webhooks

Register a webhook endpoint

Registers an HTTPS URL that receives brand.extraction.completed and brand.extraction.failed deliveries for every extraction this organization is waiting on — including one another organization triggered and this one joined. The response carries signing_secret and it is the only time that value is ever returned. The URL must be HTTPS and its hostname must not be a loopback, private, link-local or cloud-metadata address; the hostname is checked as written and is never resolved, and the same check runs again immediately before every delivery. An organization may register up to five endpoints.

Request body

WebhookRegisterRequest application/json

Responses

  • 201 WebhookEndpointCreated — The endpoint was registered. Store signing_secret now — no later read returns it.
  • 400 WebhookUrlRejectedErrorinvalid_webhook_url when the URL broke one of the target rules — error.reason names which. invalid_parameter when the body carried no url string.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 409 ApiErrorwebhook_url_registered when this organization already registered that URL. webhook_limit_reached when it already holds the maximum number of endpoints.
  • 429 RateLimitedErrorrate_limited — the API key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the registry could not be written.

DELETE /v1/webhooks/{endpoint_id}

Delete a webhook endpoint

Stops deliveries to an endpoint. The endpoint becomes disabled rather than disappearing so its delivery history stays readable and the change is reversible by registering the URL again. A delivery already queued for this endpoint is dropped rather than sent — the target is re-read at delivery time. An endpoint id that belongs to another organization answers 404 exactly as an unknown one does.

Parameters

  • endpoint_id path · required — The endpoint's own id, as returned when it was registered.

Responses

  • 200 WebhookEndpoint — The endpoint is disabled and will receive no further deliveries.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 404 ApiErrornot_found — this organization has no endpoint with that id.
  • 429 RateLimitedErrorrate_limited — the API key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the registry could not be written.

POST /v1/webhooks/{endpoint_id}/test

Send a test delivery

Sends one signed webhook.test delivery to a registered endpoint and answers with what the receiver did. The payload carries no brand data by design — a probe that arrived as a real extraction event would push invented data through your production handler. Exactly one attempt is made and it is never retried; the attempt is recorded in the endpoint's delivery history alongside its real deliveries.

Parameters

  • endpoint_id path · required — The endpoint's own id, as returned when it was registered.

Responses

  • 200 WebhookTestResult — The attempt was made. delivered reports whether the receiver answered 2xx.
  • 401 ApiErrorinvalid_api_key — the Authorization header is missing or malformed or names a key that does not verify.
  • 404 ApiErrornot_found — this organization has no active endpoint with that id.
  • 429 RateLimitedErrorrate_limited — the API key exceeded its request window.
  • 503 ApiErrorservice_unavailable — the request authenticated but the registry could not be read.

Response and request shapes

Every shape below is generated from the Zod schemas the runtime validates against.

ActiveJobStatusSchema

"queued" | "running"

ApiError

  • error object required
    • code ApiErrorCode required
    • message string required — A human-readable sentence describing the failure.

ApiErrorCode

"invalid_domain" | "invalid_parameter" | "invalid_api_key" | "rate_limited" | "refresh_rate_limited" | "quota_exceeded" | "extraction_failed" | "service_unavailable" | "removed" | "internal_error" | "not_found" | "invalid_webhook_url" | "webhook_limit_reached" | "webhook_url_registered" | "brand_claimed" | "refresh_not_included"

BillingError

  • error object required
    • code BillingErrorCode required
    • message string required — A human-readable sentence describing the answer.

BillingErrorCode

"unknown_plan" | "plan_unavailable" | "no_billing_account" | "invalid_ceiling"

Brand

  • name string | null required
  • tagline string | null required
  • description string | null required
  • industry string | null required
  • audience string | null required
  • positioning_statement string | null required — Why this company exists, who it serves, and what makes it different.
  • values string[] required
  • colors object[] required — Every colour of the brand, as one ranked list, deduplicated by hex. Read colors[0] for the dominant colour, or find the entry whose role you want — match on role rather than assuming a position, because a brand missing a role shifts every entry below it up. The entries with a non-null role are the brand's paint kit, in rank order; the entries with role: null are colours the page genuinely paints that fill no role, ranked after them by painted area. A colour holds one role only: a brand whose body ink is also its primary reports that hex once, under the higher-ranked role.
    • hex string required — The colour as #rrggbb, lowercase.
    • role "primary" | "secondary" | "accent" | "background" | "surface" | "text" required — What the colour is for, or null for a colour the page paints that fills no role. The order of this list is the rank order.
    • on string required — A text colour that is legible on hex, so a caller never derives one.
    • names string[] required — The names the site's own stylesheet gives this colour — its CSS custom properties. Empty when the site names it nothing.
    • coverage number | null required — Fraction of the rendered page this colour paints, 0 to 1. Null when the page could not be rendered or the entry predates this measurement.
  • fonts object[] required — The brand's faces, most prominent first, at most two. Replaces the former array of family names and the typography block: family is what that array carried, role is the heading/body split and availability is how to load the face.
    • family string required — The family name as the site declares it.
    • role "display" | "body" required — Where the face is used. The first family is the display face.
    • availability "google" | "custom-webfont" | "system" | "unknown" required — How the face can be loaded. unknown when the site gave no signal.
  • font_confidence number required — How firmly the families above were resolved, 0 to 1.
  • logo BrandLogo required
  • visual_style BrandVisualStyle required
  • motion BrandMotion | null required — Null when the site carries no motion evidence.
  • spacing object required — The spacing system read off the brand's own CSS.
    • base_unit number required — The base spacing unit in pixels.
    • scale number[] required — The scale, as multipliers of base_unit.
    • content_margin number required — The outer horizontal content margin in pixels.
  • accessibility object required
    • contrast_target "AA" | "AAA" required
    • min_font_size_px number required
    • colorblind_safe boolean required
  • tone string[] required
  • voice string[] required
  • copywriting object required
    • preferred_terms string[] required — Words and phrases the brand actively uses.
    • avoided_terms string[] required — Words and phrases that conflict with the voice.
    • cta_style string required — How calls to action are written on this brand.
    • headline_case string required — Common values: title, sentence, upper, mixed.
    • tone_by_context object[] required — How the voice shifts across surfaces. Empty when the tone is uniform.
      • context string required
      • tone string required
  • brand_intelligence object | null required — A deeper read of the company behind the brand. Null when the site gave too little to work from.
    • company_summary string required
    • known_for string[] required
    • design_personality string required
    • design_conventions string[] required
    • slide_guidance string required
    • brand_values string[] required
    • inferred_audience string required
    • writing_style string required
  • confidence number required — Overall extraction confidence, 0 to 1. Low values are reported, never hidden.
  • confidence_per_field Record<string, number> required — Per-field confidence, keyed by extracted field name — primary, fonts, positioning_statement and the like. A field the extraction never reached is absent rather than zero.
  • confidence_signals string[] required — What the confidence score was built from.
  • gaps string[] required — Fields no specialist could fill. Empty means the extraction was complete.
  • platform "NextJsVite" | "WordPress" | "Framer" | "Webflow" | "Wix" | "Shopify" | "Generic" | "CssInJs" | "Astro" | "Squarespace" | "Ghost" | "EditorX" | "HubSpotCms" | "BigCommerce" | "Mantine" | "Tailwind" | "In5" | "GitHub" | "Webstudio" | "Remix" | "VercelStaticExport" required — The site platform the page was recognised as.
  • screenshot_blob_url string | null required — The full-page screenshot the brand was read from. Null when the page was read without rendering.
  • schema_version string required — The payload version. Bumped only when a field changes meaning.

BrandMotion

  • personality string | null required — One sentence on how the brand visibly animates.
  • intensity string | null required — Common values: none, minimal, subtle, moderate, rich. Use a richer phrase when it better captures the brand.
  • speed string | null required — Common values: instant, fast, moderate, slow. Use a richer phrase when it better captures the brand.
  • easing_style string | null required — The character of the observed timing functions.
  • signature_effects string[] required — Recognisable recurring effects, by element and behaviour.
  • detected_libraries string[] required
  • has_scroll_animations boolean | null required
  • has_hover_effects boolean | null required
  • reduced_motion_support boolean | null required

BrandProvenance

  • source "measured" | "owner" required — Whether any field in this response was authored by the brand's owner. 'owner' when owner_fields is non-empty, 'measured' otherwise.
  • owner_fields string[] required — Dotted paths into this response that the brand's verified owner authored. Every field not listed was measured from the brand's own site.

BrandResponse

  • object "brand" required
  • domain string required
  • cache_status ServedCacheStatusSchema required
  • extracted_at string required
  • expires_at string required
  • quality_verdict QualityVerdictSchema | null required
  • missing_fields string[] required
  • brand Brand required
  • provenance BrandProvenance required
  • quota Quota required

BrandVisualStyle

  • theme "light" | "dark" required
  • corner_style string | null required — Common values: sharp, subtle, rounded, pill. Use a richer phrase when it better captures the brand.
  • headline_transform string | null required — Common values: uppercase, capitalize, none. Use a richer phrase when it better captures the brand.
  • headline_tracking string | null required — Common values: tight, normal, wide. Use a richer phrase when it better captures the brand.
  • headline_weight string | null required — Common values: light, normal, medium, bold, heavy. Use a richer phrase when it better captures the brand.
  • shadow_style string | null required — Common values: none, subtle, elevated, dramatic. Use a richer phrase when it better captures the brand.
  • uses_gradients boolean | null required
  • border_style string | null required — Common values: none, subtle, prominent. Use a richer phrase when it better captures the brand.
  • photography_treatment string | null required — Common values: full-color, muted, grayscale, duotone, high-contrast. Use a richer phrase when it better captures the brand.
  • density string | null required — Common values: dense, balanced, spacious. Use a richer phrase when it better captures the brand.
  • aesthetic_tags string[] required
  • headline_letter_spacing string | null required — The authored CSS value, e.g. -0.02em.
  • headline_font_weight number | null required
  • border_radius string | null required — The authored CSS value, e.g. 12px.
  • box_shadow string | null required — The authored CSS value.
  • icon_style string | null required — Common values: outlined, filled, duotone, hand-drawn. Use a richer phrase when it better captures the brand.
  • illustration_style string | null required — Common values: geometric, organic, flat, 3d, isometric, hand-drawn, line-art, none. Use a richer phrase when it better captures the brand.
  • photography_subjects string | null required — What the brand's imagery actually shows.
  • photography_composition string | null required — How that imagery is composed and cropped.
  • photography_color_grading string | null required — The colour treatment applied to it.
  • patterns string[] required — Recurring graphic motifs, e.g. faint grid lines.

Ceiling

  • ceiling number required — The limit now in force, after clamping against the plan allowance.
  • ceiling_source "plan" | "org" required — Which of the two set the figure above.
  • clamped boolean required — True when the stored value was higher than the plan allows and the plan's number is what is being enforced. The value you sent is still what is stored.

CeilingRequest

  • monthly_ceiling number | null required — A whole number of extractions per period, or null to clear the ceiling and fall back to the plan's allowance. Zero is legal and means no fresh extractions this period. The value you send is what gets stored — the clamp against the plan allowance is applied on read, so raising your plan later restores the ceiling you actually asked for.

CheckoutRequest

  • plan "free" | "starter" required — The plan to purchase.

CheckoutSession

  • checkout_url string required — A Stripe-hosted Checkout URL. Redirect the customer to it; it is single-use and expires.

ExtractionFailedError

  • error object required
    • code ApiErrorCode required
    • message string required — A human-readable sentence describing the failure.
    • failure_reason FailureReasonSchema optional — Why the most recent extraction of this domain failed.

FailureReasonSchema

"bot_blocked" | "unreachable" | "timeout" | "extraction_failed" | "enqueue_failed" | "stale_claim_superseded" | "suppressed" | "claimed"

JobResponse

  • object "job" required
  • job_id string required
  • domain string required
  • status ActiveJobStatusSchema required
  • poll_url string required
  • quota Quota required

MeResponse

  • organization_id string required — The organization the API key belongs to.
  • organization_name string required
  • key_label string | null required — The label given to the key when it was created, or null if it has none.

PortalSession

  • portal_url string required — A Stripe-hosted Billing Portal URL for the organization's existing customer record. Single-use and expiring.

QualityVerdictSchema

"complete" | "partial" | "thin"

Quota

  • used number required
  • limit number required
  • period_end string required
  • near_limit boolean required

RateLimitedError

  • error object required
    • code ApiErrorCode required
    • message string required — A human-readable sentence describing the failure.
    • retry_after_seconds number optional — Whole seconds to wait before retrying. Mirrors the Retry-After header.

ServedCacheStatusSchema

"hit" | "stale"

UsagePeriod

  • key string required — The period's own key.
  • start string required — ISO-8601 UTC. Inclusive.
  • end string required — ISO-8601 UTC. Exclusive.

UsageStanding

  • organization_id string required — The organization the calling key acts on. There is no organization parameter anywhere in this API — the key decides the tenant.
  • period UsagePeriod required
  • plan "free" | "starter" required — The plan this organization is currently on.
  • fresh_reads_used number required — Extractions debited this period. Cached reads are never counted.
  • fresh_reads_limit number required — The effective allowance — the same number ceiling reports.
  • fresh_reads_remaining number requiredfresh_reads_limit minus fresh_reads_used, floored at zero.
  • ceiling number required — The limit actually enforced on extraction. Identical to fresh_reads_limit; both are reported so a client need not infer that they are the same figure.
  • ceiling_source "plan" | "org" requiredplan when the tier's own allowance is binding, org when this organization set a lower ceiling of its own via PUT /v1/billing/ceiling.

WebhookEndpoint

  • object "webhook_endpoint" required
  • endpoint_id string required
  • url string required
  • status "active" | "disabled" required
  • description string optional
  • created_at string required — ISO-8601 UTC.
  • disabled_at string optional — ISO-8601 UTC. Present only while the endpoint is disabled.
  • last_attempt WebhookLastAttempt optional

WebhookEndpointCreated

  • object "webhook_endpoint" required
  • endpoint_id string required
  • url string required
  • status "active" | "disabled" required
  • description string optional
  • created_at string required — ISO-8601 UTC.
  • signing_secret string required — The HMAC-SHA256 key this endpoint's deliveries are signed with. Returned exactly once — here — and never by any later read.

WebhookEndpointList

  • object "list" required
  • data WebhookEndpoint[] required

WebhookLastAttempt

  • outcome "delivered" | "failed" required
  • attempt number required — Which attempt of that delivery this was, 1-based.
  • response_status number | null required — The receiver's HTTP status, or null when nothing answered.
  • error_kind "http_status" | "network" | "timeout" | "blocked_url" optional — Why the attempt failed. Absent on a delivered attempt.
  • duration_ms number required
  • occurred_at string required — ISO-8601 UTC.

WebhookRegisterRequest

  • url string required — The HTTPS URL deliveries are POSTed to. It is validated against the platform's SSRF guard both now and immediately before every delivery, so a host that later resolves to a private address stops being delivered to.
  • description string optional — Your own label for this endpoint. Never interpreted.

WebhookTestResult

  • object "webhook_test" required
  • endpoint_id string required
  • event_id string required — The event_id inside the payload that was sent.
  • delivered boolean required — True when the receiver answered 2xx. A test send is never retried.
  • response_status number | null required — The receiver's HTTP status, or null when nothing answered.
  • error_kind "http_status" | "network" | "timeout" | "blocked_url" optional — Why it failed. Absent when it was delivered.
  • duration_ms number required

WebhookUrlRejectedError

  • error object required
    • code ApiErrorCode required
    • message string required — A human-readable sentence describing the failure.
    • reason "invalid_url" | "insecure_scheme" | "blocked_host" | "unsupported_url_component" required — Which rule the submitted URL broke.