API reference
Webhook subscription endpoints
Manage where events go. The events themselves are in the webhook reference; the receiving side is in receive webhooks.
Create a webhook subscription
POST/v1/
The response carries secret (whsec_...) this once. Store it; CorrConnect never shows it again. events is optional and defaults to activity.created and grant.revoked.
- Auth
- Console session cookie,
OriginandX-CSRF-Token - Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string | |
Origin required | header | string | Exactly the CorrConnect portal origin. |
X-CSRF-Token required | header | string | The latest csrf_token from sign-in or GET /v1/account/me. |
Request body application/json
| Field | Type | Notes |
|---|---|---|
callback_url required | string | Public HTTPS URL on port 443 (10 to 2,000 characters). 10 to 2000 characters |
events | array of string or null | Optional. activity.created, grant.revoked or both (the default). |
Response 201
Successful Response
| Field | Type | Notes |
|---|---|---|
id required | string | |
callback_url required | string | |
events required | array of string | |
status required | string | One of active, disabled |
consecutive_ required | integer | |
created_at required | string (date-time) | |
updated_at required | string (date-time) | |
secret required | string |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_ | The URL is not public HTTPS on port 443, or has credentials, a fragment or a wildcard. |
| 400 | invalid_ | events lists something other than activity.created or grant.revoked. |
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 403 | origin_rejected | The Origin header is not the exact CorrConnect portal origin. |
| 403 | csrf_rejected | X-CSRF-Token is missing or is not the latest token from GET /v1/account/me. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
| 409 | webhook_ | The app already has 3 subscriptions. |
| 422 | validation_ | A field is missing, has the wrong type or is out of range; details.fields says which. |
| 429 | rate_ | More than 20 subscriptions created by this account in an hour. |
curl "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks" \
-H "Cookie: $CORRCONNECT_COOKIE" \
-H "Origin: $CORRCONNECT_BASE" \
-H "X-CSRF-Token: $CSRF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"callback_url": "https://developer.example/hooks/corrconnect", "events": ["activity.created", "grant.revoked"]}'const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks`, {
method: "POST",
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
"Origin": `${process.env.CORRCONNECT_BASE}`,
"X-CSRF-Token": `${process.env.CSRF_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"callback_url": "https://developer.example/hooks/corrconnect", "events": ["activity.created", "grant.revoked"]}),
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}", "Origin": f"{os.environ['CORRCONNECT_BASE']}", "X-CSRF-Token": f"{os.environ['CSRF_TOKEN']}"},
json={"callback_url": "https://developer.example/hooks/corrconnect", "events": ["activity.created", "grant.revoked"]},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks")!)
request.httpMethod = "POST"
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
request.setValue("\(env["CORRCONNECT_BASE"]!)", forHTTPHeaderField: "Origin")
request.setValue("\(env["CSRF_TOKEN"]!)", forHTTPHeaderField: "X-CSRF-Token")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(#"{"callback_url": "https://developer.example/hooks/corrconnect", "events": ["activity.created", "grant.revoked"]}"#.utf8)
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"id": "whs_0123456789abcdef0123456789abcdef",
"callback_url": "https://developer.example/hooks/corrconnect",
"events": [
"activity.created",
"grant.revoked"
],
"status": "active",
"consecutive_failures": 0,
"created_at": "2026-10-09T12:00:00+00:00",
"updated_at": "2026-10-09T12:00:00+00:00",
"secret": "whsec_<shown-once>"
}List webhook subscriptions
GET/v1/
Oldest first. Secrets are never returned.
- Auth
- Console session cookie
- Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string |
Response 200
Successful Response
| Field | Type | Notes |
|---|---|---|
data required | array of object | |
data[].id required | string | |
data[]. required | string | |
data[].events required | array of string | |
data[].status required | string | One of active, disabled |
data[]. required | integer | |
data[]. required | string (date-time) | |
data[]. required | string (date-time) |
Errors
| Status | Code | When |
|---|---|---|
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
curl "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks" \
-H "Cookie: $CORRCONNECT_COOKIE"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks`, {
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.get(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks")!)
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"data": [
{
"id": "whs_0123456789abcdef0123456789abcdef",
"callback_url": "https://developer.example/hooks/corrconnect",
"events": [
"activity.created",
"grant.revoked"
],
"status": "active",
"consecutive_failures": 0,
"created_at": "2026-10-09T12:00:00+00:00",
"updated_at": "2026-10-09T12:00:00+00:00"
}
]
}Send a test event
POST/v1/
Queues one webhook.test event to this subscription, signed like any other event.
- Auth
- Console session cookie,
OriginandX-CSRF-Token - Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string | |
subscription_id required | path | string | |
Origin required | header | string | Exactly the CorrConnect portal origin. |
X-CSRF-Token required | header | string | The latest csrf_token from sign-in or GET /v1/account/me. |
Response 202
Successful Response
| Field | Type | Notes |
|---|---|---|
status required | string | One of queued |
delivery_id required | string |
Errors
| Status | Code | When |
|---|---|---|
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 403 | origin_rejected | The Origin header is not the exact CorrConnect portal origin. |
| 403 | csrf_rejected | X-CSRF-Token is missing or is not the latest token from GET /v1/account/me. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
| 404 | webhook_ | The app has no subscription with this ID. |
| 409 | webhook_disabled | The subscription is paused; enable it first. |
| 429 | rate_ | More than 30 test events from this account in an hour. |
curl -X POST "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks/$SUBSCRIPTION_ID/test" \
-H "Cookie: $CORRCONNECT_COOKIE" \
-H "Origin: $CORRCONNECT_BASE" \
-H "X-CSRF-Token: $CSRF_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks/${process.env.SUBSCRIPTION_ID}/test`, {
method: "POST",
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
"Origin": `${process.env.CORRCONNECT_BASE}`,
"X-CSRF-Token": `${process.env.CSRF_TOKEN}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks/{os.environ['SUBSCRIPTION_ID']}/test",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}", "Origin": f"{os.environ['CORRCONNECT_BASE']}", "X-CSRF-Token": f"{os.environ['CSRF_TOKEN']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks/\(env["SUBSCRIPTION_ID"]!)/test")!)
request.httpMethod = "POST"
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
request.setValue("\(env["CORRCONNECT_BASE"]!)", forHTTPHeaderField: "Origin")
request.setValue("\(env["CSRF_TOKEN"]!)", forHTTPHeaderField: "X-CSRF-Token")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"status": "queued",
"delivery_id": "whd_0123456789abcdef0123456789abcdef"
}List recent deliveries
GET/v1/
Newest first; limit defaults to 50 (1 to 100). Status and coded errors only, never payloads. Finished deliveries are kept for 30 days.
- Auth
- Console session cookie
- Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string | |
subscription_id required | path | string | |
limit | query | integer | How many deliveries to return, newest first. 1 to 100 Default 50 |
Response 200
Successful Response
| Field | Type | Notes |
|---|---|---|
data required | array of object | |
data[].id required | string | |
data[].event_id required | string | |
data[]. required | string | One of activity.created, grant.revoked, webhook.test |
data[].status required | string | One of pending, delivered, failed |
data[].attempts required | integer | |
data[]. | integer or null | |
data[]. | string or null | |
data[]. required | string (date-time) | |
data[]. required | string (date-time) |
Errors
| Status | Code | When |
|---|---|---|
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
| 404 | webhook_ | The app has no subscription with this ID. |
| 422 | validation_ | A field is missing, has the wrong type or is out of range; details.fields says which. |
curl "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks/$SUBSCRIPTION_ID/deliveries?limit=50" \
-H "Cookie: $CORRCONNECT_COOKIE"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks/${process.env.SUBSCRIPTION_ID}/deliveries?limit=50`, {
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.get(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks/{os.environ['SUBSCRIPTION_ID']}/deliveries?limit=50",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks/\(env["SUBSCRIPTION_ID"]!)/deliveries?limit=50")!)
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"data": [
{
"id": "whd_0123456789abcdef0123456789abcdef",
"event_id": "evt_0123456789abcdef0123456789abcdef",
"event_type": "activity.created",
"status": "delivered",
"attempts": 1,
"last_status_code": 200,
"last_error": null,
"created_at": "2026-10-09T12:00:00+00:00",
"updated_at": "2026-10-09T12:00:01+00:00"
}
]
}Turn a paused subscription back on
POST/v1/
A subscription pauses after 20 failed attempts in a row. This sets it active and resets the count.
- Auth
- Console session cookie,
OriginandX-CSRF-Token - Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string | |
subscription_id required | path | string | |
Origin required | header | string | Exactly the CorrConnect portal origin. |
X-CSRF-Token required | header | string | The latest csrf_token from sign-in or GET /v1/account/me. |
Response 200
Successful Response
| Field | Type | Notes |
|---|---|---|
id required | string | |
callback_url required | string | |
events required | array of string | |
status required | string | One of active, disabled |
consecutive_ required | integer | |
created_at required | string (date-time) | |
updated_at required | string (date-time) |
Errors
| Status | Code | When |
|---|---|---|
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 403 | origin_rejected | The Origin header is not the exact CorrConnect portal origin. |
| 403 | csrf_rejected | X-CSRF-Token is missing or is not the latest token from GET /v1/account/me. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
| 404 | webhook_ | The app has no subscription with this ID. |
| 429 | rate_ | More than 20 enables by this account in an hour. |
curl -X POST "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks/$SUBSCRIPTION_ID/enable" \
-H "Cookie: $CORRCONNECT_COOKIE" \
-H "Origin: $CORRCONNECT_BASE" \
-H "X-CSRF-Token: $CSRF_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks/${process.env.SUBSCRIPTION_ID}/enable`, {
method: "POST",
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
"Origin": `${process.env.CORRCONNECT_BASE}`,
"X-CSRF-Token": `${process.env.CSRF_TOKEN}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks/{os.environ['SUBSCRIPTION_ID']}/enable",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}", "Origin": f"{os.environ['CORRCONNECT_BASE']}", "X-CSRF-Token": f"{os.environ['CSRF_TOKEN']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks/\(env["SUBSCRIPTION_ID"]!)/enable")!)
request.httpMethod = "POST"
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
request.setValue("\(env["CORRCONNECT_BASE"]!)", forHTTPHeaderField: "Origin")
request.setValue("\(env["CSRF_TOKEN"]!)", forHTTPHeaderField: "X-CSRF-Token")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"id": "whs_0123456789abcdef0123456789abcdef",
"callback_url": "https://developer.example/hooks/corrconnect",
"events": [
"activity.created",
"grant.revoked"
],
"status": "active",
"consecutive_failures": 0,
"created_at": "2026-10-09T12:00:00+00:00",
"updated_at": "2026-10-09T12:00:00+00:00"
}Delete a webhook subscription
DELETE/v1/
Removes the subscription and its delivery log.
- Auth
- Console session cookie,
OriginandX-CSRF-Token - Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
app_id required | path | string | |
subscription_id required | path | string | |
Origin required | header | string | Exactly the CorrConnect portal origin. |
X-CSRF-Token required | header | string | The latest csrf_token from sign-in or GET /v1/account/me. |
Response 200
Successful Response
| Field | Type | Notes |
|---|---|---|
status required | string | One of deleted |
Errors
| Status | Code | When |
|---|---|---|
| 401 | authentication_ | No console session cookie was sent. |
| 401 | invalid_session | The session was signed out, replaced or never existed. |
| 401 | session_expired | The session is older than its lifetime (12 hours by default). |
| 403 | email_ | The account has not verified its email address yet. |
| 403 | origin_rejected | The Origin header is not the exact CorrConnect portal origin. |
| 403 | csrf_rejected | X-CSRF-Token is missing or is not the latest token from GET /v1/account/me. |
| 404 | app_not_found | The app does not exist or belongs to another account. |
| 404 | webhook_ | The app has no subscription with this ID. |
curl -X DELETE "$CORRCONNECT_BASE/v1/developer/apps/$APP_ID/webhooks/$SUBSCRIPTION_ID" \
-H "Cookie: $CORRCONNECT_COOKIE" \
-H "Origin: $CORRCONNECT_BASE" \
-H "X-CSRF-Token: $CSRF_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/developer/apps/${process.env.APP_ID}/webhooks/${process.env.SUBSCRIPTION_ID}`, {
method: "DELETE",
headers: {
"Cookie": `${process.env.CORRCONNECT_COOKIE}`,
"Origin": `${process.env.CORRCONNECT_BASE}`,
"X-CSRF-Token": `${process.env.CSRF_TOKEN}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.delete(
f"{os.environ['CORRCONNECT_BASE']}/v1/developer/apps/{os.environ['APP_ID']}/webhooks/{os.environ['SUBSCRIPTION_ID']}",
headers={"Cookie": f"{os.environ['CORRCONNECT_COOKIE']}", "Origin": f"{os.environ['CORRCONNECT_BASE']}", "X-CSRF-Token": f"{os.environ['CSRF_TOKEN']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/v1/developer/apps/\(env["APP_ID"]!)/webhooks/\(env["SUBSCRIPTION_ID"]!)")!)
request.httpMethod = "DELETE"
request.setValue("\(env["CORRCONNECT_COOKIE"]!)", forHTTPHeaderField: "Cookie")
request.setValue("\(env["CORRCONNECT_BASE"]!)", forHTTPHeaderField: "Origin")
request.setValue("\(env["CSRF_TOKEN"]!)", forHTTPHeaderField: "X-CSRF-Token")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"status": "deleted"
}