Revvly
Docs
API Reference

Read data, provision clients and subscriptions, and submit prospects with the public GraphQL API.

One endpoint, one round-trip: fetch a client together with its region, subscriptions, invoices, and payment status; create a client and a subscription for it, each with your own externalId; or send a complete prospect form submission. Authenticated with the same API keys as the rest of the platform.

REST for ingestion, GraphQL for the rest

Revvly’s external API is split by shape. High-volume ingestion — usage and event streams — stays on REST, where flat payloads and HMAC signing fit. Everything else lives here on GraphQL: reads of related data — a client together with its subscriptions, invoices, and payment status — where you select the nested fields you want in one request, and client management — creating a client, provisioning a subscription for it, and submitting a prospect form — through mutations.

iThis is a dedicated, curated public schema — separate from Revvly’s internal dashboard API. It exposes only the types documented here, so it is safe to build against as a stable contract.

#Endpoint & authentication

POSThttps://api.revvly.ai/api/v1/graphqlapplication/json

Send a standard GraphQL request (a query plus optional variables) as JSON. Authenticate with a rvly_sk_live_* API key in the Authorization header — the same keys used for the REST surface. Every request is scoped to the key’s tenant; you can never read another tenant’s data.

Authorization
Bearer rvly_sk_live_<your-token>. Missing or invalid ⇒ UNAUTHENTICATED.
Capabilities
Every key holds READ, which covers queries. Mutations need WRITE, except createEndUserToken and upsertSupportUsers (SUPPORT_TOKEN_MINT), submitForm (FORM_SUBMIT) and the knowledge mutations (KNOWLEDGE_WRITE), where WRITE is neither needed nor sufficient. A key without the needed capability ⇒ FORBIDDEN. See the API key reference.
Rate limit
Per-key, shared with the REST surface: 10 per second and 100 per minute unless the issuer sets other values. Over the limit ⇒ RATE_LIMITED with a Retry-After header.
Query depth
Nesting is bounded; excessively deep queries are rejected with QUERY_DEPTH_EXCEEDED before execution.
Request headers
Authorization: Bearer rvly_sk_live_<your-token>
Content-Type:  application/json

Give this prompt to an integration agent

Copy this context when an agent needs to build a client read, provisioning, or prospect submission integration.

You are integrating a trusted backend with Revvly public GraphQL to provision billing clients and subscriptions, and read receivables.

Working agreement before you implement:
- Do not write production code until you have enough concrete context from the operator.
- Ask clarifying questions first — prefer one short round of questions over guessing.
- Confirm at least: source system / trigger, field mapping, where the API key and any signing secret will be stored, the target environment (local, staging, or production), and the Revvly tenantId when it is not already provided below.
- Once those answers are in place, implement the full integration against the contract below end-to-end.
- Open or fetch the Full guide URL if you need examples or edge-case detail beyond this prompt.
- Never invent API keys, signing secrets, region ids, subscription ids, or other tenant data.
- Never put secrets in browser code, logs, client bundles, or source control.

Tenant context
- The operator has not supplied a tenantId in this prompt. Ask for the Revvly tenant id before making tenant-scoped assumptions.

Platform rules that apply to every Revvly API call:
- Auth is a Bearer API key issued in the dashboard under Settings → API Keys. Tokens are prefixed `rvly_sk_live_`.
- Rate limits are per API key and default to 10 requests/second and 100 requests/minute. Overage is HTTP 429 with a `Retry-After` header in seconds (public GraphQL reports `errors[].extensions.code = RATE_LIMITED`). Honour Retry-After and back off — never retry a 429 in a tight loop.
- A key can be scoped to specific subscriptions and meters. List reads are filtered to that scope; reading or writing a single out-of-scope resource is 403 (`FORBIDDEN`), not an empty result.
- Every call is tenant-scoped by the key itself. There is no tenant parameter to switch — a different tenant means a different key.
- The token and the signing secret are server-side credentials: no browser code, no client bundles, no logs, no source control.

Full guide: https://www.revvly.ai/docs/guides/public-graphql

Goal
Give a backend one intentional contract for billing-client and subscription creation, and receivables reads (invoices, payment summary, PDF download links).

