> For the complete documentation index, see [llms.txt](https://docs.kangal.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kangal.dev/developer-portal/credentials-sdks-domains.md).

# Credentials, SDKs, and domains

Developer Portal connects three separate lifecycles:

1. an application creates a consumer credential;
2. an API version supplies the immutable contract for SDK export;
3. a custom-domain resource proves DNS ownership and activates a portal edge binding.

Treat them as separate resources. A theme value does not provision a domain, and an SDK export does not issue or embed a credential.

## Portal API keys

An automatically approved registration or a successful manual approval creates an `api_key` with a random `mgk_...` value. The key is encrypted at rest and looked up through a blind index. Runtime lookup rejects expired, revoked, disabled, inactive, suspended, or deleted credentials.

Capture `credential.key` from the automatic registration or successful approval response that creates the credential, then transfer it directly to an approved secret store.

```json
{
  "status": "active",
  "consumer": {
    "id": "consumer-id",
    "username": "portal-dev-mobile-checkout",
    "tenant_id": "acme"
  },
  "credential": {
    "id": "credential-id",
    "type": "api_key",
    "key": "mgk_...",
    "expires_at": "2026-07-18T12:00:00Z"
  }
}
```

Treat the key as a production secret throughout its lifecycle. Restrict Admin API and audit-log access to designated operators, redact credential values from support material, and use the creation response as the controlled handoff to the consuming workload.

### Expiry and API versions

* `ttl_seconds` must be positive and has no schema maximum.
* With a version sunset, issuance uses the earlier of requested TTL and `sunset_at`.
* Without TTL or a sunset, the generated API key has no automatic expiration.
* A deprecated version remains listed until it is retired. At `sunset_at`, new subscriptions and credential issuance for that version are blocked.
* Retiring a bound API version expires its credentials immediately and changes application status to `suspended_api_version`.

{% hint style="warning" %}
Setting `sunset_at` on a deprecated version sets bound credential expiry to that timestamp. Review short-lived credentials before choosing a later sunset, because their expiry may be extended to the new value.
{% endhint %}

### Authorization requirements

`requested_scopes` are request metadata saved on the application. A key's actual access is determined by the gateway's route and authentication configuration for that consumer.

Configure and test route-level authorization separately. Treat displayed scope names as requested access, not proof of enforced permission.

### Revoke or replace a key

Delete an exposed portal API key:

```bash
curl -sS -X DELETE \
  "$KANGAL_ADMIN_URL/admin/consumer-credentials/$CREDENTIAL_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

Then create a replacement through a new application registration or the tenant-scoped Admin Credential API. Rejecting a **pending** application prevents issuance. Application status and credential revocation are separate operations; delete an active application's credential explicitly.

To call an API-key-protected gateway route, generated clients can send:

```
X-Tenant-ID: <publisher-tenant>
X-Gateway-ID: <gateway-id>
X-API-Key: <mgk-key>
```

The exact headers ultimately depend on the route's installed authentication plugins.

## Generated SDKs

SDK export requires a read-capable administrator in the publisher tenant, the `developer_portal.manage` entitlement, a publicly visible service, and a published or not-yet-sunset deprecated API version with a stored OpenAPI snapshot.

Supported targets:

| `target`           | Minimum runtime       | Transport       |
| ------------------ | --------------------- | --------------- |
| `typescript-fetch` | ES2022                | Fetch           |
| `python`           | Python 3.9            | `urllib`        |
| `go`               | Go 1.21               | `net/http`      |
| `java`             | Java 11               | `java.net.http` |
| `csharp`           | .NET 8                | `HttpClient`    |
| `php`              | PHP 8.1 plus ext-curl | cURL            |

Export through the API:

```bash
curl -sS \
  "$KANGAL_ADMIN_URL/portal/services/$SERVICE_ID/versions/$VERSION_ID/sdk?tenant_id=$TENANT_ID&gateway_id=$GATEWAY_ID&target=python" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

Or with the repository CLI:

```bash
python scripts/kangalctl.py sdk export \
  --service-id "$SERVICE_ID" \
  --version-id "$VERSION_ID" \
  --gateway-id "$GATEWAY_ID" \
  --target python \
  --output orders-sdk.zip
```

The API returns JSON, not raw ZIP bytes:

| Field                   | Meaning                           |
| ----------------------- | --------------------------------- |
| `schema_version`        | Artifact contract; currently `2`  |
| `filename`              | Sanitized `.zip` name             |
| `media_type`            | `application/zip`                 |
| `encoding`              | `base64`                          |
| `sha256` / `size_bytes` | Whole archive integrity           |
| `contract_sha256`       | Canonical stored OpenAPI checksum |
| `generator`             | Name, version `2.1.0`, and target |
| `files`                 | Archive file inventory            |
| `content`               | Base64 archive bytes              |

The ZIP includes generated client sources, package metadata, the complete `REFERENCE.md` method index, `openapi.json`, `checksums.sha256`, and `sdk-manifest.json`. Generation is deterministic: canonical JSON, sorted files, fixed ZIP timestamps and modes, and no package manager, network call, template path, shell command, or user-provided executable.

Generated clients provide one public SDK method per OpenAPI operation plus a route-derived generic request fallback, runtime `baseUrl`, API key and bearer-token options, published tenant/gateway header defaults, per-request overrides, and language-appropriate API errors. Credentials are never embedded. Retry status handling differs by target; see [Authentication, retries, and errors](/sdks/security-and-errors.md).

{% hint style="warning" %}
The SDK uses the route-derived OpenAPI snapshot stored with the API version. Route changes made after version creation do not alter an existing artifact, and `specification_url` remains a separate documentation link. Verify the snapshot contains the intended routes, methods, paths, and customer-facing descriptions before publishing the generated package.
{% endhint %}

Consumers must not call the SDK export endpoint or hold an Admin API token. Platform teams should verify the archive and publish it through an approved registry or delivery channel.

See [SDK overview](/sdks/sdks.md), [generated client workflow](/sdks/api-client.md), and [language quickstarts](/sdks/language-quickstarts.md). The official control plane client surface is indexed in the [Admin API SDK method reference](/sdks/admin-api-methods.md).

## Portal custom domains

There are two different fields named around portal domains:

* `PUT /portal/theme` stores `custom_domain` metadata only.
* `POST /admin/gateways/{gateway_id}/custom-domains` with `target: "portal"` creates the ownership, certificate, SNI, and binding lifecycle.

Only the second workflow can activate a hostname.

### Create a portal domain

Admin custom-domain endpoints also have `/api/v1/admin/...` aliases.

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/admin/gateways/$GATEWAY_ID/custom-domains" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{
    \"tenant_id\": \"$TENANT_ID\",
    \"hostname\": \"developers.example.com\",
    \"target\": \"portal\",
    \"dns_provider\": \"manual\",
    \"dns_provider_config\": {},
    \"dns_credentials\": {},
    \"acme_provider\": \"test-ca\",
    \"acme_config\": {},
    \"dry_run\": true
  }"
