> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dncscrub.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create or Update Project (v1.5)

> Create a Project for a client, register its FTC Org Id and SAN, and submit state registrations, all in one call

Use this endpoint when you operate a platform with many clients of your own and each
client must scrub under their **own** FTC Org Id and Subscription Account Number (SAN).
One DNCScrub Project per client keeps every client's National DNC subscription, Internal
DNC list and settings separate. Federal law does not allow one SAN to be shared across
organizations.

<Note>
  **During rollout**, FTC Org Id / SAN validation is completed by our team rather than
  automatically: allow up to one business day for `FtcValidation.Status` to leave `pending`.
  Webhooks are not sent yet, so poll [Get Project](/api-reference/other/projects-get)
  (no more than once a minute). Automatic validation and webhook delivery follow in a later
  release; requests and responses do not change.
</Note>

`PUT` is idempotent: the same request creates the Project on first call and updates it
afterwards. The Project code is the path segment; you choose it.

### Update semantics

On update, the body is **merged** into the existing Project:

* A field you omit (or send as `null`) is left unchanged.
* Send an empty string to clear `Notes` or `WebhookUrl`. Clearing `WebhookUrl` also discards
  the stored secret. `WebhookSecret` cannot be cleared on its own (`400`); sending a new
  value rotates it.
* `ProjectName` is required only when the Project is created.
* `FtcOrgId` and `FtcSan` are always sent together; sending a new pair replaces the old one
  and starts a new validation. Resending the current pair is a no-op unless its last
  validation failed, in which case it is queued again. A pair with a different Org Id
  suspends the Project's National DNC scrubbing until the new pair is validated; a new SAN
  under the same Org Id does not. They cannot be cleared through the API.
* `StateRegistrations` is merged per state key: `{ "OK": {...} }` adds or replaces OK and
  leaves LA untouched; `{ "OK": null }` withdraws OK. New or changed proof is emailed to Support for
  review. Neither a change nor a withdrawal changes your account's LA/OK scrubbing by itself.
* `UseMaster*` flags change only when present.

## What happens after you call it

1. The Project (and its default Campaign) exists immediately and can accept uploads and
   API scrubs against Internal DNC, state lists you are authorized for, litigator lists, etc.
2. The FTC Org Id and SAN you supplied are queued for validation against the FTC's
   telemarketer portal. `FtcValidation.Status` starts as `pending`.
3. When validation completes (usually within a couple of minutes; up to a few hours if
   the FTC portal is unavailable), the
   Project is enabled for National DNC scrubbing, `FtcValidation.Status` becomes
   `validated` (or `failed` with a reason), and if you supplied a `WebhookUrl` we POST the
   result to it. You can also poll [Get Project](/api-reference/other/projects-get).
4. Any `StateRegistrations` you supplied are routed to our Support team for review. See
   [Louisiana and Oklahoma Registration](/api-reference/other/state-registrations).

## Request

### Headers

<ParamField header="loginId" type="string" required>
  Your API Key. An OAuth `Authorization: Bearer` token is accepted instead.
</ParamField>

<ParamField header="Content-Type" type="string" required>
  `application/json`
</ParamField>

### Path

<ParamField path="projectCode" type="string" required>
  Your identifier for the Project. 2 to 23 characters, letters and digits only; lowercase
  is uppercased. If your account has auto-prefixing enabled, `<acctId>_` is prepended
  when missing, so `BRIGHTDENTAL` becomes `VOXAI_BRIGHTDENTAL`; the response and all later
  calls use the prefixed code, and either form is accepted on later calls. The prefixed
  code must fit in 30 characters. Reserved: codes starting with `TMPL` or equal to `DNC`
  or `ROOT`.
</ParamField>

### Body

<ParamField body="ProjectName" type="string">
  Display name, usually your client's company name. Up to 50 characters. Required when
  creating; optional on update.
</ParamField>

<ParamField body="FtcOrgId" type="string">
  Your client's FTC Organization Id, for example `10000598-10598`. Required if `FtcSan` is
  supplied.
</ParamField>

<ParamField body="FtcSan" type="string">
  Your client's Subscription Account Number, for example `10000830-100830-07`. Required if
  `FtcOrgId` is supplied. The SAN must belong to the Org Id; we verify that on the FTC
  portal.
</ParamField>

<ParamField body="StateRegistrations" type="object">
  Proof that your client is registered as a telemarketer in a state that requires it before
  its state Do Not Call list may be scrubbed. Keys are state abbreviations, currently `LA`
  and `OK`. Each value: `{ "Confirmation": string, "Notes": string }`. `Confirmation` is
  the registration or confirmation number the state issued (up to 100 characters; `Notes`
  up to 1000). Supporting documents go to
  [support@dnc.com](mailto:support@dnc.com), not through the API. See
  [Louisiana and Oklahoma Registration](/api-reference/other/state-registrations).
