# API version of: ziti edge enroll --jwt identity.jwt --out identity.json

**URL:** <https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904>\
**Category:** Uncategorized\
**Created:** [July 31, 2024, 4:20pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904 "2024-07-31T16:20:59Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![yemaney](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/yemaney/32/2201_2.png) [@yemaney](https://openziti.discourse.group/u/yemaney)\
**Post date:** [July 31, 2024, 4:20pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/1 "2024-07-31T16:20:59Z")

</div>

The docs show the below command to create a json file from an identity jwt. What would be the functional equivalen for this enroll command that returns this json using the zitii api?

I've tried the endpoint `/edge/client/v1/enroll?method=ott&id={token}` where token is the jwt, but getting an `INVALID_ENROLLMENT_TOKEN` error response. I have also tried using the id of the actual identity as the value for token.

- command

```sh
./ziti-edge-tunnel enroll --jwt ./myTunneler.jwt --identity ./myTunneler.json

```

- output

```auto
{
    "ztAPI": "https://ziti.com:1280/edge/client/v1",
    "ztAPIs": null,
    "configTypes": null,
    "id": {
        "key": "pem:-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----\n",
        "cert": "pem:-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\n",
        "ca": "pem:-----BEGIN CERTIFICATE-----...-----\n"
    },
    "enableHa": false
}

```

---

<div class="post-metadata">

**Author:** ![qrkourier](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/qrkourier/32/52_2.png) [@qrkourier](https://openziti.discourse.group/u/qrkourier)\
**Post date:** [July 31, 2024, 4:25pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/2 "2024-07-31T16:25:31Z")

</div>

Which language are you working in? I'll link you directly to an SDK sample if we've got one. If not then I'll help you navigate the spec to get this done.

---

<div class="post-metadata">

**Author:** ![yemaney](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/yemaney/32/2201_2.png) [@yemaney](https://openziti.discourse.group/u/yemaney)\
**Post date:** [July 31, 2024, 4:35pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/4 "2024-07-31T16:35:41Z")

</div>

We are using ruby to make requests to the api.  
If its simpler to do with python sdk, then that could be an option..

---

<div class="post-metadata">

**Author:** ![qrkourier](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/qrkourier/32/52_2.png) [@qrkourier](https://openziti.discourse.group/u/qrkourier)\
**Post date:** [July 31, 2024, 4:46pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/5 "2024-07-31T16:46:14Z")

</div>

I'm sure it's doable in Ruby, but I'm glad you mentioned Python. Here's how the Py SDK does it, by importing Ziti's C library.

```python
def enroll(jwt, key=None, cert=None):
    """
    Enroll Ziti Identity
    :param jwt: (required) enrollment token,
        can be either name of the token file or a string containing JWT
    :param key: private key to use for enrollment
        (required for 3rd party CA enrollment, otherwise optional,
        new key is generated if None)
    :param cert: certificate to use for enrollment
        (required for 3rd party CA enrollment)
    :return: string containing Ziti Identity in JSON format
    """
    init()
    try:
        with open(jwt, 'rb') as jwt_f:
            jwtc = bytes(jwt_f.read())
    except:
        jwtc = bytes(jwt, 'utf-8')

    id_json = ctypes.c_char_p()
    id_json_len = ctypes.c_size_t()

    keyb = None if key is None else bytes(key, 'utf-8')
    certb = None if cert is None else bytes(cert, 'utf-8')

    retcode = _ziti_enroll(jwtc, keyb, certb,
                           ctypes.byref(id_json),
                           ctypes.byref(id_json_len))
    if retcode != 0:
        raise RuntimeError(errorstr(retcode))
    try:
        return id_json.value.decode()
    finally:
        _free(id_json)

```

from: [ziti-sdk-py/src/openziti/zitilib.py at main · openziti/ziti-sdk-py · GitHub](https://github.com/openziti/ziti-sdk-py/blob/main/src/openziti/zitilib.py#L284)

This function can be called by invoking the module's `enroll` command.

```bash
python -m openziti enroll --jwt /tmp/py1.jwt --identity /tmp/py1.json

```

It would be good to document the REST requests for this so I'll also work on doing that for Python.

---

<div class="post-metadata">

**Author:** ![yemaney](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/yemaney/32/2201_2.png) [@yemaney](https://openziti.discourse.group/u/yemaney)\
**Post date:** [July 31, 2024, 4:54pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/6 "2024-07-31T16:54:13Z")

</div>

Thanks, I'll try running this an reply asap

---

<div class="post-metadata">

**Author:** ![yemaney](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/yemaney/32/2201_2.png) [@yemaney](https://openziti.discourse.group/u/yemaney)\
**Post date:** [July 31, 2024, 8:06pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/7 "2024-07-31T20:06:54Z")

</div>

It worked. I'm using it within the code rather than through cli.

```python
import openziti

def get_jwt():
   "retuns either jwt raw string or filepath to jwt file"
   ...

jwt = get_jwt()

enrolled_json = openziti.enroll(jwt)
print(enrolled_json)

```

---

<div class="post-metadata">

**Author:** ![qrkourier](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/qrkourier/32/52_2.png) [@qrkourier](https://openziti.discourse.group/u/qrkourier)\
**Post date:** [July 31, 2024, 8:17pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/8 "2024-07-31T20:17:48Z")

</div>

Awesome. Just remembered these exist and realized they are probably now stale since some time has passed since the spec they were generated from.

Still, these might be useful.

Client library would be used by the same app that is enrolling: [GitHub - openziti-test-kitchen/openziti-edge-client-python: Python Library Implementing the OpenZiti Edge Client API](https://github.com/openziti-test-kitchen/openziti-edge-client-python)

Mgmt library would be used for managing Ziti entities, including client identities: [GitHub - openziti-test-kitchen/openziti-edge-management-python: Python library for the Ziti Management API](https://github.com/openziti-test-kitchen/openziti-edge-management-python)

Here's a post by the developer explaining how and why these libraries were generated: [OpenAPI Python Clients](https://blog.openziti.io/openapi-python-clients)

---

<div class="post-metadata">

**Author:** ![qrkourier](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/qrkourier/32/52_2.png) [@qrkourier](https://openziti.discourse.group/u/qrkourier)\
**Post date:** [July 31, 2024, 9:04pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/9 "2024-07-31T21:04:29Z")

</div>

`Enroll()` from [the Go SDK](https://github.com/openziti/sdk-golang/blob/v0.23.39/ziti/enroll/enroll.go#L159) will be the best reference. Let's translate this to Python or Ruby or both.

```go
func Enroll(enFlags EnrollmentFlags) (*ziti.Config, error) {
	var key crypto.PrivateKey
	var err error

	cfg := &ziti.Config{
		ZtAPI: edge_apis.ClientUrl(enFlags.Token.Issuer),
	}

	if strings.TrimSpace(enFlags.KeyFile) != "" {
		stat, err := os.Stat(enFlags.KeyFile)

		if stat != nil && !os.IsNotExist(err) {
			if stat.IsDir() {
				return nil, errors.Errorf("specified key is a directory (%s)", enFlags.KeyFile)
			}

			if absPath, fileErr := filepath.Abs(enFlags.KeyFile); fileErr != nil {
				return nil, fileErr
			} else {
				cfg.ID.Key = "file://" + absPath
			}

		} else {
			cfg.ID.Key = enFlags.KeyFile
			pfxlog.Logger().Infof("using engine : %s\n", strings.Split(enFlags.KeyFile, ":")[0])
		}
	} else {
		var asnBytes []byte
		var keyPem []byte
		if enFlags.KeyAlg.EC() {
			key, err = generateECKey()
			asnBytes, _ := x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey))
			keyPem = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes})
		} else if enFlags.KeyAlg.RSA() {
			key, err = generateRSAKey()
			asnBytes = x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey))
			keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: asnBytes})
		} else {
			panic(fmt.Sprintf("invalid KeyAlg specified: %s", enFlags.KeyAlg.Get()))
		}
		cfg.ID.Key = "pem:" + string(keyPem)
		if err != nil {
			return nil, err
		}
	}

	if enFlags.CertFile != "" {
		enFlags.CertFile, _ = filepath.Abs(enFlags.CertFile)
		cfg.ID.Cert = "file://" + enFlags.CertFile
	}

	caPool, allowedCerts := enFlags.GetCertPool()

	//fetch so CA bundles
	pfxlog.Logger().Debug("fetching certificates from server")
	serverOnlyCaPool := x509.NewCertPool()
	serverOnlyCaPool.AddCert(enFlags.Token.SignatureCert)

	controllerCas := FetchCertificates(cfg.ZtAPI, serverOnlyCaPool)

	if len(controllerCas) == 0 {
		return nil, errors.New("expected 1 or more CAs from controller, got 0")
	}

	for _, cert := range controllerCas {
		allowedCerts = append(allowedCerts, cert)
		caPool.AddCert(cert)
	}

	var enrollErr error
	switch enFlags.Token.EnrollmentMethod {
	case "ott":
		enrollErr = enrollOTT(enFlags.Token, cfg, caPool)
	case "ottca":
		enrollErr = enrollCA(enFlags.Token, cfg, caPool)
	case "ca":
		enrollErr = enrollCAAuto(enFlags, cfg, caPool)
	default:
		enrollErr = errors.Errorf("enrollment method '%s' is not supported", enFlags.Token.EnrollmentMethod)
	}

	if enrollErr != nil {
		return nil, enrollErr
	}

	if len(allowedCerts) > 0 {
		var buf bytes.Buffer

		err := nfx509.MarshalToPem(allowedCerts, &buf)

		if err != nil {
			return nil, err
		}

		cfg.ID.CA = "pem:" + buf.String()
	}

	cfg.Credentials = edge_apis.NewIdentityCredentialsFromConfig(cfg.ID)

	return cfg, nil
}

```

This code snippet defines a function called `Enroll` in Go. The function takes an `EnrollmentFlags` parameter and returns a Ziti context, which is a data structure containing an identity typically stored as JSON.

The function starts by checking whether a private key is provided and generates one if not. The same function can be used to enroll an identity with a certificate from a trusted authority or to request a certificate from the edge enrollment signer CA managed by the Ziti controller, so there's an affordance here for an cert to be provided instead of issued.

The function then fetches the well-known (trusted) certificates from the client API provided by the controller.

Based on the `enFlags.Token.EnrollmentMethod` value, the function calls different enrollment methods (`enrollOTT`, `enrollCA`, `enrollCAAuto`) and assigns the result to the `enrollErr` variable.

We'll follow the `enrollOTT` possibility here because it's the most relevant. This `ott` method is called when enrolling with a one time token (JWT) and requesting a certificate to be issued.

Here's a summary of what `enrollOTT` does:

1. It loads the private key.
2. It creates a certificate request as PEM.
3. It sends a POST request to the enrollment URL specified in `token.EnrolmentUrl()` with the certificate request in PEM format.
4. If the status code is `http.StatusOK`, it checks the `content-type` header. If it's `application/json`, it parses the response body as JSON and extracts the `data.cert` field, which is assumed to contain the certificate in PEM format. It then sets `cfg.ID.Cert` to this value.
5. If the `content-type` header is not `application/json`, it treats the response body as PEM and sets `cfg.ID.Cert` to this value.

Finally, the function returns a Ziti context object from the key, cert, and well-known pool.

---

<div class="post-metadata">

**Author:** ![am3y](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/am3y/32/2366_2.png) [@am3y](https://openziti.discourse.group/u/am3y)\
**Post date:** [May 21, 2025, 5:06pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/10 "2025-05-21T17:06:17Z")

</div>

**Hi everyone,**

I’m building a pure-bash enrollment script and can’t pull in any of the language SDKs. I need the raw REST API equivalent of the **“3rd Party CA – One Time Token”** flow (see: [https://openziti.io/docs/learn/core-concepts/identities/enrolling/#3rd-party-ca---one-time-token](https://openziti.io/docs/learn/core-concepts/identities/enrolling/#3rd-party-ca---one-time-token))

Specifically, could someone share:

1. The exact HTTP method and full URL (including the `token` query param).

2. Any required headers.

3. The JSON payload shape for `/enroll/ott`.

4. A sample `curl`-style request and its response.

Thanks in advance!

---

<div class="post-metadata">

**Author:** ![TheLumberjack](https://yyz2.discourse-cdn.com/free1/user_avatar/openziti.discourse.group/thelumberjack/32/113_2.png) [@TheLumberjack](https://openziti.discourse.group/u/TheLumberjack)\
**Post date:** [May 21, 2025, 5:19pm UTC](https://openziti.discourse.group/t/api-version-of-ziti-edge-enroll-jwt-identity-jwt-out-identity-json/2904/11 "2025-05-21T17:19:43Z")

</div>

you can run the ziti cli though, right? this is old (we dont have a ziti-enroller binary any more) but is something like this what you're looking for? [ziti/etc/ubuntu-snap/ott.sh at main · openziti/ziti · GitHub](https://github.com/openziti/ziti/blob/main/etc/ubuntu-snap/ott.sh)

Also, it might be a good idea to make this a new topic