Endpoint and auth
- POST https://api.revvly.ai/api/v1/graphql
- Authorization: Bearer <API_KEY>
- Content-Type: application/json
- Mutations require WRITE capability on the API key ("Read & write" access level at issue time); queries need only READ.
- Introspection is enabled on this endpoint — the curated schema is the contract, so introspect it rather than guessing field names.
- Query nesting is capped at depth 8; deeper documents are rejected before execution with QUERY_DEPTH_EXCEEDED.
- Never expose the API key in browser code.

Important distinction
- createClient provisions a billing Client and requires a valid active region.
- Inbound lead / demo forms must use createProspect instead (CRM), not createClient.

Read a client
- client(id: ...) or client(externalId: ...) — supply exactly one; supplying both or neither is BAD_REQUEST.
- Nested fields of interest: region, subscriptions, invoices(status, limit), paymentSummary, and invoice.pdfUrl
- invoices returns newest first; limit defaults to 100 and caps at 1000. status filters to one payment status (PAID | PENDING | OVERDUE | FAILED | CANCELLED). There is no cursor pagination on invoices yet.
- paymentSummary is one entry per currency the client has been invoiced in: currency, outstanding (PENDING + OVERDUE + FAILED), overdue, invoiceCount, overdueCount
- pdfUrl is a short-lived (~30 min) presigned URL, minted fresh per query and null when no PDF exists — request it only when the caller will download immediately
- If the API key is scoped to specific subscriptions, subscriptions / invoices / paymentSummary are all filtered to that scope, so totals will not match the dashboard's. Use an unscoped key for receivables reporting.

Provision a billing client
1. Discover an active region with GET https://api.revvly.ai/api/v1/regions
2. Call createClient with at least name and regionId
3. Store your own externalId at creation time so later reads can use client(externalId: ...) without storing Revvly ids if you prefer

Example create
mutation CreateClient($input: CreateClientInput!) {
  createClient(input: $input) {
    id
    externalId
    name
    region { id name defaultCurrency }
  }
}

{
  "input": {
    "name": "Acme Corporation",
    "regionId": "<active-region-id>",
    "externalId": "acct_123",
    "paymentTermsDays": 30
  }
}

