How to issue a tenant-scoped API key, what scopes it needs, and how to point a development project at ProjexCloud — locally or in the cloud — without ever putting a person's password in a config file.
ProjexCloud accepts two credential classes on its tenant-callable SDK routes. Pick by who is acting.
actor.kind = service, so machine traffic is distinguishable from a human'sPOST /api/auth/login/api/mfa/challenge), and dies at the next password rotation. Use an API key.
Keys are issued by sdk-api-keys, mounted on the gateway. You need two things first: a user JWT
(to authorise the issuance) and the tenant_id the key will belong to.
curl -sX POST "$GATEWAY/api/applications" \
-H "Authorization: Bearer $JWT" \
-H 'Content-Type: application/json' \
-d '{"name":"Web backend","environment":"live"}'
# → { "data": { "application": { "application_id": "…", "slug": "web-backend", "environment": "live" } } }
One application per thing that calls the platform — your backend, a nightly job, your
staging copy. A shared credential means a leak forces every integration to rotate at once and no call can be
attributed to the app that made it. environment belongs to the application: a test
application mints pk_test_ keys and a live one mints pk_live_, so the two can
never be confused by inspection. The slug is your client_id.
curl -sX POST "$GATEWAY/api/applications/$APPLICATION_ID/keys" \
-H "Authorization: Bearer $JWT" \
-H 'Content-Type: application/json' \
-d '{
"name": "nightly sync",
"scopes": ["sla.clock.write", "sla.clock.read", "assignment.assign-by-task.write"],
"rate_limit_rpm": 600,
"expires_at": "2027-01-01T00:00:00Z"
}'
Answers 201 with both the key record and the plaintext:
{ "data": {
"key": { "key_id": "…", "tenant_id": "…", "prefix": "pk_live_A1B2...WXYZ",
"scopes": ["…"], "status": "active" },
"plaintext": "pk_live_A1B2C3D4E5F6G7H8J9K…"
} }
plaintext is returned exactly once and is never recoverable. Only a keyed HMAC-SHA256 lookup and a display prefix are stored. Copy it into your secret store immediately.
Lose it and your only option is to rotate. Never log it, never commit it, never paste it into a ticket.
rate_limit_rpm and expires_at are optional but recommended — an unbounded,
never-expiring key is a standing liability. Give every integration its own key so one revocation cannot take
down two apps.
The format accepts a pk_test_ prefix alongside pk_live_. Issue separate keys for
development and production so a leaked dev key can never touch live data.
Scopes follow <domain>.<resource>.<action> — for example
crm.contact.read, rebac.relationship.write. The scope a request requires is
derived from the route itself — on every tenant route, not a subset — so it is predictable
without a lookup table, and a route added tomorrow is covered the moment it exists:
| Rule | Value |
|---|---|
| domain | The SDK's namespace, taken from the path — sla, crm, coverage, and so on for every mounted SDK |
| resource | The path segment after the domain, singularised (clocks → clock, policies → policy). Path parameters are skipped. |
| action | read for GET/HEAD, write for everything else |
A missing scope answers 403 and tells you exactly which one is absent, along with what the
key does hold — so the fastest way to discover the scope list you need is to call the endpoint and read the error.
Generated from tests/api_definitions by scripts/verify/gen-scope-table.mjs —
524 endpoints across 90 domains. Do not edit by hand.
A domain wildcard such as sla.* covers every row in that domain, including ones added later.
| Request | Scope required |
|---|---|
GET /api/agent-runtime/agents | agent-runtime.agent.read |
GET /api/agent-runtime/agents/:id | agent-runtime.agent.read |
GET /api/agent-runtime/health | agent-runtime.health.read |
GET /api/agent-runtime/runs | agent-runtime.run.read |
GET /api/agent-runtime/runs/:id | agent-runtime.run.read |
POST /api/agent-runtime/agents | agent-runtime.agent.write |
POST /api/agent-runtime/runs | agent-runtime.run.write |
POST /api/agent-runtime/runs/:run_id/replay | agent-runtime.run.write |
POST /api/agent-runtime/runs/:run_id/rollback | agent-runtime.run.write |
POST /api/agent-runtime/tokens | agent-runtime.token.write |
POST /api/agent-runtime/tokens/:token_id/revoke | agent-runtime.token.write |
POST /api/agent-runtime/tokens/:token_id/validate | agent-runtime.token.write |
DELETE /api/ai-gateway/tenant-credentials/:binding_id | ai-gateway.tenant-credential.write |
GET /api/ai-gateway/health | ai-gateway.health.read |
GET /api/ai-gateway/tenant-credentials | ai-gateway.tenant-credential.read |
PATCH /api/ai-gateway/tenant-credentials/:binding_id | ai-gateway.tenant-credential.write |
POST /api/ai-gateway/complete | ai-gateway.complete.write |
POST /api/ai-gateway/stream | ai-gateway.stream.write |
POST /api/ai-gateway/tenant-credentials | ai-gateway.tenant-credential.write |
GET /api/analytics/datasets | analytic.dataset.read |
GET /api/analytics/datasets/:spec_id/builds | analytic.dataset.read |
POST /api/analytics/builds/:build_id/export | analytic.build.write |
POST /api/analytics/datasets | analytic.dataset.write |
POST /api/analytics/datasets/:spec_id/build | analytic.dataset.write |
PUT /api/analytics/datasets/:spec_id/label-source | analytic.dataset.write |
GET /api/api-keys | api-key.api-key.read |
POST /api/api-keys | api-key.api-key.write |
POST /api/api-keys/{key_id}/revoke | api-key.revoke.write |
POST /api/api-keys/{key_id}/rotate | api-key.rotate.write |
GET /api/app-identities/:app_identity_id | app-identity.app-identity.read |
GET /api/app-identities/:app_identity_id/memberships | app-identity.membership.read |
POST /api/app-identities | app-identity.app-identity.write |
GET /api/applications | application.application.read |
GET /api/applications/{application_id} | application.application.read |
POST /api/applications | application.application.write |
POST /api/applications/{application_id}/disable | application.disable.write |
POST /api/applications/{application_id}/keys | application.key.write |
GET /api/approvals/requests | approval.request.read |
GET /api/approvals/requests/:request_id | approval.request.read |
GET /api/approvals/routes | approval.route.read |
POST /api/approvals/requests | approval.request.write |
POST /api/approvals/requests/:request_id/decide | approval.request.write |
POST /api/approvals/routes | approval.route.write |
POST /api/approvals/steps/:step_id/decide | approval.step.write |
GET /api/assets/:asset_id/commands | asset.command.read |
GET /api/assets/:asset_id/readings | asset.reading.read |
GET /api/assets/:asset_id/twin | asset.twin.read |
POST /api/assets | asset.asset.write |
POST /api/assets/:asset_id/credentials | asset.credential.write |
POST /api/assignment/assign-by-task | assignment.assign-by-task.write |
PUT /api/assignment/workload/:persona_id | assignment.workload.write |
POST /api/audit/append | audit.append.write |
POST /api/audit/export | audit.export.write |
POST /api/audit/verify | audit.verify.write |
POST /api/auth/login | auth.login.write |
POST /api/auth/register | auth.register.write |
POST /api/auth/signup-tenant | auth.signup-tenant.write |
POST /api/auth/token | auth.token.write |
GET /api/billing/live | billing.live.read |
GET /api/billing/showback | billing.showback.read |
POST /api/billing/invoices/generate | billing.invoice.write |
POST /api/billing/reprice-dry-run | billing.reprice-dry-run.write |
GET /api/break-glass/:grant_id | break-glass.break-glass.read |
POST /api/break-glass | break-glass.break-glass.write |
POST /api/break-glass/:grant_id/decide | break-glass.decide.write |
POST /api/break-glass/:grant_id/use | break-glass.use.write |
POST /api/build/plan | build.plan.write |
POST /api/campaigns | campaign.campaign.write |
POST /api/campaigns/:campaign_id/journeys | campaign.journey.write |
POST /api/campaigns/:campaign_id/segments | campaign.segment.write |
POST /api/campaigns/journeys/:journey_id/runs | campaign.journey.write |
POST /api/campaigns/runs/:run_id/advance | campaign.run.write |
POST /api/campaigns/segments/:segment_id/compute | campaign.segment.write |
GET /api/commands/:command_id | command.command.read |
GET /api/commands/stream/:asset_id | command.stream.read |
POST /api/commands | command.command.write |
POST /api/commands/:command_id/decision | command.decision.write |
GET /api/config | config.config.read |
GET /api/config/resolve | config.resolve.read |
GET /api/config/value | config.value.read |
POST /api/config | config.config.write |
POST /api/config/revoke | config.revoke.write |
POST /api/config/rotate | config.rotate.write |
GET /api/connectors | connector.connector.read |
GET /api/connectors/installs/:install_id | connector.install.read |
GET /api/connectors/installs/:install_id/health | connector.install.read |
GET /api/connectors/installs/:install_id/tools | connector.install.read |
GET /api/connectors/kinds | connector.kind.read |
GET /api/connectors/tenants/:tenant_id/dlq | connector.tenant.read |
GET /api/connectors/tenants/:tenant_id/installs | connector.tenant.read |
POST /api/connectors/dlq/replay | connector.dlq.write |
POST /api/connectors/dlq/retry-tick | connector.dlq.write |
POST /api/connectors/inbound/:kind | connector.inbound.write |
POST /api/connectors/installs | connector.install.write |
POST /api/connectors/installs/:install_id/sync | connector.install.write |
POST /api/connectors/installs/:install_id/tools/call | connector.install.write |
POST /api/connectors/installs/:install_id/uninstall | connector.install.write |
POST /api/connectors/slack/events | connector.slack.write |
POST /api/connectors/slack/install | connector.slack.write |
POST /api/connectors/slack/post-message | connector.slack.write |
POST /api/connectors/tenants/:tenant_id/dlq/reconcile | connector.tenant.write |
GET /api/consent/purposes | consent.purpos.read |
GET /api/consent/receipts | consent.receipt.read |
GET /api/consents/export | consent.export.read |
POST /api/consent/receipts/:receipt_id/revoke | consent.receipt.write |
POST /api/consents | consent.consent.write |
POST /api/consents/:receipt_id/revoke | consent.revoke.write |
POST /api/consents/check | consent.check.write |
POST /api/consents/purposes | consent.purpos.write |
GET /api/content/items/:item_id | content.item.read |
GET /api/content/items/:item_id/versions | content.item.read |
POST /api/content/items | content.item.write |
POST /api/content/items/:item_id/archive | content.item.write |
POST /api/content/items/:item_id/versions | content.item.write |
POST /api/content/items/:item_id/versions/:version_id/publish | content.item.write |
PUT /api/content/taxonomies | content.taxonomy.write |
GET /api/crm/activities/calls | crm.activity.read |
GET /api/crm/contacts/:contact_id | crm.contact.read |
GET /api/crm/deals | crm.deal.read |
GET /api/crm/deals/:deal_id | crm.deal.read |
GET /api/crm/deals/:deal_id/next-action | crm.deal.read |
GET /api/crm/deals/:deal_id/save-gate | crm.deal.read |
GET /api/crm/deals/:deal_id/stage-guard | crm.deal.read |
GET /api/crm/funnel-stages | crm.funnel-stage.read |
GET /api/crm/pipeline/board | crm.pipeline.read |
GET /api/crm/pipeline/stale | crm.pipeline.read |
PATCH /api/crm/contacts/:contact_id | crm.contact.write |
PATCH /api/crm/deals/:deal_id | crm.deal.write |
POST /api/crm/activities | crm.activity.write |
POST /api/crm/activities/call | crm.activity.write |
POST /api/crm/activities/voicemail | crm.activity.write |
POST /api/crm/contacts | crm.contact.write |
POST /api/crm/deals | crm.deal.write |
POST /api/crm/deals/:deal_id/next-action | crm.deal.write |
POST /api/crm/deals/:deal_id/next-action/complete | crm.deal.write |
POST /api/crm/deals/:deal_id/transition | crm.deal.write |
POST /api/crm/funnel-stages | crm.funnel-stage.write |
GET /api/data-rights/requests/:request_id | data-right.request.read |
GET /api/data-rights/residency/:person_id | data-right.residency.read |
POST /api/data-rights/executions/:execution_id/result | data-right.execution.write |
POST /api/data-rights/reconciliation/run | data-right.reconciliation.write |
POST /api/data-rights/requests | data-right.request.write |
POST /api/data-rights/requests/:request_id/certificate | data-right.request.write |
POST /api/data-rights/requests/:request_id/plan-executions | data-right.request.write |
POST /api/data-rights/requests/:request_id/transition | data-right.request.write |
POST /api/data-rights/residency/touch | data-right.residency.write |
GET /api/deliverability/bounce-events | deliverability.bounce-event.read |
GET /api/deliverability/mailboxes | deliverability.mailbox.read |
GET /api/deliverability/reply-events | deliverability.reply-event.read |
GET /api/deliverability/reputation | deliverability.reputation.read |
GET /api/deliverability/suppressions | deliverability.suppression.read |
POST /api/deliverability/check | deliverability.check.write |
POST /api/deliverability/mailboxes | deliverability.mailbox.write |
POST /api/deliverability/mailboxes/:mailbox_id/replies | deliverability.mailbox.write |
POST /api/deliverability/mailboxes/:mailbox_id/sync | deliverability.mailbox.write |
POST /api/deliverability/optout-tokens | deliverability.optout-token.write |
POST /api/deliverability/optout/redeem | deliverability.optout.write |
POST /api/deliverability/reputation/record | deliverability.reputation.write |
POST /api/deliverability/reputation/resume | deliverability.reputation.write |
POST /api/deliverability/suppressions | deliverability.suppression.write |
POST /api/deliverability/suppressions/remove | deliverability.suppression.write |
POST /api/deliverability/webhook-secrets | deliverability.webhook-secret.write |
POST /api/deliverability/webhooks/:provider | deliverability.webhook.write |
GET /api/devices/:device_uuid | device.device.read |
GET /api/devices/:device_uuid/persons | device.person.read |
POST /api/devices | device.device.write |
POST /api/devices/:device_uuid/attest | device.attest.write |
POST /api/devices/:device_uuid/link-person | device.link-person.write |
POST /api/devices/:device_uuid/revoke | device.revoke.write |
GET /api/diagnostic/crash | diagnostic.crash.read |
GET /api/diagnostic/crash/:id | diagnostic.crash.read |
GET /api/diagnostic/health | diagnostic.health.read |
POST /api/diagnostic/crash | diagnostic.crash.write |
POST /api/diagnostic/health | diagnostic.health.write |
POST /api/diagnostic/session-replay | diagnostic.session-replay.write |
GET /api/dispatch/ws/:persona_id | dispatch.w.read |
POST /api/dispatch/routes/optimize | dispatch.route.write |
GET /api/empi/candidate-links | empi.candidate-link.read |
GET /api/empi/metrics | empi.metric.read |
POST /api/empi/candidate-links/:link_id/adjudicate | empi.candidate-link.write |
POST /api/empi/candidate-links/:link_id/steward-review | empi.candidate-link.write |
POST /api/empi/merges | empi.merge.write |
POST /api/empi/merges/:merge_id/unmerge | empi.merge.write |
GET /api/encounters/:encounter_id | encounter.encounter.read |
GET /api/encounters/:encounter_id/grants | encounter.grant.read |
GET /api/encounters/:encounter_id/participants | encounter.participant.read |
POST /api/encounters | encounter.encounter.write |
POST /api/encounters/:encounter_id/grants | encounter.grant.write |
POST /api/encounters/:encounter_id/grants/check | encounter.grant.write |
POST /api/encounters/:encounter_id/participants | encounter.participant.write |
POST /api/encounters/:encounter_id/transition | encounter.transition.write |
GET /api/events/sessions/:session_id | event.session.read |
GET /api/events/types | event.type.read |
GET /api/events/types/:type | event.type.read |
POST /api/events/checkin | event.checkin.write |
POST /api/events/sessions | event.session.write |
POST /api/events/tickets | event.ticket.write |
GET /api/evidence/capture | evidence.capture.read |
GET /api/evidence/capture/:id | evidence.capture.read |
POST /api/evidence/capture | evidence.capture.write |
GET /api/flags | flag.flag.read |
GET /api/flags/:flag_id | flag.flag.read |
POST /api/flags/:flag_id/evaluate | flag.evaluate.write |
POST /api/flags/:flag_id/kill-switch | flag.kill-switch.write |
POST /api/flags/:flag_id/rollouts | flag.rollout.write |
PUT /api/flags | flag.flag.write |
GET /api/geo/addresses/:address_id | geo.address.read |
POST /api/geo/bbox-query | geo.bbox-query.write |
POST /api/geo/canonicalize | geo.canonicalize.write |
POST /api/geo/geocode | geo.geocode.write |
POST /api/geo/merge | geo.merge.write |
POST /api/geo/reverse-geocode | geo.reverse-geocode.write |
POST /api/geo-nodes | geo-node.geo-node.write |
POST /api/grants/:grant_id/revoke | grant.revoke.write |
GET /api/handoffs | handoff.handoff.read |
GET /api/handoffs/:handoff_id | handoff.handoff.read |
GET /api/handoffs/:handoff_id/saga | handoff.saga.read |
PATCH /api/handoffs/:handoff_id | handoff.handoff.write |
POST /api/handoffs | handoff.handoff.write |
POST /api/handoffs/:handoff_id/approval/decision | handoff.approval.write |
POST /api/handoffs/:handoff_id/approval/request | handoff.approval.write |
POST /api/handoffs/:handoff_id/saga/start | handoff.saga.write |
POST /api/handoffs/:handoff_id/transition | handoff.transition.write |
GET /api/hdk/camera/capabilities | hdk.camera.read |
GET /api/hdk/camera/recording-presets | hdk.camera.read |
GET /api/hdk/image-editor/capabilities | hdk.image-editor.read |
GET /api/hdk/map/capabilities | hdk.map.read |
GET /api/hdk/map/tile-providers | hdk.map.read |
GET /api/hdk/measure | hdk.measure.read |
GET /api/hdk/measure/:id | hdk.measure.read |
GET /api/hdk/scanner/capabilities | hdk.scanner.read |
GET /api/hdk/video-editor/capabilities | hdk.video-editor.read |
GET /api/hdk/watermark | hdk.watermark.read |
GET /api/hdk/watermark/:id | hdk.watermark.read |
POST /api/hdk/measure | hdk.measure.write |
POST /api/hdk/watermark | hdk.watermark.write |
POST /api/hdk-diagnostic/drain | hdk-diagnostic.drain.write |
POST /api/hdk-diagnostic/events | hdk-diagnostic.event.write |
GET /api/hdk-idp/devices/:device_uuid/claims | hdk-idp.device.read |
POST /api/hdk-idp/claims | hdk-idp.claim.write |
POST /api/hdk-idp/offline-auth/log | hdk-idp.offline-auth.write |
GET /api/hdk-permissions/devices/:device_uuid/latest | hdk-permission.device.read |
POST /api/hdk-permissions/snapshots | hdk-permission.snapshot.write |
GET /api/hdk-sync/event-type-policies | hdk-sync.event-type-policy.read |
GET /api/hdk-sync/event-type-policies/:event_type | hdk-sync.event-type-policy.read |
GET /api/hdk-sync/human-review/open | hdk-sync.human-review.read |
POST /api/hdk-sync/conflicts/resolve | hdk-sync.conflict.write |
POST /api/hdk-sync/human-review/:task_id/resolve | hdk-sync.human-review.write |
POST /api/hdk-sync/replay/:batch_id/complete | hdk-sync.replay.write |
POST /api/hdk-sync/replay/start | hdk-sync.replay.write |
PUT /api/hdk-sync/event-type-policies | hdk-sync.event-type-policy.write |
POST /api/identity/aliases | identity.alias.write |
POST /api/identity/social/:provider/callback | identity.social.write |
POST /api/impersonation/:grant_id/approve | impersonation.approve.write |
POST /api/impersonation/:grant_id/end | impersonation.end.write |
POST /api/impersonation/request | impersonation.request.write |
GET /api/imports/mapping-templates | import.mapping-template.read |
GET /api/imports/runs | import.run.read |
GET /api/imports/runs/:run_id | import.run.read |
GET /api/imports/runs/:run_id/exceptions | import.run.read |
POST /api/imports/mapping-templates | import.mapping-template.write |
POST /api/imports/mapping-templates/:template_id/version | import.mapping-template.write |
POST /api/imports/runs | import.run.write |
POST /api/imports/runs/:run_id/commit | import.run.write |
POST /api/imports/runs/:run_id/dry-run | import.run.write |
POST /api/imports/runs/:run_id/mapping-suggestions | import.run.write |
POST /api/imports/runs/:run_id/preview | import.run.write |
POST /api/imports/runs/:run_id/rollback | import.run.write |
POST /api/imports/runs/:run_id/transform-plan | import.run.write |
PUT /api/imports/runs/:run_id/mapping | import.run.write |
GET /api/incidents | incident.incident.read |
GET /api/incidents/:incident_id | incident.incident.read |
GET /api/incidents/:incident_id/evidence | incident.evidence.read |
GET /api/incidents/sla-breaches | incident.sla-breache.read |
PATCH /api/incidents/:incident_id | incident.incident.write |
POST /api/incidents | incident.incident.write |
POST /api/incidents/:incident_id/evidence | incident.evidence.write |
POST /api/incidents/:incident_id/transition | incident.transition.write |
POST /api/ingest/:entity/batch | ingest.batch.write |
POST /api/ingest/customer/batch | ingest.customer.write |
POST /api/ingest/sensor-readings/batch | ingest.sensor-reading.write |
GET /api/keys | key.key.read |
POST /api/keys | key.key.write |
POST /api/keys/:key_id/revoke | key.revoke.write |
GET /api/lead-scoring/models/:id | lead-scoring.model.read |
GET /api/lead-scoring/models/:id/weights | lead-scoring.model.read |
GET /api/lead-scoring/models/active | lead-scoring.model.read |
POST /api/lead-scoring/models | lead-scoring.model.write |
POST /api/lead-scoring/models/:id/activate | lead-scoring.model.write |
POST /api/lead-scoring/models/:id/retire | lead-scoring.model.write |
POST /api/lead-scoring/next-best-action | lead-scoring.next-best-action.write |
POST /api/lead-scoring/score | lead-scoring.score.write |
PUT /api/lead-scoring/models/:id/weights/:feature | lead-scoring.model.write |
GET /api/mcp/health | mcp.health.read |
GET /api/mcp/server-registrations | mcp.server-registration.read |
GET /api/mcp/server-registrations/:id | mcp.server-registration.read |
POST /api/mcp/server-registrations | mcp.server-registration.write |
POST /api/mcp/server-registrations/:id/disable | mcp.server-registration.write |
POST /api/mcp/tools/:tool_id/invoke | mcp.tool.write |
PUT /api/me/profile | me.profile.write |
GET /api/media/:blob_id/playback-url | media.playback-url.read |
GET /api/media/transcode-jobs/:job_id | media.transcode-job.read |
POST /api/media/:blob_id/ready | media.ready.write |
POST /api/media/:blob_id/transcode | media.transcode.write |
POST /api/media/upload-url | media.upload-url.write |
GET /api/memberships/:membership_id/personas | membership.persona.read |
POST /api/memberships | membership.membership.write |
POST /api/memberships/:membership_id/terminate | membership.terminate.write |
GET /api/meter/assets/:asset_id/usage | meter.asset.read |
GET /api/meter/health | meter.health.read |
POST /api/mfa/challenge | mfa.challenge.write |
POST /api/mfa/verify | mfa.verify.write |
DELETE /api/notifications/providers/:provider_id | notification.provider.write |
GET /api/notifications/delivery-receipts | notification.delivery-receipt.read |
GET /api/notifications/providers | notification.provider.read |
GET /api/notifications/sms-consent | notification.sms-consent.read |
GET /api/notifications/sms-inbound | notification.sms-inbound.read |
PATCH /api/notifications/providers/:provider_id | notification.provider.write |
POST /api/notifications/dispatch | notification.dispatch.write |
POST /api/notifications/providers | notification.provider.write |
POST /api/notifications/providers/:provider_id/verify | notification.provider.write |
POST /api/notifications/quiet-hours | notification.quiet-hour.write |
POST /api/notifications/send | notification.send.write |
POST /api/notifications/sms-consent | notification.sms-consent.write |
POST /api/notifications/sms-settings | notification.sms-setting.write |
POST /api/notifications/templates | notification.template.write |
POST /api/notifications/webhooks/delivery/:provider | notification.webhook.write |
POST /api/notifications/webhooks/sms/inbound | notification.webhook.write |
GET /api/offers/:offer_id/current | offer.current.read |
GET /api/offers/:offer_id/version-stamp | offer.version-stamp.read |
GET /api/offers/:offer_id/versions/:version_id/features | offer.version.read |
POST /api/offers | offer.offer.write |
POST /api/offers/:offer_id/check-reference | offer.check-reference.write |
POST /api/offers/:offer_id/versions | offer.version.write |
POST /api/offers/:offer_id/versions/:version_id/activate | offer.version.write |
POST /api/offers/:offer_id/versions/:version_id/features | offer.version.write |
POST /api/offers/:offer_id/versions/:version_id/publish-decision | offer.version.write |
POST /api/offers/:offer_id/versions/:version_id/publish-request | offer.version.write |
POST /api/participants/:participant_id/leave | participant.leave.write |
GET /api/payments/provider | payment.provider.read |
POST /api/payments/:charge_id/distribute | payment.distribute.write |
POST /api/payments/:charge_id/refund | payment.refund.write |
POST /api/payments/charge | payment.charge.write |
POST /api/payments/methods | payment.method.write |
GET /api/persons/:person_id/app-identities | person.app-identity.read |
GET /api/persons/:person_id/devices | person.device.read |
GET /api/personas | persona.persona.read |
GET /api/personas/:persona_id | persona.persona.read |
GET /api/personas/:persona_id/roles | persona.role.read |
POST /api/personas | persona.persona.write |
POST /api/personas/:persona_id/bu | persona.bu.write |
POST /api/personas/:persona_id/deactivate | persona.deactivate.write |
POST /api/personas/:persona_id/role | persona.role.write |
POST /api/personas/:persona_id/shred | persona.shred.write |
GET /api/policies/:policy_id | policy.policy.read |
POST /api/policies | policy.policy.write |
POST /api/policies/evaluate | policy.evaluate.write |
POST /api/principal-token | principal-token.principal-token.write |
GET /api/profile/bands/:app_identity_id/:band_kind | profile.band.read |
GET /api/profile/secure-data/:person_id | profile.secure-data.read |
GET /api/profile/secure-data/:person_id/shred-history | profile.secure-data.read |
POST /api/profile/secure-data/set-field | profile.secure-data.write |
POST /api/profile/secure-data/shred-field | profile.secure-data.write |
PUT /api/profile/bands | profile.band.write |
POST /api/relationships | relationship.relationship.write |
POST /api/relationships/check | relationship.check.write |
PUT /api/relationships/:relationship_id/scope | relationship.scope.write |
POST /api/resellers | reseller.reseller.write |
POST /api/resolver/explain | resolver.explain.write |
POST /api/resolver/resolve | resolver.resolve.write |
GET /api/resources | resource.resource.read |
GET /api/resources/:resource_id | resource.resource.read |
POST /api/resources | resource.resource.write |
POST /api/role-assignments | role-assignment.role-assignment.write |
POST /api/role-assignments/:assignment_id/revoke | role-assignment.revoke.write |
POST /api/role-templates | role-template.role-template.write |
GET /api/router/resolve | router.resolve.read |
GET /api/scheduling/appointments | scheduling.appointment.read |
GET /api/scheduling/appointments/:appointment_id | scheduling.appointment.read |
GET /api/scheduling/appointments/:appointment_id/events | scheduling.appointment.read |
GET /api/scheduling/appointments/:appointment_id/ics | scheduling.appointment.read |
GET /api/scheduling/appointments/:appointment_id/reminders | scheduling.appointment.read |
GET /api/scheduling/availability | scheduling.availability.read |
GET /api/scheduling/calendar-connections | scheduling.calendar-connection.read |
GET /api/scheduling/calendar-connections/:connection_id | scheduling.calendar-connection.read |
GET /api/scheduling/meeting-types | scheduling.meeting-type.read |
GET /api/scheduling/public/links/:slug | scheduling.public.read |
GET /api/scheduling/public/links/:slug/availability | scheduling.public.read |
GET /api/scheduling/scheduling-links | scheduling.scheduling-link.read |
GET /api/scheduling/scheduling-links/:link_id | scheduling.scheduling-link.read |
POST /api/scheduling/appointments | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/calendar-push | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/cancel | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/confirm | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/rebook | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/reminders | scheduling.appointment.write |
POST /api/scheduling/appointments/:appointment_id/reschedule | scheduling.appointment.write |
POST /api/scheduling/availability-rules | scheduling.availability-rule.write |
POST /api/scheduling/calendar-connections | scheduling.calendar-connection.write |
POST /api/scheduling/calendar-connections/:connection_id/sync | scheduling.calendar-connection.write |
POST /api/scheduling/meeting-types | scheduling.meeting-type.write |
POST /api/scheduling/no-show/scan | scheduling.no-show.write |
POST /api/scheduling/public/appointments/:public_token/cancel | scheduling.public.write |
POST /api/scheduling/public/appointments/:public_token/confirm | scheduling.public.write |
POST /api/scheduling/public/links/:slug/book | scheduling.public.write |
POST /api/scheduling/reminders/tick | scheduling.reminder.write |
POST /api/scheduling/scheduling-links | scheduling.scheduling-link.write |
GET /api/search | search.search.read |
GET /api/search/saved-queries | search.saved-query.read |
POST /api/search | search.search.write |
POST /api/search/index | search.index.write |
POST /api/search/saved-queries | search.saved-query.write |
GET /api/secrets/:ref | secret.secret.read |
POST /api/secrets | secret.secret.write |
POST /api/secrets/:ref/rotate | secret.rotate.write |
GET /api/sequences/:sequence_id | sequence.sequence.read |
GET /api/sequences/guards/log | sequence.guard.read |
POST /api/sequences | sequence.sequence.write |
POST /api/sequences/:sequence_id/enroll | sequence.enroll.write |
POST /api/sequences/:sequence_id/steps | sequence.step.write |
POST /api/sequences/:sequence_id/triggers | sequence.trigger.write |
POST /api/sequences/enrollments/:enrollment_id/control | sequence.enrollment.write |
POST /api/sequences/guards/check | sequence.guard.write |
POST /api/sequences/guards/outcome | sequence.guard.write |
POST /api/sequences/tick | sequence.tick.write |
POST /api/sequence-templates | sequence-template.sequence-template.write |
GET /api/service-request/tickets/:ticket_id | service-request.ticket.read |
POST /api/service-request/queues | service-request.queue.write |
POST /api/service-request/tickets | service-request.ticket.write |
POST /api/service-request/tickets/:ticket_id/assign | service-request.ticket.write |
POST /api/service-request/tickets/:ticket_id/transition | service-request.ticket.write |
GET /api/sla/at-risk | sla.at-risk.read |
GET /api/sla/attainment | sla.attainment.read |
GET /api/sla/breach-reasons | sla.breach-reason.read |
GET /api/sla/breaches | sla.breache.read |
GET /api/sla/breaches/:breach_id | sla.breache.read |
GET /api/sla/calendars | sla.calendar.read |
GET /api/sla/calendars/:calendar_id | sla.calendar.read |
GET /api/sla/clocks | sla.clock.read |
GET /api/sla/clocks/:clock_id | sla.clock.read |
GET /api/sla/clocks/:clock_id/firings | sla.clock.read |
GET /api/sla/policies | sla.policy.read |
GET /api/sla/policies/:policy_id | sla.policy.read |
GET /api/sla/policies/:policy_id/rungs | sla.policy.read |
PATCH /api/sla/rungs/:rung_id | sla.rung.write |
POST /api/sla/breach-reasons | sla.breach-reason.write |
POST /api/sla/breach-scan | sla.breach-scan.write |
POST /api/sla/breaches/:breach_id/recovery | sla.breache.write |
POST /api/sla/calendars | sla.calendar.write |
POST /api/sla/clocks | sla.clock.write |
POST /api/sla/clocks/:clock_id/breach | sla.clock.write |
POST /api/sla/clocks/:clock_id/cancel | sla.clock.write |
POST /api/sla/clocks/:clock_id/pause | sla.clock.write |
POST /api/sla/clocks/:clock_id/reassign | sla.clock.write |
POST /api/sla/clocks/:clock_id/resume | sla.clock.write |
POST /api/sla/clocks/:clock_id/satisfy | sla.clock.write |
POST /api/sla/clocks/merge | sla.clock.write |
POST /api/sla/policies | sla.policy.write |
POST /api/sla/policies/:policy_id/rungs | sla.policy.write |
POST /api/sla/systemic-incidents/open-pending | sla.systemic-incident.write |
POST /api/sla/tick | sla.tick.write |
POST /api/social/handles | social.handle.write |
POST /api/social/interactions | social.interaction.write |
POST /api/social/interactions/:interaction_id/capture-lead | social.interaction.write |
GET /api/source-assertions | source-assertion.source-assertion.read |
POST /api/source-assertions | source-assertion.source-assertion.write |
POST /api/source-assertions/:assertion_id/supersede | source-assertion.supersede.write |
GET /api/source-records | source-record.source-record.read |
GET /api/source-records/:capture_id | source-record.source-record.read |
POST /api/source-records | source-record.source-record.write |
POST /api/source-records/:capture_id/crosswalks | source-record.crosswalk.write |
POST /api/source-records/:capture_id/normalize | source-record.normalize.write |
POST /api/source-records/:capture_id/promote | source-record.promote.write |
GET /api/source-rights/attestations | source-right.attestation.read |
GET /api/source-rights/attestations/:attestation_id | source-right.attestation.read |
GET /api/source-rights/permitted-use | source-right.permitted-use.read |
POST /api/source-rights/attestations | source-right.attestation.write |
GET /api/storm/overlay | storm.overlay.read |
GET /api/taxonomy/extraction-schemas | taxonomy.extraction-schema.read |
GET /api/taxonomy/health | taxonomy.health.read |
GET /api/taxonomy/prompt-templates | taxonomy.prompt-template.read |
POST /api/taxonomy/versions/:taxonomy_version_id/activate | taxonomy.version.write |
GET /api/tenants/:tenant_id | tenant.tenant.read |
GET /api/tenants/:tenant_id/contact | tenant.contact.read |
POST /api/tenants | tenant.tenant.write |
POST /api/tenants/:tenant_id/bus | tenant.bu.write |
POST /api/tenants/:tenant_id/fiscal-calendar | tenant.fiscal-calendar.write |
POST /api/tenants/:tenant_id/reseller-attach | tenant.reseller-attach.write |
POST /api/tenants/:tenant_id/sub-tenants | tenant.sub-tenant.write |
GET /api/tenant-lifecycle/:tenant_id/state | tenant-lifecycle.state.read |
POST /api/tenant-lifecycle/:tenant_id/offboard | tenant-lifecycle.offboard.write |
POST /api/tenant-lifecycle/:tenant_id/reinstate | tenant-lifecycle.reinstate.write |
POST /api/tenant-lifecycle/:tenant_id/suspend | tenant-lifecycle.suspend.write |
POST /api/tenant-lifecycle/sandbox | tenant-lifecycle.sandbox.write |
GET /api/trace/:trace_id | trace.trace.read |
GET /api/trace/health | trace.health.read |
POST /api/trace/exports | trace.export.write |
POST /api/trace/regression-assert | trace.regression-assert.write |
GET /api/userinfo | userinfo.userinfo.read |
GET /api/vault/health | vault.health.read |
POST /api/vault/decrypt | vault.decrypt.write |
POST /api/vault/encrypt | vault.encrypt.write |
POST /api/vault/keys | vault.key.write |
POST /api/vault/keys/:key_id/rotate | vault.key.write |
POST /api/vault/keys/:key_id/shred | vault.key.write |
GET /api/voice/calls | voice.call.read |
GET /api/voice/calls/:voice_call_id | voice.call.read |
GET /api/voice/tracking-numbers | voice.tracking-number.read |
POST /api/voice/calls | voice.call.write |
POST /api/voice/tracking-numbers | voice.tracking-number.write |
POST /api/voice/tracking-numbers/:tracking_number_id/release | voice.tracking-number.write |
POST /api/voice/webhooks/twilio/recording | voice.webhook.write |
POST /api/voice/webhooks/twilio/status | voice.webhook.write |
GET /api/webhooks/deliveries | webhook.delivery.read |
GET /api/webhooks/dlq | webhook.dlq.read |
GET /api/webhooks/endpoints | webhook.endpoint.read |
POST /api/webhooks/deliveries/:delivery_id/replay | webhook.delivery.write |
POST /api/webhooks/endpoints | webhook.endpoint.write |
POST /api/webhooks/endpoints/:endpoint_id/subscribe | webhook.endpoint.write |
POST /api/webhooks/publish | webhook.publish.write |
GET /api/workflows/:run_id | workflow.workflow.read |
POST /api/workflows/:run_id/signal | workflow.signal.write |
POST /api/workflows/definitions | workflow.definition.write |
POST /api/workflows/start | workflow.start.write |
Two environment variables and one header. Both a local gateway and the hosted one are supported — the only difference is the base URL and which key you present.
| Target | Base URL | Notes |
|---|---|---|
| Hosted | https://cloud.projexlight.com | Use a pk_live_ key issued against your real tenant |
| Local | http://localhost:4000 | Port from GATEWAY_PORT in the root .env (4000 by default). Issue a pk_test_ key against your dev tenant |
# .env of YOUR application — never commit the key PROJEXCLOUD_GATEWAY_URL=https://cloud.projexlight.com PROJEXCLOUD_API_KEY=pk_live_… PROJEXCLOUD_TENANT_ID=<your-tenant-uuid> PROJEXCLOUD_TIMEOUT_MS=8000
POST {PROJEXCLOUD_GATEWAY_URL}/api/sla/clocks
Authorization: Bearer {PROJEXCLOUD_API_KEY} ← the key goes HERE
Content-Type: application/json
Idempotency-Key: <stable key per logical operation>
x-correlation-id: <uuid>
{ "tenant_id": "…", "policy_id": "…", "subject_ref": "lead:…" }
x-api-key: … — not read by these routes. The credential goes in Authorization: Bearer.x-tenant-id: … — not read either. tenant_id belongs in the request body or query string./{sdk}/v1/… URL — there is no per-SDK path prefix. Every SDK registers its own real
/api/<domain>/… routes on the one gateway. Check the API reference
for the exact path before you code against it.Because handlers read tenant_id from the payload, the gateway cross-checks it against the
key's own tenant and answers 403 on a mismatch. A leaked key therefore cannot be aimed at
another tenant — but it also means the tenant_id you send must be the one the key was issued for.
Read the base URL from the environment rather than hard-coding it, keep a separate key per target, and let a
run-time override win over the committed default. A shell-provided value beats a .env entry with
most dotenv loaders, which makes switching a one-command affair:
PROJEXCLOUD_GATEWAY_URL=http://localhost:4000 npm run dev # local gateway PROJEXCLOUD_GATEWAY_URL=https://cloud.projexlight.com npm run dev
Both are supported on every tenant route, and both are checked by the same gate. Pick by volume.
curl "$GATEWAY/api/sla/policies?tenant_id=$TENANT" -H "Authorization: Bearer $PROJEXCLOUD_API_KEY"
client_credentials. Verify once, call many times.curl -sX POST "$GATEWAY/api/auth/token" -H 'Content-Type: application/json' -d '{"grant_type":"client_credentials",
"client_id":"web-backend",
"client_secret":"'"$PROJEXCLOUD_API_KEY"'"}'
# → { "access_token":"eyJ…", "token_type":"Bearer",
# "expires_in":900, "scope":"sla.clock.read sla.clock.write" }
scope may narrow the token below what the key holds — never widen itinvalid_client with identical wording. That is
deliberate — a distinguishable answer would confirm to anyone probing with harvested strings that a
particular client exists. Check GET /api/api-keys to see which of your own keys are live.
| Operation | Call | Behaviour |
|---|---|---|
| List | GET /api/api-keys?tenant_id=… | Returns records with the display prefix only — never the key |
| Rotate | POST /api/api-keys/:key_id/rotate | 201 with a new plaintext. The old key keeps working for a 24-hour grace window, so you can deploy before revoking |
| Revoke | POST /api/api-keys/:key_id/revoke | Immediate, and broadcast over Redis to every gateway replica within a second |
Rotation without downtime: rotate → deploy the new key → confirm traffic on the new prefix via
last_used_at → revoke the old key. Skipping the final revoke leaves a live credential in the wild
after the grace window ends.
Every issuance, rotation and revocation emits an audit event (api-key.issued.v1,
api-key.rotated.v1, api-key.revoked.v1), so key custody is reviewable after the fact.
| Symptom | Cause | Fix |
|---|---|---|
401 Missing bearer token | No Authorization header, or the key is in x-api-key | Send Authorization: Bearer pk_live_… |
401 API key invalid, revoked, or expired | Wrong key, revoked, past expires_at, or a live key against a local gateway (each target has its own store) | Check with GET /api/api-keys?tenant_id=…; confirm you are pointed at the right gateway |
403 …missing required scope(s) | Key lacks the derived scope | The response lists required_scopes and granted_scopes — rotate or re-issue with the missing scope |
403 …issued for a different tenant | Payload tenant_id ≠ the key's tenant | Send the key's own tenant id |
404 on a plausible path | A /{sdk}/v1/… style URL | Use the SDK's real /api/<domain>/… route from the API reference |
Works locally, 401 in cloud | Dev key used against the hosted gateway | Issue a key per environment |