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

Guide

# Turn a form event into a *pipeline-ready prospect*.

Send one public GraphQL mutation from your form webhook to create the prospect company, its primary contact, and the deal your team will work in Revvly.

## Before you start

Enable CRM for the tenant and issue an API key with the WRITE capability. Keep that key on your server or webhook worker only; a browser form must submit to your backend, not directly to Revvly. The public API is at https://api.revvly.ai/api/v1/graphql.

## Give this prompt to an integration agent

Copy this context into another coding or automation agent so it can build the integration without confusing a CRM prospect with a billing Client.

```
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": "avery@northstarlabs.example"
    },
    "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.
```

## Example 1: map a standard lead form

Map the company to organization, the person to contact, and the form intent to lead. A lead title and the provider’s submission id are required.

```
// POST https://api.revvly.ai/api/v1/graphql
mutation CreateProspect($input: CreateProspectInput!) {
  createProspect(input: $input) {
    organization { id name domain }
    contact { id firstName lastName email }
    lead { id title externalRef pipelineId pipelineStageId }
  }
}

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

## Example 2: forward a webhook from your server

This server-side example takes a normalized form event and forwards only the fields supported by the public contract. Add your own webhook signature verification before this handler runs.

From form event to prospect

1. Form is submittedYour form provider sends a webhook.
2. Your serverVerifies the webhook and holds the API key with WRITE.
3. createProspectOne public GraphQL mutation, with the submission id as externalRef.
4. New prospectCreates the company, contact and lead, in the default pipeline.orEvent delivered againReturns the records already created.orDomain or email takenReturns CONFLICT.

Your server sits between the form and Revvly, and the submission id is what makes a retry safe.

```
export async function submitToRevvly(formEvent: {
  id: string;
  company: string;
  website?: string;
  firstName?: string;
  lastName?: string;
  email?: string;
}) {
  const response = await fetch('https://api.revvly.ai/api/v1/graphql', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.REVVLY_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: `mutation CreateProspect($input: CreateProspectInput!) {
        createProspect(input: $input) {
          organization { id name }
          contact { id email }
          lead { id externalRef }
        }
      }`,
      variables: {
        input: {
          organization: { name: formEvent.company, domain: formEvent.website },
          contact: {
            firstName: formEvent.firstName,
            lastName: formEvent.lastName,
            email: formEvent.email,
          },
          lead: {
            title: 'Website demo request',
            externalRef: formEvent.id,
          },
        },
      },
    }),
  });

  const result = await response.json();
  if (result.errors) throw new Error(result.errors[0].message);
  return result.data.createProspect;
}
```

## Example 3: retry safely

Use an immutable provider event or submission id for lead.externalRef. Do not generate a new value on every delivery. If a completed event is delivered again, Revvly returns the previously created records instead of duplicating the prospect.

```
// Keep the same externalRef if the provider retries this event.
const prospect = await submitToRevvly({
  id: 'form-submission-evt_01J8K3',
  company: 'Northstar Labs',
  email: 'avery@northstarlabs.example',
});

// A completed previous delivery returns the same prospect, contact, and lead.
console.log(prospect.lead.externalRef);
```

## Defaults and validation

- **Pipeline placement**: Omit pipeline and stage to use the tenant’s default pipeline and its first Open stage. Supply ids only when the form belongs in a specific pipeline.

- **Duplicates**: Organization domains and contact email addresses are unique per tenant. A duplicate returns CONFLICT; use the provider submission id for retries, not as a way to merge separately submitted leads.

- **Custom form fields**: Revvly stores reserved custom-field data internally, but custom fields are not a supported public API contract yet. Send only organization, contact, and lead fields documented in the API reference.

## Related

- [Public GraphQL API reference ›](https://www.revvly.ai/docs/api/graphql-public)
- [Run your sales pipeline ›](https://www.revvly.ai/docs/guides/crm)