Provision a subscription for that client
- Call createSubscription with clientId or clientExternalId (exactly one), period, startDate, and at least one of bundleId / moduleId / moduleIds / variableComponentIds / components — a subscription that bills nothing is BAD_REQUEST.
- Set externalId on the subscription itself, separate from the client's externalId. This is the id the usage/event APIs resolve against as externalSubscriptionId — without it, a subscription created here has nothing for a usage-reporting integration to find, and every usage call for it 404s until someone sets it by hand on the dashboard.
- Use components[] to seed a starting quantity per Variable Component at creation (e.g. an initial seat/child count) via startingQuantity, and an inclusion via includedQuantity. This seeds the initial usage counter so the start invoice bills correctly and later usage reports are deltas from that baseline, not raw totals.
- Deliberately not exposed here: historical invoice backfill, billing schedules, initial discounts, legacy catalog-version pins, and plan-change timelines. Those stay dashboard-only (the subscription wizard's createSubscriptionWithPricing on the internal, JWT-authed GraphQL API); this mutation covers provisioning a new subscription going forward.
- A duplicate externalId (client or subscription) is CONFLICT.

Example create
mutation CreateSubscription($input: CreateSubscriptionInput!) {
  createSubscription(input: $input) {
    id
    externalId
    status
    period
    currency
    startDate
    endDate
    autoRenew
  }
}

{
  "input": {
    "clientExternalId": "acct_123",
    "externalId": "sub-acme-growth-2026",
    "bundleId": "<bundle-id>",
    "period": "MONTHLY",
    "startDate": "2026-08-01T00:00:00.000Z",
    "components": [
      { "componentId": "<component-id>", "startingQuantity": 2 }
    ]
  }
}

Example read
query ClientBillingStatus($externalId: String!) {
  client(externalId: $externalId) {
    id
    name
    invoices(status: "OVERDUE", limit: 10) {
      invoiceNumber
      dueDate
      amount { amount currency }
      paymentStatus
      pdfUrl
    }
    paymentSummary { currency outstanding overdue overdueCount }
  }
}

External id namespaces
- A client's externalId and a subscription's externalId are separate fields on separate objects. The usage and event APIs resolve externalSubscriptionId against the SUBSCRIPTION's externalId — passing a client externalId there returns 404.

Errors
- Handle GraphQL errors through errors[].extensions.code. The stable codes are UNAUTHENTICATED (401), FORBIDDEN (403 — missing WRITE, or out-of-key-scope), RATE_LIMITED (429), BAD_REQUEST (400), NOT_FOUND (404), CONFLICT (409, e.g. duplicate client name or externalId), QUERY_DEPTH_EXCEEDED, and INTERNAL_ERROR (server faults are masked).
- Do not treat a non-2xx HTTP status alone as the only signal; inspect the GraphQL body.
- Retry only UNAUTHENTICATED-free transport faults and RATE_LIMITED (after Retry-After). CONFLICT / BAD_REQUEST / NOT_FOUND will not change on retry.

Implementation checklist
1. Confirm whether the operator needs create, read, or both — and, for create, whether it is a client, a subscription, or both.
2. Confirm how they will choose regionId (fixed region vs lookup).
3. Confirm the externalId scheme they already use in their system of record — a client and its subscription each need their own.
4. For a subscription, confirm the bundle/module/component selection and any starting quantities (components[].startingQuantity) with the operator; do not invent catalog ids.
5. Implement server-side only, with retries that are safe for the operations you call.

You are integrating an external form or webhook with Revvly CRM so each submission becomes a pipeline-ready prospect.

Working agreement before you implement:
- Do not write production code until you have enough concrete context from the operator.
- Ask clarifying questions first — prefer one short round of questions over guessing.
- Confirm at least: source system / trigger, field mapping, where the API key and any signing secret will be stored, the target environment (local, staging, or production), and the Revvly tenantId when it is not already provided below.
- Once those answers are in place, implement the full integration against the contract below end-to-end.
- Open or fetch the Full guide URL if you need examples or edge-case detail beyond this prompt.
- Never invent API keys, signing secrets, region ids, subscription ids, or other tenant data.
- Never put secrets in browser code, logs, client bundles, or source control.

Tenant context
- The operator has not supplied a tenantId in this prompt. Ask for the Revvly tenant id before making tenant-scoped assumptions.

Platform rules that apply to every Revvly API call:
- Auth is a Bearer API key issued in the dashboard under Settings → API Keys. Tokens are prefixed `rvly_sk_live_`.
- Rate limits are per API key and default to 10 requests/second and 100 requests/minute. Overage is HTTP 429 with a `Retry-After` header in seconds (public GraphQL reports `errors[].extensions.code = RATE_LIMITED`). Honour Retry-After and back off — never retry a 429 in a tight loop.
- A key can be scoped to specific subscriptions and meters. List reads are filtered to that scope; reading or writing a single out-of-scope resource is 403 (`FORBIDDEN`), not an empty result.
- Every call is tenant-scoped by the key itself. There is no tenant parameter to switch — a different tenant means a different key.
- The token and the signing secret are server-side credentials: no browser code, no client bundles, no logs, no source control.

Full guide: https://www.revvly.ai/docs/guides/prospect-form-integration

Goal
Turn one inbound form/webhook event into a Revvly prospect: organization (company) + contact (person) + lead (deal the sales team works).

Endpoint and auth
- POST https://api.revvly.ai/api/v1/graphql from a trusted backend or webhook worker only.
- Authorization: Bearer <API_KEY>
- Content-Type: application/json
- The API key needs WRITE capability ("Read & write" access level at issue time). CRM must be enabled for the tenant.
- Browser forms must POST to your backend; your backend forwards to Revvly. Never call Revvly from the browser with the API key.

Mutation
Use createProspect(input: CreateProspectInput!).
Do NOT use createClient — that creates a billing client, not a CRM prospect, and requires a region.

Required / optional fields
- organization.name — required (2–100 chars)
- organization.domain, email, description, industry, regionId — optional
- contact — include at least one of firstName, lastName, or email (enforced server-side; a contact with only a phone is rejected)
- lead.title — required (what the form is about, e.g. "Website demo request")
- lead.externalRef — required; set to the form provider's immutable submission or event id
- lead.estimatedValue, currency, and similar lead fields — optional when the form provides them
- Omit pipelineId and pipelineStageId unless the form must land in a specific pipeline; defaults are the tenant default pipeline and its first Open stage

Idempotency / retries
- Reuse the same lead.externalRef unchanged when the provider retries the same event.
- Do not generate a new externalRef on every delivery.
- A completed retry returns the previously created organization, contact, and lead instead of duplicating them.
- lead.externalRef is its own namespace. It is unrelated to a client's externalId or a subscription's externalId — do not reuse one as the other.

Validation and errors
- Organization domains and contact emails are unique per tenant; duplicates return errors[].extensions.code = CONFLICT.
- CONFLICT is also returned when a lead already exists for the externalRef but its organization or contact can no longer be resolved — that is a data-repair case, not something a retry fixes.
- FORBIDDEN means either CRM is not enabled for the tenant or the key lacks WRITE. UNAUTHENTICATED means the token is bad, revoked, or past its rotation grace window.
- Custom / arbitrary form fields are not a supported public API contract yet — send only documented organization, contact, and lead fields.
- Surface GraphQL failures through errors[].extensions.code; do not assume HTTP status alone.

Example mutation shape
mutation CreateProspect($input: CreateProspectInput!) {
  createProspect(input: $input) {
    organization { id name domain }
    contact { id firstName lastName email }
    lead { id title externalRef pipelineId pipelineStageId }
  }
}

Example variables
{
  "input": {
    "organization": {
      "name": "Northstar Labs",
      "domain": "northstarlabs.example",
      "industry": "Software"
    },
    "contact": {
      "firstName": "Avery",
      "lastName": "Chen",
      "email": "[email protected]"
    },
    "lead": {
      "title": "Enterprise demo request",
      "externalRef": "form-submission-evt_01J8K3",
      "estimatedValue": 24000,
      "currency": "USD"
    }
  }
}

Implementation checklist
1. Verify webhook authenticity on your side before forwarding.
2. Map provider fields → organization / contact / lead.
3. Persist or log the Revvly ids returned for support, but keep using the provider submission id as externalRef for retries.
4. Add tests for happy path, retry with the same externalRef, and CONFLICT on duplicate domain/email when that is a real risk.

#Fetch a client

Look a client up by its Revvly id or by your own externalId — supply exactly one. Select only the fields you need.

curl -s -X POST https://api.revvly.ai/api/v1/graphql \
  -H "Authorization: Bearer $REVVLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query($ref: String!) { client(externalId: $ref) { id name region { id defaultCurrency } subscriptions { id status } invoices { invoiceNumber paymentStatus amount { amount currency } } paymentSummary { currency outstanding overdue } } }",
    "variables": { "ref": "acct_123" }
  }'