</ParamField>

<ParamField body="WebhookUrl" type="string">
  HTTPS URL (up to 500 characters) we POST to when FTC validation completes. See
  [Webhook](#webhook). Must resolve to a public host: URLs with
  embedded credentials, non-443 ports, or hosts that resolve to loopback, private,
  link-local or reserved addresses are rejected with `400`, both when saved and again at
  delivery time. Redirects are not followed. Requires `WebhookSecret`.
</ParamField>

<ParamField body="WebhookSecret" type="string">
  Shared secret, 16 to 128 characters, used to sign every delivery with HMAC-SHA256.
  Required when `WebhookUrl` is set (`400` otherwise). Stored encrypted and never returned
  by the API; rotate it by sending a new value.
</ParamField>

<ParamField body="Notes" type="string">
  Free-form notes shown in the DNCScrub portal. Up to 512 characters.
</ParamField>

<ParamField body="UseMasterDncDb" type="boolean" default="false">
  Share the Master Project's Internal DNC list with this Project. Leave `false` when each
  client must keep its own Internal DNC.
</ParamField>

<ParamField body="UseMasterEbrDb" type="boolean" default="false">
  Share the Master Project's EBR / exemption database.
</ParamField>

<ParamField body="UseMasterDncPolicy" type="boolean" default="false">
  Inherit the Master Project's DNC policy.
</ParamField>

<ParamField body="UseMasterDncSub" type="boolean" default="false">
  Inherit the Master Project's National DNC subscription instead of `FtcOrgId`/`FtcSan`.
  Only valid when the client is the same legal organization as the master account.
</ParamField>

<ParamField body="UseMasterTraining" type="boolean" default="false">
  Inherit the Master Project's training settings.
</ParamField>

## Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl --request PUT 'https://dataapi.dncscrub.com/v1.5/projects/BRIGHTDENTAL' \
    --header 'loginId: YOUR_API_KEY' \
    --header 'Content-Type: application/json' \
    --data '{
      "ProjectName": "Bright Dental",
      "FtcOrgId": "10000598-10598",
      "FtcSan": "10000830-100830-07",
      "StateRegistrations": {
        "LA": { "Confirmation": "LPSC-2026-1234" },
        "OK": { "Confirmation": "OK-AG-55512" }
      },
      "WebhookUrl": "https://example.com/dncscrub/webhook",
      "WebhookSecret": "a-random-secret-of-at-least-16-chars"
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://dataapi.dncscrub.com/v1.5/projects/BRIGHTDENTAL", {
    method: "PUT",
    headers: { loginId: "YOUR_API_KEY", "Content-Type": "application/json" },
    body: JSON.stringify({
      ProjectName: "Bright Dental",
      FtcOrgId: "10000598-10598",
      FtcSan: "10000830-100830-07",
      WebhookUrl: "https://example.com/dncscrub/webhook",
      WebhookSecret: "a-random-secret-of-at-least-16-chars",
    }),
  });
  console.log(res.status); // 201 on create, 200 on update
  const project = await res.json();
  console.log(project.FtcValidation.Status); // "pending"
  ```

  ```csharp C# theme={null}
  using var client = new HttpClient();
  client.DefaultRequestHeaders.Add("loginId", "YOUR_API_KEY");

  var body = new
  {
      ProjectName = "Bright Dental",
      FtcOrgId = "10000598-10598",
      FtcSan = "10000830-100830-07",
      WebhookUrl = "https://example.com/dncscrub/webhook",
      WebhookSecret = "a-random-secret-of-at-least-16-chars",
  };

  var res = await client.PutAsJsonAsync("https://dataapi.dncscrub.com/v1.5/projects/BRIGHTDENTAL", body);
  var project = await res.Content.ReadFromJsonAsync<JsonElement>();
  Console.WriteLine(project.GetProperty("FtcValidation").GetProperty("Status")); // pending
  ```
</CodeGroup>

## Response

| Status | Meaning |
| - | - |
| `201` | Project created |
| `200` | Project updated |
| `400` | Validation error (bad id format, `FtcOrgId` without `FtcSan`, `WebhookUrl` without `WebhookSecret`, non-public webhook host). `ReasonPhrase` names the field. |
| `401` | Missing or invalid API key |
| `403` | Project exists but is inactive. Reactivate it in the portal or contact Support. Also returned when your user lacks the permission to create Projects (`insert_project`) or change them (`update_project`); contact Support if your user needs it. |
| `404` | `projectCode` belongs to another account |

```json 201 Created theme={null}
{
  "ProjectCode": "VOXAI_BRIGHTDENTAL",
  "ProjectName": "Bright Dental",
  "AcctId": "VOXAI",
  "Status": "active",
  "DefaultCampaignId": 184201,
  "Notes": "",
  "UseMasterDncDb": false,
  "UseMasterEbrDb": false,
  "UseMasterDncPolicy": false,
  "UseMasterDncSub": false,
  "UseMasterTraining": false,
  "FtcValidation": {
    "Status": "pending",
    "OrgId": "10000598-10598",
    "San": "10000830-100830-07",
    "CheckedOn": null,
    "Message": null,
    "Expiration": null,
    "HasEntireUs": null,
    "AreaCodes": null
  },
  "StateRegistrations": {
    "LA": { "Status": "submitted", "Confirmation": "LPSC-2026-1234", "SubmittedOn": "2026-10-01T17:02:11Z" },
    "OK": { "Status": "submitted", "Confirmation": "OK-AG-55512", "SubmittedOn": "2026-10-01T17:02:11Z" }
  },
  "Webhook": { "Url": "https://example.com/dncscrub/webhook", "LastDelivery": null },
  "CreatedOn": "2026-10-01T17:02:11Z",
  "LastModifiedOn": "2026-10-01T17:02:11Z"
}
```

`Status` is `active` or `inactive`.

### ftcValidation.status

| Value | Meaning |
| - | - |
| `none` | No Org Id / SAN supplied. Project cannot scrub against the National DNC list. |
| `pending` | Queued for verification against the FTC portal. National DNC scrubbing is not yet enabled. |
| `validated` | SAN confirmed under the Org Id. `Expiration` is the SAN's expiration date and `AreaCodes` lists the subscribed area codes (`HasEntireUs` true means all). National DNC scrubbing is enabled. |
| `failed` | Verification failed. `Message` gives the reason (Org Id not found, SAN not under this Org Id, no current subscription). Fix the values and call this endpoint again. |

## Webhook

Webhook delivery starts with automatic validation (see the rollout note at the top of this
page); until then, poll [Get Project](/api-reference/other/projects-get).

When `WebhookUrl` is set, we send `POST` requests with a JSON body:

```json theme={null}
{
  "Id": "evt_01J9F3ZQ6K8W2D5R7C0YV4T1XA",
  "Event": "project.ftcValidation",
  "OccurredOn": "2026-10-01T18:04:12Z",
  "ProjectCode": "VOXAI_BRIGHTDENTAL",
  "AcctId": "VOXAI",
  "FtcValidation": {
    "Status": "validated",
    "OrgId": "10000598-10598",
    "San": "10000830-100830-07",
    "CheckedOn": "2026-10-01T18:04:11Z",
    "Expiration": "2027-03-31",
    "HasEntireUs": false,
    "AreaCodes": ["212", "213", "310"],
    "Message": null
  }
}
```

Headers:

| Header | Value |
| - | - |
| `X-DNCScrub-Event` | `project.ftcValidation` |
| `X-DNCScrub-Event-Id` | Same as body `Id`. Deliveries are at-least-once; dedupe on it. |
| `X-DNCScrub-Timestamp` | Unix seconds when the delivery was sent |
| `X-DNCScrub-Signature` | `sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>" using webhookSecret>` |

