Create or Update Project (v1.5)
curl --request PUT \
--url https://dataapi.dncscrub.com/v1.5/projects/{projectCode} \
--header 'Content-Type: <content-type>' \
--header 'loginId: <loginid>' \
--data '
{
"ProjectName": "<string>",
"FtcOrgId": "<string>",
"FtcSan": "<string>",
"StateRegistrations": {},
"WebhookUrl": "<string>",
"WebhookSecret": "<string>",
"Notes": "<string>",
"UseMasterDncDb": true,
"UseMasterEbrDb": true,
"UseMasterDncPolicy": true,
"UseMasterDncSub": true,
"UseMasterTraining": true
}
'import requests
url = "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}"
payload = {
"ProjectName": "<string>",
"FtcOrgId": "<string>",
"FtcSan": "<string>",
"StateRegistrations": {},
"WebhookUrl": "<string>",
"WebhookSecret": "<string>",
"Notes": "<string>",
"UseMasterDncDb": True,
"UseMasterEbrDb": True,
"UseMasterDncPolicy": True,
"UseMasterDncSub": True,
"UseMasterTraining": True
}
headers = {
"loginId": "<loginid>",
"Content-Type": "<content-type>"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {loginId: '<loginid>', 'Content-Type': '<content-type>'},
body: JSON.stringify({
ProjectName: '<string>',
FtcOrgId: '<string>',
FtcSan: '<string>',
StateRegistrations: {},
WebhookUrl: '<string>',
WebhookSecret: '<string>',
Notes: '<string>',
UseMasterDncDb: true,
UseMasterEbrDb: true,
UseMasterDncPolicy: true,
UseMasterDncSub: true,
UseMasterTraining: true
})
};
fetch('https://dataapi.dncscrub.com/v1.5/projects/{projectCode}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'ProjectName' => '<string>',
'FtcOrgId' => '<string>',
'FtcSan' => '<string>',
'StateRegistrations' => [
],
'WebhookUrl' => '<string>',
'WebhookSecret' => '<string>',
'Notes' => '<string>',
'UseMasterDncDb' => true,
'UseMasterEbrDb' => true,
'UseMasterDncPolicy' => true,
'UseMasterDncSub' => true,
'UseMasterTraining' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: <content-type>",
"loginId: <loginid>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}"
payload := strings.NewReader("{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("loginId", "<loginid>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://dataapi.dncscrub.com/v1.5/projects/{projectCode}")
.header("loginId", "<loginid>")
.header("Content-Type", "<content-type>")
.body("{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://dataapi.dncscrub.com/v1.5/projects/{projectCode}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["loginId"] = '<loginid>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}"
response = http.request(request)
puts response.read_bodyOther APIs
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
PUT
/
v1.5
/
projects
/
{projectCode}
Create or Update Project (v1.5)
curl --request PUT \
--url https://dataapi.dncscrub.com/v1.5/projects/{projectCode} \
--header 'Content-Type: <content-type>' \
--header 'loginId: <loginid>' \
--data '
{
"ProjectName": "<string>",
"FtcOrgId": "<string>",
"FtcSan": "<string>",
"StateRegistrations": {},
"WebhookUrl": "<string>",
"WebhookSecret": "<string>",
"Notes": "<string>",
"UseMasterDncDb": true,
"UseMasterEbrDb": true,
"UseMasterDncPolicy": true,
"UseMasterDncSub": true,
"UseMasterTraining": true
}
'import requests
url = "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}"
payload = {
"ProjectName": "<string>",
"FtcOrgId": "<string>",
"FtcSan": "<string>",
"StateRegistrations": {},
"WebhookUrl": "<string>",
"WebhookSecret": "<string>",
"Notes": "<string>",
"UseMasterDncDb": True,
"UseMasterEbrDb": True,
"UseMasterDncPolicy": True,
"UseMasterDncSub": True,
"UseMasterTraining": True
}
headers = {
"loginId": "<loginid>",
"Content-Type": "<content-type>"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {loginId: '<loginid>', 'Content-Type': '<content-type>'},
body: JSON.stringify({
ProjectName: '<string>',
FtcOrgId: '<string>',
FtcSan: '<string>',
StateRegistrations: {},
WebhookUrl: '<string>',
WebhookSecret: '<string>',
Notes: '<string>',
UseMasterDncDb: true,
UseMasterEbrDb: true,
UseMasterDncPolicy: true,
UseMasterDncSub: true,
UseMasterTraining: true
})
};
fetch('https://dataapi.dncscrub.com/v1.5/projects/{projectCode}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'ProjectName' => '<string>',
'FtcOrgId' => '<string>',
'FtcSan' => '<string>',
'StateRegistrations' => [
],
'WebhookUrl' => '<string>',
'WebhookSecret' => '<string>',
'Notes' => '<string>',
'UseMasterDncDb' => true,
'UseMasterEbrDb' => true,
'UseMasterDncPolicy' => true,
'UseMasterDncSub' => true,
'UseMasterTraining' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: <content-type>",
"loginId: <loginid>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://dataapi.dncscrub.com/v1.5/projects/{projectCode}"
payload := strings.NewReader("{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("loginId", "<loginid>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://dataapi.dncscrub.com/v1.5/projects/{projectCode}")
.header("loginId", "<loginid>")
.header("Content-Type", "<content-type>")
.body("{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://dataapi.dncscrub.com/v1.5/projects/{projectCode}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["loginId"] = '<loginid>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"ProjectName\": \"<string>\",\n \"FtcOrgId\": \"<string>\",\n \"FtcSan\": \"<string>\",\n \"StateRegistrations\": {},\n \"WebhookUrl\": \"<string>\",\n \"WebhookSecret\": \"<string>\",\n \"Notes\": \"<string>\",\n \"UseMasterDncDb\": true,\n \"UseMasterEbrDb\": true,\n \"UseMasterDncPolicy\": true,\n \"UseMasterDncSub\": true,\n \"UseMasterTraining\": true\n}"
response = http.request(request)
puts response.read_bodyUse 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.
Headers:
Respond with any
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
(no more than once a minute). Automatic validation and webhook delivery follow in a later
release; requests and responses do not change.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
NotesorWebhookUrl. ClearingWebhookUrlalso discards the stored secret.WebhookSecretcannot be cleared on its own (400); sending a new value rotates it. ProjectNameis required only when the Project is created.FtcOrgIdandFtcSanare 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.StateRegistrationsis 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
- 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.
- The FTC Org Id and SAN you supplied are queued for validation against the FTC’s
telemarketer portal.
FtcValidation.Statusstarts aspending. - 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.Statusbecomesvalidated(orfailedwith a reason), and if you supplied aWebhookUrlwe POST the result to it. You can also poll Get Project. - Any
StateRegistrationsyou supplied are routed to our Support team for review. See Louisiana and Oklahoma Registration.
Request
Headers
string
required
Your API Key. An OAuth
Authorization: Bearer token is accepted instead.string
required
application/jsonPath
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.Body
string
Display name, usually your client’s company name. Up to 50 characters. Required when
creating; optional on update.
string
Your client’s FTC Organization Id, for example
10000598-10598. Required if FtcSan is
supplied.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.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, not through the API. See
Louisiana and Oklahoma Registration.string
HTTPS URL (up to 500 characters) we POST to when FTC validation completes. See
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.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.string
Free-form notes shown in the DNCScrub portal. Up to 512 characters.
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.boolean
default:"false"
Share the Master Project’s EBR / exemption database.
boolean
default:"false"
Inherit the Master Project’s DNC policy.
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.boolean
default:"false"
Inherit the Master Project’s training settings.
Example Request
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"
}'
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"
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
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 |
201 Created
{
"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. WhenWebhookUrl is set, we send POST requests with a JSON body:
{
"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
}
}
| 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> |
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 as Webhook.LastDelivery.
Verify the signature and reject stale timestamps (older than 5 minutes) before trusting
the body:
Verify signature (Node)
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
Create the Project before collecting the SAN
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.Use the webhook, poll as a fallback
Use the webhook, poll as a fallback
Validation completes asynchronously, usually within a couple of minutes. Subscribe
with
WebhookUrl and poll Get Project no more
than once a minute if you need a fallback.Scrub with the Project's campaign
Scrub with the Project's campaign
Pass the returned
DefaultCampaignId as campaignId on
Scrub API calls so each client’s numbers are scrubbed
under that client’s own subscription and Internal DNC list.Legacy endpoint
Legacy endpoint
POST https://www.dncscrub.com/app/main/rpc/project
(docs) remains available for existing
integrations. New integrations should use this endpoint.