Arguments

idone of
The internal Revvly client id.
externalIdone of
Your own reference, set on the client (unique per tenant). Supplying both id and externalId, or neither, is a BAD_REQUEST.
Response
{
  "data": {
    "client": {
      "id":         "663b1f2e4a3c2d1e0f8b7a01",
      "externalId": "acct_123",
      "name":       "Acme Corporation",
      "region": {
        "id":              "663b1f2e4a3c2d1e0f8b7a55",
        "name":            "United States",
        "defaultCurrency": "USD"
      },
      "subscriptions": [
        {
          "id":                      "663b…c1",
          "status":                  "ACTIVE",
          "period":                  "MONTHLY",
          "currency":                "USD",
          "autoRenew":               true,
          "billingArrangement":      "PREPAID_TERM",
          "planChargeBilledThrough": "2026-09-01T00:00:00.000Z",
          "awaitingActivation":      false
        }
      ],
      "invoices": [
        {
          "invoiceNumber": "ACME-00007",
          "paymentStatus": "OVERDUE",
          "dueDate":       "2026-06-04T00:00:00.000Z",
          "amount":        { "amount": 1200.00, "currency": "USD" },
          "pdfUrl":        "https://files.revvly.com/…/ACME-00007.pdf?X-Amz-Expires=1800&…"
        }
      ],
      "paymentSummary": [
        { "currency": "USD", "outstanding": 1200.00, "overdue": 1200.00, "overdueCount": 1 }
      ]
    }
  }
}

What you can read on a client

Beyond the client’s own fields, four related selections let you pull a customer’s whole billing picture in one request instead of stitching several REST calls together.

