Projects (REST API)
Create projects from your system, look up leads, list projects, fetch team members, 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 | /oauth/token |
Request an access token with the client id and secret Catapult issued to your company |
| POST | /createProject |
Create project |
| GET | /getPotentialProjectLeads |
Get project leads |
| GET | /getProjectList |
Get project list with IDs |
| GET | /getAllTeamMembers |
Get all active team members in your business |
| GET | /testWebhook |
Send dummy webhook events to your endpoint |
Authorization: Bearer and
Ocp-Apim-Subscription-Key.
The token is checked first. The subscription key selects your company.
See Authentication.
Authentication
Catapult issues an access token for your company. Use the client id and client secret Catapult shared with you. Request the token from the same environment you call.
| Environment | Token URL |
|---|---|
| Migration | https://public-api-integration.azure-api.net/migration-public-api/oauth/token |
| Production | https://public-api-integration.azure-api.net/production-public-api/oauth/token |
Request a token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
client_id=YOUR_CATAPULT_CLIENT_ID
client_secret=YOUR_CATAPULT_CLIENT_SECRET
A successful response includes access_token, token_type (Bearer),
and expires_in. Request a new token when it expires. Send the client secret only to the token URL.
Then call any operation with both headers. Catapult checks the access token, then continues with the subscription key checks that select your company.
| Header | Required when OAuth is enabled | Description |
|---|---|---|
Authorization |
Yes | Bearer <access_token> from POST /oauth/token. |
Ocp-Apim-Subscription-Key |
Yes | Your primary or secondary subscription key. Selects your company. |
If OAuth is not enabled for your company, send the subscription key only. The operations, request bodies, and webhook event payloads stay the same.
Environments
Use the Migration base URL while integrating, and the Production base URL for live traffic. Code samples below show both URLs for every operation.
| Environment | Base URL |
|---|---|
| Migration | https://public-api-integration.azure-api.net/migration-public-api |
| Production | https://public-api-integration.azure-api.net/production-public-api |
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 |
|---|---|---|
Authorization |
Yes |
Bearer <access_token> from POST /oauth/token
when OAuth is enabled for your company. Checked before the subscription key.
|
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. "Draft" |
{
"flag": "success",
"message": "Project created successfully",
"external_project_id": "EXT-PROJECT-10042",
"projectID": 12345,
"status": "Draft"
}
-
400
Bad Request - missing/invalid fields, invalid
projectName, or no matching users forprojectLeadEmail. - 401 Unauthorized - missing bearer token or subscription key.
- 403 Forbidden - invalid or inactive subscription.
Code samples
# Migration
curl --request POST \
--url 'https://public-api-integration.azure-api.net/migration-public-api/createProject' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--data '{
"projectName": "2026 Annual RFP",
"projectLeadEmail": "jane.doe@example.com",
"external_project_id": "EXT-PROJECT-10042"
}'
# Production
curl --request POST \
--url 'https://public-api-integration.azure-api.net/production-public-api/createProject' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--data '{
"projectName": "2026 Annual RFP",
"projectLeadEmail": "jane.doe@example.com",
"external_project_id": "EXT-PROJECT-10042"
}'
// Migration
const url = 'https://public-api-integration.azure-api.net/migration-public-api/createProject';
// Production
const url = 'https://public-api-integration.azure-api.net/production-public-api/createProject';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Ocp-Apim-Subscription-Key': subscriptionKey,
},
body: JSON.stringify({
projectName: '2026 Annual RFP',
projectLeadEmail: 'jane.doe@example.com',
external_project_id: 'EXT-PROJECT-10042',
}),
});
const data = await response.json();
console.log(data);
<?php
// Migration
$url = 'https://public-api-integration.azure-api.net/migration-public-api/createProject';
// Production
$url = 'https://public-api-integration.azure-api.net/production-public-api/createProject';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$payload = [
'projectName' => '2026 Annual 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',
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'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 Annual 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"))
.uri(URI.create("https://public-api-integration.azure-api.net/production-public-api/createProject"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.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
# Migration
url = "https://public-api-integration.azure-api.net/migration-public-api/createProject"
# Production
url = "https://public-api-integration.azure-api.net/production-public-api/createProject"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
}
payload = {
"projectName": "2026 Annual 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 |
|---|---|---|
Authorization |
Yes |
Bearer <access_token> from POST /oauth/token
when OAuth is enabled for your company. Checked before the subscription key.
|
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
# Migration
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
# Production
curl --request GET \
--url 'https://public-api-integration.azure-api.net/production-public-api/getPotentialProjectLeads' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
// Migration
const url = 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads';
// Production
const url = 'https://public-api-integration.azure-api.net/production-public-api/getPotentialProjectLeads';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
<?php
// Migration
$url = 'https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads';
// Production
$url = 'https://public-api-integration.azure-api.net/production-public-api/getPotentialProjectLeads';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'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"))
.uri(URI.create("https://public-api-integration.azure-api.net/production-public-api/getPotentialProjectLeads"))
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.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
# Migration
url = "https://public-api-integration.azure-api.net/migration-public-api/getPotentialProjectLeads"
# Production
url = "https://public-api-integration.azure-api.net/production-public-api/getPotentialProjectLeads"
headers = {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"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 |
|---|---|---|
Authorization |
Yes |
Bearer <access_token> from POST /oauth/token
when OAuth is enabled for your company. Checked before the subscription key.
|
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 Annual 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
# Migration
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
# Production
curl --request GET \
--url 'https://public-api-integration.azure-api.net/production-public-api/getProjectList' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
// Migration
const url = 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList';
// Production
const url = 'https://public-api-integration.azure-api.net/production-public-api/getProjectList';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
<?php
// Migration
$url = 'https://public-api-integration.azure-api.net/migration-public-api/getProjectList';
// Production
$url = 'https://public-api-integration.azure-api.net/production-public-api/getProjectList';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'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"))
.uri(URI.create("https://public-api-integration.azure-api.net/production-public-api/getProjectList"))
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.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
# Migration
url = "https://public-api-integration.azure-api.net/migration-public-api/getProjectList"
# Production
url = "https://public-api-integration.azure-api.net/production-public-api/getProjectList"
headers = {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
"Accept": "application/json",
}
response = requests.get(url, headers=headers)
print(response.json())
Get all team members
Returns every active, non-deleted user in your Catapult business. Results are ordered by
first name, then user id. Email addresses are decrypted. date_created is the registration time in UTC
(YYYY-MM-DDTHH:mm:ssZ). An empty team returns 200 with
teamMembers: [] and totalCount: 0.
Request
Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes |
Bearer <access_token> from POST /oauth/token
when OAuth is enabled for your company. Checked before the subscription key.
|
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 All Team Members Success
| Property | Type | Description |
|---|---|---|
flag |
string | "success" |
message |
string | Human-readable confirmation |
totalCount |
number | Number of team members returned |
teamMembers |
array | List of team members |
TeamMember
| Property | Type | Description |
|---|---|---|
id |
number | Catapult users.id |
full_name |
string | null | Full display name |
first_name |
string | null | First name |
last_name |
string | null | Last name |
email |
string | null | Decrypted email address |
role |
string | Catapult role; Admin users are returned as "Admin" |
title |
string | null | Job title |
phone |
string | null | Phone number |
department_name |
string | null | Department name |
date_created |
string | null | Registration time in UTC (YYYY-MM-DDTHH:mm:ssZ) |
{
"flag": "success",
"message": "Team members fetched successfully",
"totalCount": 1,
"teamMembers": [
{
"id": 101,
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@example.com",
"role": "Admin",
"title": "Underwriter",
"phone": "5551234567",
"department_name": "Underwriting",
"date_created": "2022-02-07T06:00:29Z"
}
]
}
- 401 Unauthorized - missing or invalid bearer token, or missing subscription key.
- 403 Forbidden - invalid or inactive subscription key.
Code samples
# Migration
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/getAllTeamMembers' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
# Production
curl --request GET \
--url 'https://public-api-integration.azure-api.net/production-public-api/getAllTeamMembers' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY' \
--header 'Accept: application/json'
// Migration
const url = 'https://public-api-integration.azure-api.net/migration-public-api/getAllTeamMembers';
// Production
const url = 'https://public-api-integration.azure-api.net/production-public-api/getAllTeamMembers';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Ocp-Apim-Subscription-Key': subscriptionKey,
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
<?php
// Migration
$url = 'https://public-api-integration.azure-api.net/migration-public-api/getAllTeamMembers';
// Production
$url = 'https://public-api-integration.azure-api.net/production-public-api/getAllTeamMembers';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'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/getAllTeamMembers"))
.uri(URI.create("https://public-api-integration.azure-api.net/production-public-api/getAllTeamMembers"))
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.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
# Migration
url = "https://public-api-integration.azure-api.net/migration-public-api/getAllTeamMembers"
# Production
url = "https://public-api-integration.azure-api.net/production-public-api/getAllTeamMembers"
headers = {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY",
"Accept": "application/json",
}
response = requests.get(url, headers=headers)
print(response.json())
Test webhook
Sends 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".
Supplier dummies include supplier_id, domain, and
businessName (or null when the company name is unknown).
The Scoring Status dummy includes a complete scorers list with
email, fullName, and status.
Finalist and winner dummies include supplier_id "supplier-0",
domain "example.com", and businessName.
Ignore these in production processing.
Request
Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes |
Bearer <access_token> from POST /oauth/token
when OAuth is enabled for your company. Checked before the subscription key.
|
Ocp-Apim-Subscription-Key |
Yes |
Primary or secondary APIM subscription key. Resolves your
business_id via public_api_business_credentials.
|
No path parameters. Omit eventType to send all event types.
Query parameter (optional)
| Field | Type | Required | Description |
|---|---|---|---|
eventType |
string | No | One webhook event type. Omit to send all. |
Allowed eventType values:
project.dates.updatedproject.status.updatedproject.suppliers.updatedproject.scoring.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
# Migration - all event types
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY'
# Production - all event types
curl --request GET \
--url 'https://public-api-integration.azure-api.net/production-public-api/testWebhook' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY'
# Migration - Scoring Status only
curl --request GET \
--url 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook?eventType=project.scoring.status_updated' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY'
# Production - Scoring Status only
curl --request GET \
--url 'https://public-api-integration.azure-api.net/production-public-api/testWebhook?eventType=project.scoring.status_updated' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Ocp-Apim-Subscription-Key: YOUR_SUBSCRIPTION_KEY'
// Migration
const url = 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook';
// Production
const url = 'https://public-api-integration.azure-api.net/production-public-api/testWebhook';
const subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
const headers = {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Ocp-Apim-Subscription-Key': subscriptionKey,
};
// All event types
let response = await fetch(url, { headers });
// Scoring Status only
response = await fetch(
`${url}?eventType=project.scoring.status_updated`,
{ headers },
);
const data = await response.json();
console.log(data);
<?php
// Migration
$url = 'https://public-api-integration.azure-api.net/migration-public-api/testWebhook';
// Production
$url = 'https://public-api-integration.azure-api.net/production-public-api/testWebhook';
$subscriptionKey = 'YOUR_SUBSCRIPTION_KEY';
$headers = ['Authorization: Bearer YOUR_ACCESS_TOKEN', 'Ocp-Apim-Subscription-Key: ' . $subscriptionKey];
function getWebhook($url, $headers) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
// All event types
echo getWebhook($url, $headers);
// Scoring Status only
echo getWebhook($url . '?eventType=project.scoring.status_updated', $headers);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// Migration
String baseUrl = "https://public-api-integration.azure-api.net/migration-public-api/testWebhook";
// Production
String baseUrl = "https://public-api-integration.azure-api.net/production-public-api/testWebhook";
HttpClient client = HttpClient.newHttpClient();
// All event types
HttpRequest allEvents = HttpRequest.newBuilder()
.uri(URI.create(baseUrl))
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.GET()
.build();
// Scoring Status only
HttpRequest oneEventQuery = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "?eventType=project.scoring.status_updated"))
.header("Authorization", "Bearer YOUR_ACCESS_TOKEN")
.header("Ocp-Apim-Subscription-Key", "YOUR_SUBSCRIPTION_KEY")
.GET()
.build();
HttpResponse<String> response = client.send(allEvents, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import requests
# Migration
url = "https://public-api-integration.azure-api.net/migration-public-api/testWebhook"
# Production
url = "https://public-api-integration.azure-api.net/production-public-api/testWebhook"
headers = {"Authorization": "Bearer YOUR_ACCESS_TOKEN", "Ocp-Apim-Subscription-Key": "YOUR_SUBSCRIPTION_KEY"}
# All event types
response = requests.get(url, headers=headers)
# Scoring Status only
response = requests.get(
url,
params={"eventType": "project.scoring.status_updated"},
headers=headers,
)
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 GET /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.
When your company has given Catapult an OAuth URL, client id, and client secret,
each webhook POST includes Authorization: Bearer. Catapult requests that
token from your OAuth URL. The event body is not HMAC-signed.
x-webhook-secret is sent only when that shared secret is also configured.
| Item | Value |
|---|---|
| Header | Authorization: Bearer <access_token> |
| Scheme | Access token Catapult requested from the OAuth URL, client id, and client secret you provided. |
| Optional header | x-webhook-secret when a shared secret is also configured. |
| Body | Raw JSON envelope. Not HMAC-signed. |
Use GET /testWebhook to send dummy envelopes
to your configured URL without updating a project.
1. Project date updates
eventType: project.dates.updated
Sent when a project is created in Catapult with any of these dates set, and whenever project acceptance, question submission, or due dates change afterwards. The project.status.updated event carries no dates, so this is the event to read dates from. A date that is not set is null.
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 Annual 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 Annual 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 Annual RFP",
"projectID": 12345,
"acceptBy": "06/15/2026",
"questionSubmissionDate": null,
"dueDate": "06/30/2026"
}
}
2. Project status updates
eventType: project.status.updated
Sent only when the human-readable project lifecycle status actually changes. Repeat activity that leaves the status untouched - such as inviting a second or third supplier once the project is already Invited - does not send an event. This event can also carry a webhook-only Scoring Complete value when every scorer has marked scoring complete. That value is not stored as a project lifecycle status in Catapult.
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, or webhook-only Scoring Complete (see values below). |
Project Status Values
| Value | Typical trigger |
|---|---|
Draft | New project created, review round reset to draft. |
Invited | First participant invited. Later invitees do not change the project status, so no event is sent for them - use project.suppliers.updated to track each invitee. |
Pending Approval | Project sent for review round. |
Approved | Requestor review approved. |
Completed | Project closed or winner selected. |
Scoring Complete | Webhook-only. Sent when every scorer has marked themselves Complete (shared collaborative status, or every assigned individual scorer). This does not change the project's lifecycle status in Catapult; per-scorer detail still arrives on project.scoring.status_updated. |
{
"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 Annual RFP",
"projectID": 12345,
"projectStatus": "Invited"
}
}
{
"schemaVersion": 1,
"eventId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"eventType": "project.status.updated",
"occurredAt": "2026-05-19T15:45:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Annual RFP",
"projectID": 12345,
"projectStatus": "Scoring Complete"
}
}
3. Invitee information
Use project.suppliers.updated as a live snapshot of the Invite Responders table. It fires whenever a supplier is added, an invite is rescinded, or any invitee status changes (SendGrid email events or Catapult outcomes). Replace your local table with invitees on every event. Use supplier_id to uniquely identify an invitee when multiple people share the same email domain.
eventType: project.suppliers.updated
This event replaces the previous project.participants.invited, project.participants.invite_rescinded, and project.participants.status_updated events.
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 | Count of all active invitees on the project. Always the full table, never a changed subset. |
inviteeEmailDomains |
string[] | Unique domains from every active invitee, lowercased, sorted (e.g. ["acme.com","broker.com"]). |
invitees |
object[] | Full Invite Responders table. Every active invitee is included on every event, with supplier_id, domain, businessName, 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[].businessName |
string | null | Responder company name when known (business name, else user company). null when it cannot be resolved. |
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
Fired for add, rescind, SendGrid email events (delivered, dropped, deferred, bounce, open, click, spamreport, unsubscribe), and Catapult responder status changes. Replace the table by projectID. status is always the single latest value (Catapult wins over SendGrid). A rescinded supplier is omitted from invitees.
{
"schemaVersion": 1,
"eventId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"eventType": "project.suppliers.updated",
"occurredAt": "2026-05-19T15:00:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Annual RFP",
"projectID": 12345,
"totalInviteeCount": 3,
"inviteeEmailDomains": ["acme.com", "contoso.com", "example.org"],
"invitees": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance", "status": "Email Opened" },
{ "supplier_id": "supplier-102", "domain": "contoso.com", "businessName": "Contoso Ltd", "status": "Accepted" },
{ "supplier_id": "supplier-103", "domain": "example.org", "businessName": null, "status": "Status Pending" }
]
}
}
4. Scoring Status
eventType: project.scoring.status_updated
A full snapshot of the people currently responsible for scoring. It is sent when the first
responder submits, a scorer is added or removed, a score is first entered, a status is
manually changed, or the project switches between collaborative and individual scoring.
Replace your local scorer-status list with scorers on every event.
If no scorers are assigned, the list contains the project leads. The first assigned scorer
replaces that fallback list. Collaborative scoring gives every scorer the same status;
individual scoring can give each scorer a different status. Removing a score does not move
an In Progress status back to Not Started. When every scorer has
marked Complete, Catapult also sends project.status.updated with webhook-only
projectStatus Scoring Complete.
Payload fields
| Field | Type | Description |
|---|---|---|
external_project_id | string | null | Partner Extended ID, or null when none is registered. |
projectName | string | null | Catapult project display name. |
projectID | number | Stable Catapult project identifier. |
scoringType | string | Collaborative or Individual. |
scorers | object[] | Complete current scorer list, or project-lead fallback list. |
scorers[].email | string | null | Scorer email address. |
scorers[].fullName | string | null | Scorer full name (first and last name). |
scorers[].status | string | Not Started, In Progress, or Completed. |
Example
{
"schemaVersion": 1,
"eventId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"eventType": "project.scoring.status_updated",
"occurredAt": "2026-05-19T15:30:00.000Z",
"source": "catapulthq",
"correlationId": null,
"payload": {
"external_project_id": "dertb242Sv",
"projectName": "2026 Annual RFP",
"projectID": 12345,
"scoringType": "Individual",
"scorers": [
{ "email": "alex.scorer@example.com", "fullName": "Alex Morgan", "status": "Completed" },
{ "email": "jordan.scorer@example.com", "fullName": "Jordan Blake", "status": "In Progress" },
{ "email": "taylor.scorer@example.com", "fullName": "Taylor Reed", "status": "Not Started" }
]
}
}
5. 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 Annual 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 Annual 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" }
]
}
}
6. 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 Annual 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 Annual RFP",
"projectID": 12345,
"winnerCompanyNames": ["Acme Insurance"],
"winners": [
{ "supplier_id": "supplier-101", "domain": "acme.com", "businessName": "Acme Insurance" }
]
}
}