```

Hostnames are normalized with IDNA, must be concrete fully qualified names, and are globally unique across tenants. Wildcards, IP addresses, localhost, invalid labels, and an existing SNI are rejected.

The create response includes:

```
challenge_record_name: _kangal-verification.developers.example.com
challenge_value: kangal-domain-verification=<random-token>
challenge_expires_at: <24 hours after creation>
status: pending_dns
```

Publish the exact TXT value, then verify:

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/admin/gateways/$GATEWAY_ID/custom-domains/$DOMAIN_ID/verify?tenant_id=$TENANT_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

Successful verification consumes that challenge version and queues one idempotent certificate job. Before verification, `POST .../challenge` rotates an expired/compromised challenge. After activation, `POST .../renew` queues renewal. `POST .../retry` is available only after `issuance_failed`; it can override `dry_run` when deployment safety switches permit production issuance.

The gateway UI supports manual DNS or a read-only Cloudflare lookup. Cloudflare requires `zone_id` and an API token; it does not create or modify DNS records. Certificate choices are the built-in test CA or cert-manager. `dry_run` defaults to true. Non-dry-run cert-manager issuance requires the deployment's explicit public-ACME safety setting.

Common states are `pending_dns`, `verification_failed`, `issuance_pending`, `issuing`, `issuance_failed`, `active`, `renewal_due`, `renewing`, and `renewal_failed`. Renewal failure preserves the previous active certificate and SNI while exposing `last_error` and expiry alert state.

### Edge binding requirements

Successful issuance writes a `custom_domain_bindings` record with target `portal` and the portal path `/portal/{tenant_id}`. Operators must project that record into Ingress, Gateway API, DNS, or the external load balancer through a release-time Helm binding or a separately reviewed edge controller. Verify the edge route and TLS path before directing public traffic; `active` records the control-plane lifecycle, not an end-to-end reachability test.

Deleting a domain cancels pending jobs and retires managed certificate/SNI state, but external DNS and edge resources remain the operator's responsibility.

## Troubleshooting

| Symptom                                             | Check                                                                                                                                                   |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| New application cannot authenticate                 | Verify that the creation or approval workflow transferred the issued key into the workload's secret store, then rotate the key if exposure is suspected |
| Requested scope has no effect                       | Scope-to-route authorization is not automatically wired                                                                                                 |
| `409` SDK export                                    | Version is draft, retired, past sunset, or missing its stored contract                                                                                  |
| SDK request surface differs from the current routes | Export a new published version; an existing artifact remains tied to its stored route-derived snapshot                                                  |
| `422` SDK target                                    | Use one of the six documented target identifiers                                                                                                        |
| Domain verification fails                           | Exact TXT owner/name, 24-hour challenge validity, DNS propagation, and selected resolver/provider                                                       |
| Domain is active but unreachable                    | Edge projection, frontend routing, TLS secret, DNS A/AAAA/CNAME, and load-balancer configuration                                                        |
| cert-manager dry-run never serves traffic           | Dry-run validates workflow without completing a public certificate activation                                                                           |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kangal.dev/developer-portal/credentials-sdks-domains.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