regionRegion
The client’s billing region resolved to a full object — name, code, defaultCurrency, supportedCurrencies. The bare regionId is still there if you only need the id. Null if the region was removed.
subscriptions[Subscription!]!
The client’s subscriptions, newest first — status, period, currency, startDate/endDate, autoRenew, and your own externalId. Three further fields describe how the subscription is invoiced, separately from what it costs — billingArrangement, planChargeBilledThrough and awaitingActivation. Read them before reconciling invoices; see below.
invoices[Invoice!]!
The client’s invoices, newest first, each with a Money amount, paymentStatus (PAID / PENDING / OVERDUE / FAILED / CANCELLED), and dueDate / paidDate. When the invoice is taxed, amount includes tax; subtotal is the net, tax the total tax and taxBreakdown the tax by code and rate (all null or empty when untaxed). A zero-amount invoice that is awaiting collection is settled the moment it is issued: it reads PAID with paidDate equal to its invoiceDate, and never counts as outstanding or overdue — there is nothing to collect on it. A zero-amount invoice you explicitly cancelled or marked failed keeps that status, so do not treat amount of zero as implying PAID — read paymentStatus. Each invoice also exposes pdfUrl — a short-lived (30-minute) presigned link to download the invoice PDF, minted fresh per request and null when no PDF has been generated yet. Request it only when you intend to download. Optional arguments: status (filter to one payment status) and limit (default 100, max 1000). This list stops at the newest limit invoices; to read a longer history, use invoicesPage.
invoicesPageInvoicePage!
The client’s whole invoice history, one page at a time, newest first. It returns data (the same Invoice objects as above) and nextCursor. Pass nextCursor back as after until it is null. A page never repeats or skips an invoice, even one issued while you are paging. Arguments: status, limit (default 100, max 1000 per page) and after. The cursor is opaque: one the API did not issue is a BAD_REQUEST (“Invalid cursor”), not an empty page. The status filter and the key’s scope apply on every page.
paymentSummary[PaymentSummary!]!
Receivable rollups so you don’t compute them yourself — outstanding (unpaid, non-cancelled) and overdue totals plus counts, returned one entry per currency so multi-currency clients aren’t summed into a meaningless mixed total.
customFields[ClientCustomField!]!
The custom fields filled in on this client, read-only. Each entry has key, label, fieldType and valueJson, the value JSON-encoded (a string, number, boolean or list of strings; a select field holds option keys). Fields your workspace has limited to a role are not listed.

#How a subscription is invoiced

A subscription’s price and its invoicing schedule are two different things. Three read-only fields on Subscription describe the schedule, so that a renewal which legitimately raises no invoice doesn’t read to your integration as a missing one.

iRead these before reconciling invoices. Under PREPAID_TERM, several periods are billed on one invoice at signing — the plan and the components billed ahead with it — so a renewal inside that span raises no plan charge, invoices a component only for units added since signing, and raises no invoice at all when there is nothing to bill. An integration that treats each of those as a missing invoice double-counts the span.
billingArrangementString!
One of STANDARD (one plan charge per period, on renewal), PREPAID_TERM (several periods billed upfront on a single invoice, with the components billed ahead) or DEFERRED_ACTIVATION (part of the first period charged at signing, the rest when the customer goes live). Always present; STANDARD is the default and behaves exactly as it always has.
planChargeBilledThroughString
ISO 8601, or null when nothing is invoiced ahead. How far the plan charge has already been billed past the renewal date. Any renewal ending on or before this date raises no plan charge. Components billed ahead were prepaid with it, so a renewal invoices them only for units added since signing; usage counted per period still bills after each period, so a usage-only invoice inside the span is expected, not an anomaly — and so is a renewal with no invoice at all. Prefer checking this field over checking billingArrangement: an ordinary STANDARD subscription also sets it when the account has a renewal lead time that invoices ahead.
awaitingActivationBoolean!
True while the subscription is signed but not yet switched on. It earns no revenue, renews nothing, and rejects usage with a 409 until it is activated. Its startDate and endDate are provisional while this is true — activation re-anchors the term to the real go-live date — so don’t treat them as the contract term or use them to schedule anything.
trialEndsAtString
ISO 8601. When the free trial ends and the first period is invoiced. While status is TRIAL nothing is billed and startDate / endDate describe the trial, not a paid term. When it converts, startDate moves to the day paid service began. Null when there was no trial.
trialConvertedAtString
ISO 8601. When the trial converted to paid. Null until it does.

