# Service accounts

> Create a client ID and secret for a machine, scope what it can do, decide whose permissions it runs with, and rotate it without downtime.

A service account is how something that is not a person authenticates with your
organization. A webhook sender, a script that reads records, a page embedding a
dashboard — each gets its own credential, its own scopes, and its own audit
trail.

They live at **Settings → Service Accounts**. Creating and editing one, and
viewing its credentials, is limited to organization owners and admins.

## Create one

**New Service Account** asks for three things.

**Name** is for you. Name it after the integration that will hold it — *Stripe
webhook ingest* — so that revoking the right one later is obvious.

**Scopes** decide what the credential is allowed to do. At least one is
required.

| Scope | Grants |
|---|---|
| `webhooks` | Sending data into Hodoflow through webhook requests. |
| `api_read` | Read-only REST access to workflows, runs, and warehouse records. |
| `api_write` | Any JSON API request that is not a read. |
| `embed` | Loading dashboards inside external sites through the embed channel. |

Grant the narrowest set that works. A webhook sender needs `webhooks` and
nothing else.

**Run as** is the one that surprises people. A service account does not have
permissions of its own — it borrows a member's. Pick an organization member and
every request authenticated with this credential executes as them, respecting
their permissions. Only active members of the current organization are offered.

That means deactivating a person can change what a service account can reach.
Point machine credentials at whoever should own the integration long-term.

## The client ID and secret

Creating the account mints a pair: a client ID beginning `cid_` and a client
secret beginning `csec_`. Neither embeds anything about your organization, so
neither is safe to treat as public.

**Client Credentials** on the service account's page shows both, each with a
**Copy** button. Unlike most secrets, these stay retrievable — come back to this
page whenever an integration needs re-configuring. Access is limited to owners
and admins.

## Getting an access token

The pair is not what you send with a request. Exchange it for a short-lived
token first:

```bash
curl -X POST https://app.example.com/api/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "cid_...",
    "client_secret": "csec_..."
  }'
```

You can also send the pair as HTTP Basic auth instead of in the body. The
response is the token, its type, and its lifetime in seconds:

```json
{"access_token": "eyJ...", "token_type": "bearer", "expires_in": 900}
```

Fifteen minutes, so a long-lived integration exchanges again rather than caching
the token forever. Send it as `Authorization: Bearer <access_token>`.

A token carries the scopes and the run-as member that were set when it was
issued, which is why a scope change takes effect on the next exchange rather
than instantly.

Every response for a credential problem is the same `401` with
`{"error": "invalid_client"}` — missing credentials, wrong secret, unknown
client, and a deactivated account all look identical from the outside. That is
deliberate. Check the service account's status in Hodoflow rather than reading
the error.

## Rotating the secret

Rotation is two steps, so there is no window where nothing works.

**Stage new secret** generates a second client ID and secret alongside the
active pair. Both pairs authenticate at once. Copy the staged values from
**Client Credentials**, where they appear under **Staged credentials** with a
**Pending promotion** badge, and update your integrations.

**Promote staged** then makes the staged pair the active one, and the previous
pair stops working immediately. Do it once your integrations are switched over
and you have confirmed they still work.

**Restage new secret** replaces a staged pair you have not promoted yet. The
pair you overwrite stops working immediately, so only restage if nothing has
picked the staged values up.

## Turning one off

**Deactivate** stops the credential from exchanging for tokens at all. It is
reversible with **Activate**, which makes it the right tool for a suspected leak
you are still investigating.

Deactivation is refused while an active workflow depends on the credential —
deactivate the workflow first. **Usage** on the service account's page lists the
workflow versions whose triggers authenticate with it, so you know which ones.

**Delete** is permanent, and any external system using the credential loses
access the moment you confirm. It is refused while workflow triggers still
reference it.

## Related

- [Triggers and webhooks](/help/workflows/triggers-and-webhooks) — a webhook trigger
  requires a service account holding the `webhooks` scope.
- [Embeds](/help/data-dashboards/embeds) — dashboard embeds use a share token, not a
  service account.
- The developer portal covers the exchange and the token's claims in detail at
  [authentication](/developer/authentication).
