Projects (REST API)
Create projects from your system, look up leads, list projects, and send a test webhook.
You may instead keep project creation in Catapult; webhooks still fire when a webhook URL
is configured on your Public API credentials. Correlate on projectID.
external_project_id is your partner id
from POST /createProject, or null when the project was created in Catapult.
Webhook payloads always include projectName.
GET project_id accepts the partner Extended ID or Catapult projectID.
Webhook payloads are in the Webhooks section below.
Operations
| Method | Path | Summary |
|---|---|---|
| POST | /createProject |
Create project |
| GET | /getPotentialProjectLeads |
Get project leads |
| GET | /getProjectList |
Get project list with IDs |
| POST | /testWebhook |
Send dummy webhook events to your endpoint |
Ocp-Apim-Subscription-Key.
Create project
Creates a new RFP project in Catapult and registers your external
external_project_id
email values in projectLeadEmail.
Request
Headers
| Header | Required | Description |
|---|---|---|
Ocp-Apim-Subscription-Key |
Yes |
Primary or secondary APIM subscription key. Resolves your
business_id via public_api_business_credentials.
|
Content-Type |
Yes | Must be application/json. |
Body - application/json
| Field | Type | Required | Description |
|---|---|---|---|
projectName |
string | Yes | Display name (max 100 chars). Allowed: letters, digits, space, - & , ( ) . ' $ +. |
projectLeadEmail |
string or string[] | Yes | Comma-separated string or JSON array. Must match active users in your business. |
external_project_id |
string | Yes | Your external project id (stored in registry). |
business_id is not accepted from clients in production, it is
resolved from your subscription key.
Responses
200 OK - Create Project Success
| Property | Type | Description |
|---|---|---|
flag |
string | "success" |
message |
string | Human-readable confirmation |
external_project_id |
string | Your external id (echo) |
projectID |
number | Catapult id (stable Catapult identifier) |
status |
string | e.g. "Not Started" |
{
"flag": "success",
"message": "Project created successfully",
"external_project_id": "EXT-PROJECT-10042",
"projectID": 12345,
"status": "Not Started"
}
-
400
Bad Request - missing/invalid fields, invalid
projectName, or no matching users forprojectLeadEmail. - 401 Unauthorized - missing subscription key.
- 403 Forbidden - invalid or inactive subscription.
Code samples
curl --request POST \
--url 'https://public-api-integration.azure-api.net/migration-public-api/createProject' \
--header 'Content-Type: application/json' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--data '{
"projectName": "2026 Workers Comp RFP",
"projectLeadEmail": "jane.doe@example.com",
"external_project_id": "EXT-PROJECT-10042"
}'
const url = 'https://public-api-integration.azure-api.net/migration-public-api/createProject';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': subscriptionKey,
},
body: JSON.stringify({
projectName: '2026 Workers Comp RFP',
projectLeadEmail: 'jane.doe@example.com',
external_project_id: 'EXT-PROJECT-10042',
}),
});
const data = await response.json();
console.log(data);
<?php
$url = 'https://public-api-integration.azure-api.net/migration-public-api/createProject';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$payload = [
'projectName' => '2026 Workers Comp RFP',
'projectLeadEmail' => 'jane.doe@example.com',
'external_project_id' => 'EXT-PROJECT-10042',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String json = """
{
"projectName": "2026 Workers Comp RFP",
"projectLeadEmail": "jane.doe@example.com",
"external_project_id": "EXT-PROJECT-10042"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://public-api-integration.azure-api.net/migration-public-api/createProject"))
.header("Content-Type", "application/json")
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import requests
url = "https://public-api-integration.azure-api.net/migration-public-api/createProject"
headers = {
"Content-Type": "application/json",
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
}
payload = {
"projectName": "2026 Workers Comp RFP",
"projectLeadEmail": "jane.doe@example.com",
"external_project_id": "EXT-PROJECT-10042",
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
Get project leads
Returns active users in your Catapult business who may be assigned as project leads.
Excludes roles: Corporate Contact, Auditor, Committee Member.
Request
Headers
| Header | Required | Description |
|---|---|---|
Ocp-Apim-Subscription-Key |
Yes |
Primary or secondary APIM subscription key. Resolves your
business_id via public_api_business_credentials.
|
Accept |
No | Recommended: application/json. |
No query or path parameters. No request body.
Responses
200 OK - Get ProjectLead Success
| Property | Type | Description |
|---|---|---|
flag |
string | "success" |
message |
string | Human-readable confirmation |
projectLead |
array | List of eligible users |
ProjectLeadUser
| Property | Type | Description |
|---|---|---|
full_name |
string | User display name |
email |
string | Decrypted email address |
role |
string | Catapult role |
department_name |
string | Department name, if set |
{
"flag": "success",
"message": "Project lead fetched successfully",
"projectLead": [
{
"full_name": "Jane Doe",
"email": "jane.doe@example.com",
"role": "Admin",
"department_name": "Underwriting"
}
]
}
- 401 Unauthorized - same as Create project.
- 403 Forbidden - same as Create project.
Code samples
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
const url = 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
<?php
$url = 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads"))
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import requests
url = "https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads"
headers = {
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
"Accept": "application/json",
}
response = requests.get(url, headers=headers)
print(response.json())
Get project list with IDs
Returns active projects in your Catapult business: Catapult projectID, partner
external_project_id (null if the project was created in Catapult and
no partner id is stored), name, dates, and active invitee count.
Request
Headers
| Header | Required | Description |
|---|---|---|
Ocp-Apim-Subscription-Key |
Yes |
Primary or secondary APIM subscription key. Resolves your
business_id via public_api_business_credentials.
|
Accept |
No | Recommended: application/json. |
No query or path parameters. No request body.
Responses
200 OK - Get Project List Success
| Property | Type | Description |
|---|---|---|
flag |
string | "success" |
message |
string | Human-readable confirmation |
projects |
array | List of active projects |
ProjectListItem
| Property | Type | Description |
|---|---|---|
projectID |
number | Catapult projects.id |
external_project_id |
string | null | Partner Extended ID from POST /createProject; null when the project was created in Catapult |
projectName |
string | null | Project display name |
acceptBy |
string | null | Accept-by date (MM/DD/YYYY) |
questionSubmissionDate |
string | null | Question submission date (MM/DD/YYYY) |
dueDate |
string | null | Due date (MM/DD/YYYY) |
totalInviteeCount |
number | Active invitees |
{
"flag": "success",
"message": "Project list fetched successfully",
"projects": [
{
"projectID": 12345,
"external_project_id": "EXT-PROJECT-10042",
"projectName": "2026 Workers Comp RFP",
"acceptBy": "03/15/2026",
"questionSubmissionDate": "04/01/2026",
"dueDate": "04/30/2026",
"totalInviteeCount": 12
},
{
"projectID": 12346,
"external_project_id": null,
"projectName": "Internal Q2 Review",
"acceptBy": "05/01/2026",
"questionSubmissionDate": null,
"dueDate": "05/30/2026",
"totalInviteeCount": 3
}
]
}
- 401 Unauthorized - same as Create project.
- 403 Forbidden - same as Create project.
Code samples
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
const url = 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
<?php
$url = 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://public-api-integration.azure-api.net/migration-public-api/getProjectList"))
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import requests
url = "https://public-api-integration.azure-api.net/migration-public-api/getProjectList"
headers = {
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
"Accept": "application/json",
}
response = requests.get(url, headers=headers)
print(response.json())
Test webhook
POSTs dummy webhook envelopes to the webhook URL stored on your Public API credentials. Nothing in Catapult is updated. Use this to confirm your endpoint is reachable and that verification headers are present.
projectID 0,
external_project_id "TEST-WEBHOOK",
projectName "Test Project", and
correlationId "test-webhook".
Finalist and winner dummies include supplier_id "supplier-0",
domain "example.com", and businessName.
Ignore these in production processing.
Request
Headers
| Header | Required | Description |
|---|---|---|
Ocp-Apim-Subscription-Key |
Yes |
Primary or secondary APIM subscription key. Resolves your
business_id via public_api_business_credentials.
|
Content-Type |
No | Use application/json if you send a body. |
No path parameters. Body is optional. Omit eventType to send all seven event types.
Request body - application/json (optional)
| Field | Type | Required | Description |
|---|---|---|---|
eventType |
string | No | One webhook event type. Also accepted as a query parameter. Omit to send all. |
Allowed eventType values:
project.dates.updatedproject.status.updatedproject.participants.invitedproject.participants.invite_rescindedproject.participants.status_updatedproject.finalists.selection_changedproject.winners.selection_changed
Responses
200 OK
Returns a deliveries array with eventType, eventId, ok, and the HTTP statusCode from your webhook endpoint.
{
"flag": "success",
"message": "Test webhook events sent. These are dummy events and are not tied to a real project update.",
"webhookUrl": "https://example.com/webhooks/catapult",
"deliveries": [
{
"eventType": "project.status.updated",
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ok": true,
"statusCode": 200
}
]
}
-
400
Bad Request - webhook URL missing/invalid, or unknown
eventType. - 401 Unauthorized - same as Create project.
- 403 Forbidden - same as Create project.
Code samples
# All seven event types
curl --request POST \
--url 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook' \
--header 'Content-Type: application/json' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--data '{}'
# One event type (JSON body)
curl --request POST \
--url 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook' \
--header 'Content-Type: application/json' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--data '{
"eventType": "project.status.updated"
}'
# One event type (query parameter)
curl --request POST \
--url 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook?eventType=project.dates.updated' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY'
const url = 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const headers = {
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Content-Type': 'application/json',
};
// All seven event types
let response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({}),
});
// One event type (JSON body)
response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ eventType: 'project.status.updated' }),
});
// One event type (query parameter)
response = await fetch(`${url}?eventType=project.dates.updated`, {
method: 'POST',
headers: { 'Ocp-Apim-Subscription-Key': subscriptionKey },
});
const data = await response.json();
console.log(data);
<?php
$url = 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$headers = [
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
'Content-Type: application/json',
];
function postJson($url, $headers, $body) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
// All seven event types
echo postJson($url, $headers, '{}');
// One event type (JSON body)
echo postJson($url, $headers, json_encode(['eventType' => 'project.status.updated']));
// One event type (query parameter)
echo postJson($url . '?eventType=project.dates.updated', [
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
], '');
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String baseUrl = "https://public-api-integration.azure-api.net/migration-public-api/testWebhook";
HttpClient client = HttpClient.newHttpClient();
// All seven event types
HttpRequest allEvents = HttpRequest.newBuilder()
.uri(URI.create(baseUrl))
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{}"))
.build();
// One event type (JSON body)
HttpRequest oneEventBody = HttpRequest.newBuilder()
.uri(URI.create(baseUrl))
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"eventType\":\"project.status.updated\"}"))
.build();
// One event type (query parameter)
HttpRequest oneEventQuery = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "?eventType=project.dates.updated"))
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = client.send(allEvents, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import requests
url = "https://public-api-integration.azure-api.net/migration-public-api/testWebhook"
headers = {
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
"Content-Type": "application/json",
}
# All seven event types
response = requests.post(url, json={}, headers=headers)
# One event type (JSON body)
response = requests.post(
url,
json={"eventType": "project.status.updated"},
headers=headers,
)
# One event type (query parameter)
response = requests.post(
url,
params={"eventType": "project.dates.updated"},
headers={"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY"},
)
print(response.json())
Webhook payload reference
Catapult sends JSON webhooks to your configured endpoint when project dates, status, participants,
finalists, or winners change. Each message uses a shared envelope; event-specific fields live under
payload.
POST /createProject) still emit events. Those payloads set
external_project_id to null and include
projectID (Catapult projects.id) plus
payload.projectName. Correlate on projectID when there is no partner id.
GET operations that take project_id accept that partner id or
Catapult projectID. Use POST /testWebhook
to send dummy events without changing a project.
REST API operations are in the Projects (REST API) section above.
Shared envelope
Every webhook includes these top-level fields (in addition to payload):
| Field | Type | Description |
|---|---|---|
schemaVersion |
number | Payload schema version (currently 1). |
eventId |
string (UUID) | Unique id for this delivery. |
eventType |
string | Event discriminator (see sections below). |
occurredAt |
string (ISO 8601) | UTC timestamp when the change occurred. |
source |
string | Always "catapulthq" for production events. |
correlationId |
string | null | Optional trace id; often null on live events. Test deliveries use "test-webhook". |
payload |
object | Event-specific body documented per section. |
acceptBy use
MM/DD/YYYY when set, or null when cleared or not applicable.
Verification
Catapult POSTs JSON to your webhook URL (Content-Type: application/json).
Return a 2xx status; non-2xx responses are retried.
Incoming requests are authenticated with a shared secret, not HMAC-SHA256 of the body
and not Authorization: Bearer.
| Item | Value |
|---|---|
| Header | x-webhook-secret |
| Scheme | Shared secret token (static value). Compare it to the secret Catapult provides. |
| Body | Raw JSON envelope. Not HMAC-signed. |
Use POST /testWebhook to send dummy envelopes
to your configured URL without updating a project.
1. Project date updates
eventType: project.dates.updated
Sent when project acceptance, question submission, or due dates change in Catapult.
Payload fields
| Field | Type | Description |
|---|---|---|
external_project_id |
string | null | Partner Extended ID from Public API create, or null when the project was created in Catapult. Correlate on projectID when this field is null. |
projectName |
string | null | Catapult project display name. Always included (or null if the project has no name). |
projectID |
number | Catapult id - stable Catapult project identifier. |
acceptBy |
string | null | Deadline for project acceptance (MM/DD/YYYY). |
questionSubmissionDate |
string | null | Cutoff for submitting questions (MM/DD/YYYY). |
dueDate |
string | null | Final project due date (MM/DD/YYYY). |
{
"schemaVersion": 1,
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventType": "project.dates.updated",
"occurredAt": "2026-05-19T16:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"acceptBy": "06/15/2026",
"questionSubmissionDate": "06/01/2026",
"dueDate": "06/30/2026"
}
}
Example - Catapult-created project (no partner id)
{
"schemaVersion": 1,
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventType": "project.dates.updated",
"occurredAt": "2026-05-19T16:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": null,
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"acceptBy": "06/15/2026",
"questionSubmissionDate": "06/01/2026",
"dueDate": "06/30/2026"
}
}
Example - date cleared
{
"schemaVersion": 1,
"eventId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"eventType": "project.dates.updated",
"occurredAt": "2026-05-19T16:05:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"acceptBy": "06/15/2026",
"questionSubmissionDate": null,
"dueDate": "06/30/2026"
}
}
2. Project status updates
eventType: project.status.updated
Sent when the human-readable project lifecycle status changes.
Payload fields
| Field | Type | Required | Description |
|---|---|---|---|
external_project_id |
string | null | Yes | Partner Extended ID from Public API create, or null when the project was created in Catapult. Correlate on projectID when this field is null. |
projectName |
string | null | No | Catapult project display name. Always included (or null if the project has no name). |
projectID |
number | Yes | Catapult id - stable Catapult project identifier. |
projectStatus |
string | Yes | Human-readable lifecycle status (see values below). |
Project Status Values
| Value | Typical trigger |
|---|---|
Not Started | New project created, review round reset to not started. |
Invited | Participant invited. |
Pending Approval | Project sent for review round. |
Approved | Requestor review approved. |
Completed | Project closed or winner selected. |
{
"schemaVersion": 1,
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventType": "project.status.updated",
"occurredAt": "2026-05-19T14:30:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"projectStatus": "Invited"
}
}
3. Invitee information
Participant invite, rescind, and status-update events share the same payload shape. Status labels match the Invite Responders UI (SendGrid email events and Catapult responder outcomes). Use supplier_id to uniquely identify an invitee when multiple people share the same email domain.
Event types
| eventType | When |
|---|---|
project.participants.invited |
New participant added. |
project.participants.invite_rescinded |
Invite rescinded. |
project.participants.status_updated |
Invitee status changed (SendGrid email event or Catapult status such as Accepted / Declined / Completed / Winner). |
Payload fields
| Field | Type | Description |
|---|---|---|
external_project_id |
string | null | Partner Extended ID from Public API create, or null when the project was created in Catapult. Correlate on projectID when this field is null. |
projectName |
string | null | Catapult project display name. Always included (or null if the project has no name). |
projectID |
number | Catapult id - stable Catapult project identifier. |
totalInviteeCount |
number | For invite/rescind: count of all active invitees. For status_updated: count of invitees included in this event (often the changed subset). |
inviteeEmailDomains |
string[] | Unique domains from the invitees in this payload, lowercased, sorted (e.g. ["acme.com","broker.com"]). |
invitees |
object[] | Per-invitee rows with supplier_id, domain, and display status. |
invitees[].supplier_id |
string | Stable id for the invitee: supplier-{{questionnaire_participants.id}}. Use this when two invitees share a domain. |
invitees[].domain |
string | null | Email domain for the invitee (lowercased). |
invitees[].status |
string | Single latest status. Catapult outcome wins when set (Undecided, Accepted, Declined, Completed, Winner); otherwise the latest SendGrid status (Status Pending, Email Delivered, Email Dropped, Email Deferred, Email Bounced, Email Opened, Email Clicked, Spam Report, Unsubscribe). |
Example: invited
{
"schemaVersion": 1,
"eventId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"eventType": "project.participants.invited",
"occurredAt": "2026-05-19T15:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"totalInviteeCount": 3,
"inviteeEmailDomains": ["acme.com", "contoso.com", "example.org"],
"invitees": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "status": "Status Pending" },
{ "supplier_id": "supplier-102", "domain": "contoso.com", "status": "Status Pending" },
{ "supplier_id": "supplier-103", "domain": "example.org", "status": "Status Pending" }
]
}
}
Example: invite rescinded
Same payload shape, only eventType changes.
{
"schemaVersion": 1,
"eventId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"eventType": "project.participants.invite_rescinded",
"occurredAt": "2026-05-19T15:30:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"totalInviteeCount": 2,
"inviteeEmailDomains": ["acme.com", "contoso.com"],
"invitees": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "status": "Email Opened" },
{ "supplier_id": "supplier-102", "domain": "contoso.com", "status": "Accepted" }
]
}
}
Example: status updated
Emitted for every SendGrid email event (delivered, dropped, deferred, bounce, open, click, spamreport, unsubscribe) and for Catapult responder status changes. Upsert by supplier_id. status is always the single latest value (Catapult wins over SendGrid).
{
"schemaVersion": 1,
"eventId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"eventType": "project.participants.status_updated",
"occurredAt": "2026-05-19T16:05:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"totalInviteeCount": 1,
"inviteeEmailDomains": ["acme.com"],
"invitees": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "status": "Email Bounced" }
]
}
}
{
"schemaVersion": 1,
"eventId": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"eventType": "project.participants.status_updated",
"occurredAt": "2026-05-19T17:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"totalInviteeCount": 1,
"inviteeEmailDomains": ["contoso.com"],
"invitees": [
{ "supplier_id": "supplier-102", "domain": "contoso.com", "status": "Accepted" }
]
}
}
4. Finalist updates
eventType: project.finalists.selection_changed
Sent when finalists are selected or cleared. Company names stay in
finalistCompanyNames. Each selected participant is also listed in
finalists with supplier_id, domain, and
businessName (correlate with invitees[]). Both arrays are empty when
all finalists are cleared.
Payload fields
| Field | Type | Description |
|---|---|---|
external_project_id |
string | null | Partner Extended ID from Public API create, or null when the project was created in Catapult. Correlate on projectID when this field is null. |
projectName |
string | null | Catapult project display name. Always included (or null if the project has no name). |
projectID |
number | Catapult id - stable Catapult project identifier. |
finalistCompanyNames |
string[] | Distinct company names for active participants with finalist recipient. |
finalists |
object[] | Active finalist participants: supplier_id, domain, and businessName. |
finalists[].supplier_id |
string | Stable id: supplier-{{questionnaire_participants.id}}. |
finalists[].domain |
string | null | Email domain for the finalist (lowercased). |
finalists[].businessName |
string | null | Responder business name (falls back to user company if business name is empty). |
{
"schemaVersion": 1,
"eventId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"eventType": "project.finalists.selection_changed",
"occurredAt": "2026-05-19T16:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"finalistCompanyNames": ["Acme Insurance", "Beta Brokers LLC"],
"finalists": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance" },
{ "supplier_id": "supplier-102", "domain": "betabrokers.com", "businessName": "Beta Brokers LLC" }
]
}
}
Example - Catapult-created project (no partner id)
{
"schemaVersion": 1,
"eventId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"eventType": "project.finalists.selection_changed",
"occurredAt": "2026-05-19T16:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": null,
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"finalistCompanyNames": ["Acme Insurance", "Beta Brokers LLC"],
"finalists": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance" },
{ "supplier_id": "supplier-102", "domain": "betabrokers.com", "businessName": "Beta Brokers LLC" }
]
}
}
5. Winner updates
eventType: project.winners.selection_changed
Sent when winners are selected. Company names stay in
winnerCompanyNames. Each winner is also listed in
winners with supplier_id, domain, and
businessName (correlate with invitees[]).
Payload fields
| Field | Type | Description |
|---|---|---|
external_project_id |
string | null | Partner Extended ID from Public API create, or null when the project was created in Catapult. Correlate on projectID when this field is null. |
projectName |
string | null | Catapult project display name. Always included (or null if the project has no name). |
projectID |
number | Catapult id - stable Catapult project identifier. |
winnerCompanyNames |
string[] | Distinct company names for active participants with (winner). Same name resolution as finalists. |
winners |
object[] | Active winner participants: supplier_id, domain, and businessName. |
winners[].supplier_id |
string | Stable id: supplier-{{questionnaire_participants.id}}. |
winners[].domain |
string | null | Email domain for the winner (lowercased). |
winners[].businessName |
string | null | Responder business name (falls back to user company if business name is empty). |
{
"schemaVersion": 1,
"eventId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"eventType": "project.winners.selection_changed",
"occurredAt": "2026-05-19T17:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"winnerCompanyNames": ["Acme Insurance"],
"winners": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance" }
]
}
}
Example - Catapult-created project (no partner id)
{
"schemaVersion": 1,
"eventId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"eventType": "project.winners.selection_changed",
"occurredAt": "2026-05-19T17:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": null,
"projectName": "2026 Workers Comp RFP",
"projectID": 12345,
"winnerCompanyNames": ["Acme Insurance"],
"winners": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance" }
]
}
}