> 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/publishing-applications.md).

# Publish APIs and register applications

Developer Portal publishes existing gateway services and routes through a label convention on those resources.

## Publish a service

A service is public when any one of these values is true:

```
portal_visible: true
published: true
labels.portal_visible: true
labels.published: true
labels.portal: "public"
labels.portal: "published"
labels.portal: true
```

The Admin Service API exposes publication metadata through `labels`. It also reads these optional customer fields from labels: `description`, `version`, `docs_url`, and `openapi_url`.

```bash
curl -sS -X PATCH \
  "$KANGAL_ADMIN_URL/admin/gateways/$GATEWAY_ID/services/$SERVICE_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "labels": {
      "portal": "public",
      "description": "Create and inspect customer orders.",
      "version": "2026.2",
      "docs_url": "https://docs.example.com/orders",
      "openapi_url": "https://docs.example.com/orders/openapi.json",
      "portal_approval_required": true
    }
  }'
```

`PATCH` replaces the complete `labels` object. Read and preserve unrelated labels before publishing.

## Publish routes

A public service only contains routes that independently meet the same portal marker rule. The route must also reference the service by `service_id` or `service_name`.

```bash
curl -sS -X PATCH \
  "$KANGAL_ADMIN_URL/admin/gateways/$GATEWAY_ID/routes/$ROUTE_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "labels": {
      "portal_visible": true,
      "summary": "List orders",
      "description": "Returns orders visible to the authenticated consumer."
    }
  }'
```

The catalog exposes route `path`, uppercase `methods`, `hosts`, `snis`, linked service identifiers, summary, description, and labels. Runtime headers, query matchers, plugins, and upstream destinations are not included in the public portal response.

Changelog entry publication is controlled independently from service and route labels. Before making a service private, set `published: false` on all of its changelog entries through the changelog management workflow, then change the service and route markers to non-public values. Retire API versions through the forward-only version lifecycle; retirement and catalog visibility are separate operations.

## Verify anonymous visibility

```bash
curl -sS \
  "$KANGAL_ADMIN_URL/portal/catalog?tenant_id=$TENANT_ID&gateway_id=$GATEWAY_ID"
```

Then verify one service and its generated contract:

```bash
curl -sS \
  "$KANGAL_ADMIN_URL/portal/services/$SERVICE_ID/openapi?tenant_id=$TENANT_ID&gateway_id=$GATEWAY_ID"
```

Admin preview adds `include_unpublished=true` and the bearer JWT from an authenticated, read-capable admin user's Kangal session. An Admin API token is not a portal preview credential, and an anonymous preview attempt returns `403`.

When publishing or making a service private, verify anonymous responses for the catalog, service, OpenAPI, changelog, and version endpoints. In particular, confirm that the service's anonymous changelog is empty before changing its service and route markers to private.

## Generated OpenAPI content

The OpenAPI endpoint builds version `3.0.3` from the current published route inventory:

* service name, description, and version become `info`;
* each route path and method becomes an operation;
* route summary/description are copied;
* generic `200`, `400`, `401`, and `500` responses are added.

The generated document is a route-level catalog contract. `openapi_url` remains an external documentation link and is not used as the source of this generated document. Review the generated JSON before using it for client generation or contract testing.

## Manage API versions

Create a draft:

```bash
curl -sS -X POST \
  "$KANGAL_ADMIN_URL/portal/services/$SERVICE_ID/versions?tenant_id=$TENANT_ID&gateway_id=$GATEWAY_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "version": "2026.2",
    "lifecycle": "draft",
    "release_notes": "Adds bulk order lookup.",
    "specification_url": "https://docs.example.com/orders/2026.2/openapi.json"
  }'
```

`version` is 1-80 characters and unique by tenant, gateway, service, and value. `release_notes` is limited to 5,000 characters and `specification_url` to 500. The version stores an immutable route-derived OpenAPI snapshot at creation. `specification_url` is metadata/link only; its document is not downloaded or validated and does not replace the snapshot used for SDK generation.

Lifecycle is forward-only:

```
draft -> published -> deprecated -> retired
```

Self-transitions are allowed for metadata updates. Skipping or reversing a state returns `409`. Deprecation requires a non-empty `deprecation_notice` (maximum 2,000 characters) and can include `sunset_at`. Public version lists show `published` and `deprecated`; `draft` and `retired` require admin preview.

