---
title: Project tokens
description: Create a token that reaches only one project, and deploy from GitHub Actions or another CI system with it.
section: Account
order: 4
---

A project token is an [API key](/docs/api-keys) that reaches only one project, which makes it the safer choice for CI. It can use that project's services, deployments, environments and variables, and every other project and every organisation-level route answer it with 404.

## Create a token

Open the project's **Settings** and choose **Create token** under **Project tokens**. Give it a name that says where it's used, such as `github-actions`, and leave **Can deploy and change settings** on for a token that deploys, or turn it off for one that only reads. Dekka shows the token once and stores only a hash, so copy it into your CI secrets, as `DEKKA_TOKEN` for the examples below, before you close the dialog.

Only admins and owners can create project tokens, and an API key can't create one. A token made in the dashboard doesn't expire, so revoke it when you stop using it.

## What can a token do?

| Access | Acts as | Lets the token |
| --- | --- | --- |
| Read | Viewer | See the project's services, deployments, variables and logs |
| Write | Developer | Deploy, and create and change services and variables |

A token never does more than the person who created it can, and Dekka checks their role on every request. If that person becomes a viewer, their write tokens can only read, and if they leave the organisation their tokens are revoked.

## Find the service to deploy

Deploying needs the ID of the service, which the API lists for the project. The project ID is in the dashboard's address after `/projects/`.

```bash
curl https://api.dekka.cloud/v1/projects/$PROJECT_ID/resources \
  -H "Authorization: Bearer $DEKKA_TOKEN" | jq '.[] | {id, name, environment_id}'
```

## Deploy from GitHub Actions

This workflow runs the tests on every push to `main`, then deploys the commit that passed and waits until it's healthy. Save the token as a repository secret called `DEKKA_TOKEN` and the service ID as a variable called `DEKKA_SERVICE_ID`.

```yaml title=".github/workflows/deploy.yml"
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      API: https://api.dekka.cloud/v1
      DEKKA_TOKEN: ${{ secrets.DEKKA_TOKEN }}
      SERVICE_ID: ${{ vars.DEKKA_SERVICE_ID }}
    steps:
      - name: Deploy this commit
        run: |
          id=$(curl -sS --fail-with-body -X POST "$API/resources/$SERVICE_ID/deployments" \
            -H "Authorization: Bearer $DEKKA_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"commit_sha\": \"$GITHUB_SHA\"}" | jq -r .id)
          echo "Started deployment $id"
          while true; do
            status=$(curl -sS --fail-with-body "$API/deployments/$id" \
              -H "Authorization: Bearer $DEKKA_TOKEN" | jq -r .status)
            case "$status" in
              healthy) echo "Deployment $id is healthy"; exit 0 ;;
              failed|cancelled|removed) echo "Deployment $id is $status"; exit 1 ;;
            esac
            sleep 10
          done
```

`POST /v1/resources/{id}/deployments` takes an optional `branch` and `commit_sha`, and without them deploys the head of the service's branch. It returns the new deployment, whose `status` moves through the [deployment states](/docs/deployments#deployment-states) until it's `healthy` or `failed`. A newer deployment of the same service cancels this one, so the job fails with `cancelled` if someone deploys while it waits.

> [!TIP]
> Turn off **Deploy on push** in the service's settings when CI deploys it, or each push starts a deployment from GitHub as well as the one from your workflow. See [Deploying from GitHub](/docs/deploying-from-github#turn-off-deploys-on-push).

## Revoke a token

Choose **Revoke** next to the token under **Project tokens**. It stops working straight away, and a request with it answers 401 `invalid_api_key`.
