Partnership
Partnership administration with an agency key.
Base URL https://api.klockwork.org. Send X-Klockwork-Key or Authorization: Bearer kw_…. An agency key acts as the agency that created it. JSON bodies use these fields. A missing required field returns 400 with { "error": "Validation failed", "details": {} }.
These routes use the agency key of the partner company. Customer calls made with a partner key are under Partner.
Get the partnership
GET /api/public/agency/partner
A company that is not a partner returns 404 { "error": "This company is not a partner" }. A member who is not an admin returns 403 { "error": "Only a company admin can do this" }.
Response 200: { partner: Partner }
| Field | Type | Required |
|---|---|---|
id | string (uuid) | yes |
clerkOrgId | string | yes |
displayName | string | yes |
contactEmail | string or null | yes |
status | "pending" or "active" or "suspended" or "terminated" or "rejected" | yes |
subscriptionExempt | boolean | yes |
monthlyRequestCap | integer, min -9007199254740991 or null | yes |
acceptanceFeeCents | integer, min -9007199254740991 | yes |
acceptanceFeeStatus | "due" or "paid" or "waived" | yes |
activatedAt | string or null | yes |
suspendedAt | string or null | yes |
terminatedAt | string or null | yes |
Request a partnership
POST /api/public/agency/partner/request
The body may be {}. This request does not charge the company.
Request body:
| Field | Type | Required |
|---|---|---|
contactEmail | string (email), max 200 | no |
Response 201: { partner: Partner }
| Field | Type | Required |
|---|---|---|
id | string (uuid) | yes |
clerkOrgId | string | yes |
displayName | string | yes |
contactEmail | string or null | yes |
status | "pending" or "active" or "suspended" or "terminated" or "rejected" | yes |
subscriptionExempt | boolean | yes |
monthlyRequestCap | integer, min -9007199254740991 or null | yes |
acceptanceFeeCents | integer, min -9007199254740991 | yes |
acceptanceFeeStatus | "due" or "paid" or "waived" | yes |
activatedAt | string or null | yes |
suspendedAt | string or null | yes |
terminatedAt | string or null | yes |
List partner customers
GET /api/public/agency/partner/customers
Response 200: { customers: Customer[] }
Customer objects use the same fields as Customers.
Create a partner customer
POST /api/public/agency/partner/customers
Request body:
| Field | Type | Required |
|---|---|---|
companyName | string, min 1, max 160 | yes |
invoiceEmail | string (email), max 200 | yes |
Response 201: { customer, inviteUrl, invitationSent }
End, suspend, or restore a customer
POST /api/public/agency/partner/customers/{id}/end
The same empty body and { "ok": true } response apply to /suspend and /restore. An unknown id returns 404 { "error": "Customer not found" }.
No request body.
Response 200: { ok: true }
List partner keys
GET /api/public/agency/partner/keys
Response 200: { keys: PartnerKey[] }
| Field | Type | Required |
|---|---|---|
id | string (uuid) | yes |
name | string | yes |
keyPrefix | string | yes |
environment | "live" or "test" | yes |
scopes | array of "status:read" or "customers:read" or "customers:write" | yes |
expiresAt | string or null | yes |
lastUsedAt | string or null | yes |
revokedAt | string or null | yes |
createdAt | string | yes |
Create a partner key
POST /api/public/agency/partner/keys
secret is returned once. A twentieth active key returns 409 { "error": "This company already has the maximum of 20 active API keys" }. A duplicate name returns 409 { "error": "An active API key with this name already exists" }.
Request body:
| Field | Type | Required |
|---|---|---|
name | string, min 1, max 64 | yes |
environment | "live" or "test" | yes |
scopes | array of "status:read" or "customers:read" or "customers:write" | yes |
Response 201: { apiKey: PartnerKey, secret: string }
| Field | Type | Required |
|---|---|---|
id | string (uuid) | yes |
name | string | yes |
keyPrefix | string | yes |
environment | "live" or "test" | yes |
scopes | array of "status:read" or "customers:read" or "customers:write" | yes |
expiresAt | string or null | yes |
lastUsedAt | string or null | yes |
revokedAt | string or null | yes |
createdAt | string | yes |
Revoke a partner key
POST /api/public/agency/partner/keys/{id}/revoke
No request body.
Response 200: { ok: true }
Get open terms
GET /api/public/agency/partner/terms
Response 200: { terms: Terms | null }
| Field | Type | Required |
|---|---|---|
id | string (uuid) | yes |
model | "wholesale_fixed" or "revenue_share" or "hybrid" | yes |
wholesaleCents | integer, min -9007199254740991 or null | yes |
revenueShareBps | integer, min -9007199254740991 or null | yes |
volumeTiers | array of object or null | yes |
effectiveFrom | string | yes |
volumeTiers items:
| Field | Type | Required |
|---|---|---|
upTo | integer or null | yes |
wholesaleCents | integer, min 0 | yes |
Volume tier items:
| Field | Type | Required |
|---|---|---|
upTo | integer or null | yes |
wholesaleCents | integer, min 0 | yes |
Get the monthly report
GET /api/public/agency/partner/report
Query periodStart is YYYY-MM-01. Omit it to use the current UTC month. Any other day returns 400 { "error": "periodStart must be the first day of a month" }.
Response 200: { report }
The report object:
| Field | Type |
|---|---|
periodStart | date |
periodEnd | date |
terms | { model, wholesaleCents, revenueShareBps, volumeTiers } or null |
usage.requestCount | number |
usage.keyCount | number |
usage.busiestDate | date or null |
usage.degraded | boolean |
reconciliation.customerCount | number |
reconciliation.billableCount | number |
reconciliation.openCount | number |
reconciliation.adjustedCount | number |
reconciliation.amountCents | number |
reconciliation.wholesaleCents | number |
reconciliation.shareCents | number |
reconciliation.reportedCollectedCents | number |
reconciliation.unreportedCount | number |
periods | array of period rows |
Each period row: id, customerClerkOrgId, billable, amountCents, wholesaleCents, shareCents, reportedCollectedCents, status.