#Create a prospect from a form

Use createProspect for a CRM form submission, not createClient. One call creates a Prospect organization, its primary Contact, and a Lead in the tenant’s default pipeline. It requires both a WRITE-capable key and CRM enabled for the tenant.

iUse the form provider’s immutable submission id as lead.externalRef. It is required and makes a completed submission safe to retry: Revvly returns the existing prospect instead of making another one.
mutation CreateProspect($input: CreateProspectInput!) {
  createProspect(input: $input) {
    organization { id name domain email }
    contact { id firstName lastName email phone }
    lead { id title externalRef pipelineId pipelineStageId }
  }
}

# variables
{
  "input": {
    "organization": {
      "name": "Northstar Labs",
      "domain": "northstarlabs.example",
      "industry": "Software"
    },
    "contact": {
      "firstName": "Avery",
      "lastName": "Chen",
      "email": "[email protected]",
      "phone": "+1 415 555 0142"
    },
    "lead": {
      "title": "Enterprise demo request",
      "externalRef": "hubspot-form-evt_01J8K3",
      "estimatedValue": 24000,
      "currency": "USD"
    }
  }
}

Input rules

organization
A name is required; domain, email, description, industry, and regionId are optional. A supplied domain is normalized and must be unique per tenant.
contact
Include at least one of first name, last name, or email. Contact email addresses are unique per tenant when supplied.
lead
Title and externalRef are required. Pipeline and stage default to the tenant’s default pipeline and first Open stage; estimated value and currency are optional.
custom fields
Custom fields are not part of the public API yet. Do not send arbitrary form fields; only the documented inputs are stored.
Response
{
  "data": {
    "createProspect": {
      "organization": {
        "id": "663b1f2e4a3c2d1e0f8b7b01",
        "name": "Northstar Labs",
        "domain": "northstarlabs.example",
        "email": null
      },
      "contact": {
        "id": "663b1f2e4a3c2d1e0f8b7b02",
        "firstName": "Avery",
        "lastName": "Chen",
        "email": "[email protected]",
        "phone": "+1 415 555 0142"
      },
      "lead": {
        "id": "663b1f2e4a3c2d1e0f8b7b03",
        "title": "Enterprise demo request",
        "externalRef": "hubspot-form-evt_01J8K3",
        "pipelineId": "663b1f2e4a3c2d1e0f8b7b04",
        "pipelineStageId": "663b1f2e4a3c2d1e0f8b7b05"
      }
    }
  }
}

#Create a client

The createClient mutation creates a client in your tenant and returns it. Only name and regionId are required — every other field is optional. This is a write, so it needs an API key granted the WRITE capability; a READ-only key is rejected with FORBIDDEN.

mutation CreateClient($input: CreateClientInput!) {
  createClient(input: $input) {
    id
    externalId
    name
    region { id name }
  }
}

# variables
{
  "input": {
    "name": "Acme Corporation",
    "regionId": "663b1f2e4a3c2d1e0f8b7a55",
    "externalId": "acct_123",
    "billingAddress": "1 Market St, San Francisco, CA",
    "taxId": "US-123456789",
    "paymentTermsDays": 30,
    "renewalInvoiceLeadDays": { "monthly": 7, "yearly": 30 }
  }
}

Input fields

namerequired
Display name, 2–100 chars. Must be unique per tenant (case-insensitively) — a duplicate returns CONFLICT.
regionIdrequired
The id of an active region (see the regions discovery endpoint). An unknown region returns NOT_FOUND; an inactive one, BAD_REQUEST.
externalId
Your own reference for the client (unique per tenant, max 100). A duplicate returns CONFLICT.
incubatorId
Optional incubator / partner reference.
invoicePrefix
Prefix for the client’s invoice numbers, e.g. INV-ACME yields INV-ACME-00042 (max 50). Unique per tenant; a duplicate returns CONFLICT. Omit it and one is derived from the client name.
nextInvoiceNumber
The sequence number the client’s next invoice should carry (min 1). Set it when the client already has invoice history elsewhere and the numbering has to continue — a client at invoice 14 takes 15. Consumed by the first generated invoice; after that the sequence continues on its own.
description
Free-text description (max 500).
billingAddress / taxId
Billing address (max 500) and tax / VAT id (max 50).
paymentTermsDays
Payment terms in days; overrides the tenant default when set.
renewalInvoiceLeadDays
Per-period (monthly / quarterly / yearly) override of how many days before renewal to raise the invoice. Each period left unset inherits the tenant default; 0 disables it.
isTrialActive
Whether the client starts on a trial (default false). When true, both trialStartDate and trialEndDate (ISO 8601) are required and start must precede end.
Response
{
  "data": {
    "createClient": {
      "id":         "663b1f2e4a3c2d1e0f8b7a01",
      "externalId": "acct_123",
      "name":       "Acme Corporation",
      "region":     { "id": "663b1f2e4a3c2d1e0f8b7a55", "name": "United States" }
    }
  }
}

