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.
#Endpoint & authentication
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> 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
{
"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.
#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.
#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.
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
{
"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
{
"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.
#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.
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
{
"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
}
}
}#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.
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.
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 }
}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.