Respond with any `2xx` within 10 seconds. Anything else, or a timeout, is retried up to
five times with exponential backoff (1 min, 5 min, 30 min, 2 h, 12 h), so six deliveries
at most over about 15 hours.
The most recent delivery outcome is visible on
[Get Project](/api-reference/other/projects-get) as `Webhook.LastDelivery`.

Verify the signature and reject stale timestamps (older than 5 minutes) before trusting
the body:

```javascript Verify signature (Node) theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, headers, secret) {
  const ts = Number(headers["x-dncscrub-timestamp"]);
  const given = headers["x-dncscrub-signature"];
  if (!Number.isFinite(ts) || typeof given !== "string") return false;
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Create the Project before collecting the SAN">
    Org Id and SAN are optional on create. Create the Project as soon as your client signs
    up, then call the endpoint again with `FtcOrgId` and `FtcSan` once they have them. Only
    National DNC scrubbing waits on validation.
  </Accordion>

  <Accordion title="Use the webhook, poll as a fallback">
    Validation completes asynchronously, usually within a couple of minutes. Subscribe
    with `WebhookUrl` and poll [Get Project](/api-reference/other/projects-get) no more
    than once a minute if you need a fallback.
  </Accordion>

  <Accordion title="Scrub with the Project's campaign">
    Pass the returned `DefaultCampaignId` as `campaignId` on
    [Scrub API](/api-reference/scrub/overview) calls so each client's numbers are scrubbed
    under that client's own subscription and Internal DNC list.
  </Accordion>

  <Accordion title="Legacy endpoint">
    `POST https://www.dncscrub.com/app/main/rpc/project`
    ([docs](/api-reference/other/project-create)) remains available for existing
    integrations. New integrations should use this endpoint.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.