#Update a client

The updateClient mutation corrects a client and returns it. Name the client with exactly one of clientId or clientExternalId, then send only the fields you want to change: a field you leave out is left alone. It needs WRITE.

name
2–100 chars, unique in your tenant. Cannot be cleared.
externalId / description / billingAddress / taxId
Same limits as on create. null clears the field. A new externalId already used by another client returns CONFLICT.
paymentTermsDays / renewalInvoiceLeadDays
Cannot be cleared. For renewalInvoiceLeadDays, only the periods you send change.
iThe region, the invoice prefix and numbering, trial dates and CRM fields are not part of the input, so they cannot be changed through the API. Sending null for a field that cannot be cleared, or a request that changes nothing, is a BAD_REQUEST. A scoped key reaches only clients that own a subscription in its scope; any other client reads as NOT_FOUND.

#Create a subscription

The createSubscription mutation provisions a subscription for a client and returns it. It wraps the same pricing engine the dashboard’s subscription wizard uses, behind a curated input — enough to onboard a subscription end-to-end from an integration, without the wizard’s full authoring surface (see what’s excluded, below). Like createClient, this is a write and needs an API key granted the WRITE capability.

iSet externalId here so the usage-ingestion API can find this subscription by externalSubscriptionId. Without it, a subscription created only through createClient has nothing for a usage-reporting integration to resolve against — every usage call for that client fails with 404 until someone sets it by hand on the dashboard.
mutation CreateSubscription($input: CreateSubscriptionInput!) {
  createSubscription(input: $input) {
    id
    externalId
    status
    period
    currency
    startDate
    endDate
    autoRenew
  }
}

# variables
{
  "input": {
    "clientExternalId": "h_4471",
    "externalId": "h_4471",
    "bundleId": "663b1f2e4a3c2d1e0f8b7a9e",
    "period": "MONTHLY",
    "startDate": "2026-09-24T00:00:00.000Z",
    "components": [
      { "componentId": "663b1f2e4a3c2d1e0f8b7b10", "startingQuantity": 2 }
    ]
  }
}

Input fields

clientIdone of
The client’s Revvly id.
clientExternalIdone of
The client’s own externalId, as set on createClient or the dashboard. Supplying both clientId and clientExternalId, or neither, is a BAD_REQUEST. An unresolved client is NOT_FOUND.
externalId
Your own reference for the subscription (unique per tenant). A duplicate returns CONFLICT.
periodrequired
One of MONTHLY, QUARTERLY, YEARLY.
currency
Must be one of the client’s region’s supported currencies; defaults to the region’s default currency.
startDaterequired
ISO 8601.
endDate
ISO 8601. Optional for an auto-renewing subscription.
autoRenew
Whether the subscription auto-renews. Default true.
bundleId / moduleId / moduleIds / variableComponentIds / componentsat least one
What the subscription bills — a bundle, one or more modules, standalone Variable Components, or components attached directly via components (below). At least one is required; a subscription that bills nothing is a BAD_REQUEST.
components
Variable Components attached directly, each with its own componentId, optional includedQuantity (units included before overage), and optional startingQuantity — the quantity provisioned at creation, e.g. a starting seat or child count. That seeds the initial usage counter, so the subscription-start invoice bills any overage above the inclusion correctly, and usage reported afterward through the usage-ingestion API is a delta from that baseline rather than a raw total.
trialDays
A free trial of 1 to 365 days. The subscription starts as TRIAL: usage can be reported (add seats during the trial), nothing is invoiced and nothing counts as revenue. When the trial ends it converts to ACTIVE on its own, its first period starts that day and is invoiced in advance at the quantities reported by then. startDate must not be in the future, and endDate cannot be combined with a trial.
notes
Free-text notes about the subscription.
Response
{
  "data": {
    "createSubscription": {
      "id":         "663b1f2e4a3c2d1e0f8b7c01",
      "externalId": "h_4471",
      "status":     "ACTIVE",
      "period":     "MONTHLY",
      "currency":   "USD",
      "startDate":  "2026-09-24T00:00:00.000Z",
      "endDate":    "2026-10-24T00:00:00.000Z",
      "autoRenew":  true
    }
  }
}
iDeliberately not exposed here: historical invoice backfill, billing schedules, initial discounts, legacy catalog-version pins, and plan-change timelines — the dashboard subscription wizard’s authoring surface for migrating existing subscription histories. Those stay dashboard-only; this mutation covers the common case of provisioning a new subscription going forward. To change the plan or cancel later, see Work with an existing subscription.

