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

Guide

# Provision and read clients through *GraphQL*.

Give a backend one intentional contract for billing client creation and receivables reads.

## Give this prompt to an integration agent

```
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.
```

## Example 1: provision a billing client

A client is billing-ready, so a valid active region is required. This is separate from the CRM prospect flow, which does not require a region.

```
// 1. Discover an active region with GET https://api.revvly.ai/api/v1/regions.
// 2. Create the billing client through POST https://api.revvly.ai/api/v1/graphql.
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
  }
}
```

## Example 2: provision a subscription for that client

Set the subscription’s own externalId at creation — separate from the client’s — so the usage-ingestion API can find it by externalSubscriptionId. components[].startingQuantity seeds an initial count (e.g. starting seats) so the first invoice bills correctly.

```
// 3. Provision a subscription for that client, with its own externalId.
mutation CreateSubscription($input: CreateSubscriptionInput!) {
  createSubscription(input: $input) {
    id
    externalId
    status
    period
    currency
  }
}

{
  "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 3: show a customer’s outstanding balance

Query by your own external id and ask only for overdue invoices and per-currency payment totals. Request the PDF link only when rendering a download action.

```
// POST https://api.revvly.ai/api/v1/graphql
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 }
  }
}

{ "externalId": "acct_123" }
```

## Related

- [Public GraphQL API reference ›](https://www.revvly.ai/docs/api/graphql-public)
- [Prospect form integration ›](https://www.revvly.ai/docs/guides/prospect-form-integration)