```bash
curl -sS -X PATCH \
  "$KANGAL_ADMIN_URL/portal/services/$SERVICE_ID/versions/$VERSION_ID?tenant_id=$TENANT_ID&gateway_id=$GATEWAY_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "lifecycle": "deprecated",
    "deprecation_notice": "Move to 2027.1 before sunset.",
    "sunset_at": "2027-06-30T21:00:00Z"
  }'
```

A deprecated version remains in the public version list until it is explicitly moved to `retired`; reaching `sunset_at` does not hide it or change its lifecycle. At sunset, Kangal blocks new subscriptions and credential issuance for that version. Move the version to `retired` through the `PATCH` lifecycle workflow to remove it from the public list; retirement also changes bound applications to `suspended_api_version` and expires their credentials immediately.

## Publish changelog entries

`POST /portal/services/{service_id}/changelog` accepts:

| Field         | Rules                                                               |
| ------------- | ------------------------------------------------------------------- |
| `title`       | Required, 1-200 characters                                          |
| `body`        | Optional, up to 5,000 characters                                    |
| `version`     | Optional, up to 80 characters                                       |
| `change_type` | `added`, `changed`, `deprecated`, `removed`, `fixed`, or `security` |
| `published`   | Defaults to `true`; false entries require admin preview             |
| `metadata`    | Arbitrary JSON object                                               |

Changelog entries are returned newest-first. Their `published` value is checked independently of service and route publication labels. Before making a service private, unpublish its changelog entries and verify the anonymous changelog endpoint.

## Application request fields

Signed-in developers call:

```
POST /portal/services/{service_id}/applications?tenant_id=...&gateway_id=...
```

| Field              | Rules                                        |
| ------------------ | -------------------------------------------- |
| `app_name`         | Required, 1-120 characters                   |
| `developer_email`  | Required valid email                         |
| `developer_name`   | Optional, up to 120 characters               |
| `description`      | Optional, up to 500 characters               |
| `requested_scopes` | String list; blanks removed                  |
| `ttl_seconds`      | Optional positive integer                    |
| `api_version`      | Optional version name or ID, 1-80 characters |
| `labels`           | Arbitrary JSON object                        |

If versions exist, the request must resolve to a published or not-yet-sunset deprecated version. With no `api_version`, Kangal chooses the newest published version, then the newest compatible deprecated version. If no version records exist, an unversioned application is still allowed.

Requests from the public portal use a 24-hour key lifetime and standard sample scopes. The gateway-admin portal form defaults to 30 days and accepts a comma-separated scope list. API clients can submit the fields in the table above.

## Automatic and manual approval

Manual approval is enabled by any true service field/label named `approval_required` or `portal_approval_required`, or `labels.portal_approval` set to `required`, `manual`, or `true`.

Without that marker:

1. Kangal creates or reuses a tenant-scoped `developer-portal` consumer.
2. It issues an encrypted-at-rest `api_key` beginning with `mgk_`.
3. It returns application status `active` with the newly issued key. Capture that creation response and place the key in an approved secret store.

With manual approval:

1. Registration returns `pending_approval` with no credential.
2. Tenant admins list requests with `GET /portal/applications?tenant_id=...&gateway_id=...&status=pending_approval`.
3. Approval issues the credential. Capture the successful approval response and place the key in an approved secret store; rejection records reviewer, timestamp, and optional reason.
4. Repeating approval is idempotent.

The authenticated applicant may be from another tenant. The generated consumer and credential belong to the API publisher tenant because that is the runtime scope of the published API.

## Troubleshooting

| Status or symptom                            | Check                                                                                                  |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Published service has no routes              | Each route needs its own public marker and a matching service reference                                |
| Active private services appear               | No entity in that scope has any portal marker, so compatibility fallback is active                     |
| `403` on preview                             | Use a read-capable admin user's Kangal session JWT for the requested tenant, not an Admin API token    |
| `401` on registration                        | Missing JWT or expired Redis-backed session                                                            |
| `404 Portal service not found`               | Service is unpublished, wrong tenant/gateway/service ID, or route scope is mismatched                  |
| `409 API version ... already exists`         | Version values are unique within the service scope                                                     |
| `409 No published API version`               | Publish a draft or choose a compatible version before registering                                      |
| OpenAPI contains route-level operations only | Verify the generated route contract and use `openapi_url` for separately hosted detailed documentation |
| Application stays pending                    | Service requires manual approval; approve from the admin portal or API                                 |


---

# 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/publishing-applications.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.