#Work with an existing subscription

Every write takes a subscription by its Revvly id or your externalId and needs WRITE. A subscription outside a scoped key reads as not found.

subscription(id | externalId)
One subscription. Its items list what it bills for: the plan (bundle, modules or a custom price) with its price per period, and each metered component with the quantity it stands at now and how many are included.
changeSubscriptionQuantity
Set a metered component’s total, for example seats. An increase on a component billed in advance is invoiced now for the rest of the period, prorated by day, and the invoice comes back; a decrease takes effect without a credit. Nothing is invoiced during a free trial. Pass preview: true to see chargedNow before committing. Asking for the quantity it already has returns changed: false, so a retry is safe.
changeSubscriptionPlan
Move a subscription to another bundle or set of modules. Send bundleId and/or moduleIds (the exact set of modules it should have), an optional reason kept in its history, and timing: IMMEDIATE invoices or credits the difference for the rest of the period now; NEXT_RENEWAL applies the change at the next renewal and invoices nothing today. Leave a field out to keep it, send bundleId: null to remove the bundle, or an empty moduleIds to remove the modules; the subscription must keep a bundle or at least one module. A change the plan rules refuse comes back as BAD_REQUEST with the reason. Recording a past change, pinning an archived catalog version, choosing a tier and settling a prepaid span stay in the dashboard.
cancelSubscription
It stays in service until its end date and does not renew. Cancelled during a free trial, it is never billed.
setSubscriptionAutoRenew
Turn automatic renewal on or off.
endSubscriptionTrial
End a free trial now: the first period starts today and is invoiced in advance.
invoice(id)
One invoice, or null.

Save a card on file

With Stripe connected under Settings → Payments, invoices are charged to the client’s card when they are issued. The card is collected on your own page by Stripe.js and never passes through Revvly.

startPaymentMethodSetup
Takes a client by clientId or clientExternalId and returns clientSecret, publishableKey and, on a connected account, stripeAccountId. Load Stripe.js with those and confirm the setup with the Payment Element; once Stripe confirms it, the card becomes the client’s default. Needs WRITE.
Client.paymentMethod
The card on file — brand, last four digits, expiry — or null.

Errors

Errors follow the GraphQL convention — an errors array with a stable code in extensions.code (also mirrored as a top-level code). Internal details are never leaked in production.

{
  "errors": [
    {
      "message": "No client found for externalId \"ghost\"",
      "code": "NOT_FOUND",
      "path": ["client"],
      "extensions": { "code": "NOT_FOUND" }
    }
  ],
  "data": { "client": null }
}
UNAUTHENTICATED
Missing or invalid API key.
FORBIDDEN
Key lacks the capability the operation requires.
BAD_REQUEST
Invalid arguments (e.g. both or neither of id / externalId).
NOT_FOUND
No matching resource in your tenant.
CONFLICT
A uniqueness constraint was violated — e.g. creating or renaming a client to a name or externalId another client in your tenant already uses.
RATE_LIMITED
Per-key rate limit exceeded; see Retry-After.
QUERY_DEPTH_EXCEEDED
Query nesting exceeds the allowed depth.

How the schema changes

The schema expands additively (new fields and types, deprecations via @deprecated — never breaking changes), so what you build today keeps working. New fields and mutations are announced in the changelog.

Related