# Call the Cloud Platform API

**Goal:** make one authenticated call to the Cloud Platform API: the REST API behind everything [`acli`](/start-here/glossary/#acli) and the Cloud Platform user interface can do.

Reach for it when a task has to run without a person at the terminal (a nightly database refresh, environment creation in a pipeline) or when no `acli` command covers what you need. For everything else, `acli` wraps this API and handles the token exchange for you: start at the [CLI quickstart](/cloud-platform/cli/quickstart/).

## What you'll have when you're done

- An [API token](/start-here/glossary/#api-token) (API Key + API Secret) for your Acquia account.
- A working OAuth 2.0 access token and one successful `curl` call listing your applications.
- The two failure modes you'll actually hit (expiry and rate limiting) and their fixes.

## Prerequisites

- An Acquia account that can sign in to [cloud.acquia.com](https://cloud.acquia.com)
- [`curl`](https://curl.se/) (preinstalled on macOS and Linux)
- Optional: [`jq`](https://jqlang.org/download/), the standard JSON filter. Step 4 uses it to pull the token out of the exchange response, and states how to manage without it.

## Steps

<Steps>

1. ### Generate an API token

   Open [cloud.acquia.com/a/profile/tokens](https://cloud.acquia.com/a/profile/tokens), give the token a human-readable label (for example `local-automation`), and click `Create Token`.

   Record the `API Key` and `API Secret` now: you can't retrieve them after closing the browser tab.

2. ### Put the credentials in a `.env` file

   ```bash
   # .env
   ACQUIA_CLOUD_API_KEY=d2b18a6c-0000-0000-0000-000000000000
   ACQUIA_CLOUD_API_SECRET=your-api-secret
   ```

   These are deliberately **not** named `DRUPAL_CLIENT_ID` / `DRUPAL_CLIENT_SECRET`. That pair names a per-[site](/start-here/glossary/#site) [API client](/start-here/glossary/#api-client) for the Content API; this key/secret pair is an account-level Cloud Platform credential that inherits your user's permissions across the whole [subscription](/start-here/glossary/#subscription). Distinct names keep the two from ever being swapped in a script.

3. ### Exchange the credentials for an access token

   The Cloud Platform API uses the OAuth 2.0 `client_credentials` grant against Acquia's central accounts service. Use `--data-urlencode` (not `--data`) so non-alphanumeric characters in the secret survive encoding:

   ```bash
   set -a; source .env; set +a

   curl -s https://accounts.acquia.com/api/auth/oauth/token \
     --data-urlencode "client_id=$ACQUIA_CLOUD_API_KEY" \
     --data-urlencode "client_secret=$ACQUIA_CLOUD_API_SECRET" \
     --data-urlencode "grant_type=client_credentials"
   ```

   A successful response looks like this:

   ```json
   {
     "token_type": "Bearer",
     "expires_in": 300,
     "access_token":
       "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9…(redacted)",
     "scope": "role:authenticated-user"
   }
   ```

   Note `expires_in: 300`: the token lives **300 seconds (5 minutes)**. Scripts regenerate it rather than store it.

4. ### Make one authenticated call

   Capture the token and list the applications your account can see:

   ```bash
   TOKEN=$(curl -s \
     https://accounts.acquia.com/api/auth/oauth/token \
     --data-urlencode "client_id=$ACQUIA_CLOUD_API_KEY" \
     --data-urlencode "client_secret=$ACQUIA_CLOUD_API_SECRET" \
     --data-urlencode "grant_type=client_credentials" \
     | jq -r .access_token)

   curl -s https://cloud.acquia.com/api/applications \
     -H "Authorization: Bearer $TOKEN"
   ```

   Without `jq`, run the token request from step 3 on its own and paste the `access_token` value into `TOKEN=` yourself.

   A `200` response returns JSON listing your applications (trimmed):

   ```json
   {
     "total": 1,
     "_embedded": {
       "items": [
         {
           "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
           "name": "myproject",
           "subscription": {
             "uuid": "…(redacted)",
             "name": "MyProject"
           }
         }
       ]
     },
     "_links": {
       "self": {
         "href": "https://cloud.acquia.com/api/applications"
       }
     }
   }
   ```

   That application `uuid` is the handle every other Cloud Platform API endpoint takes: [environments](/start-here/glossary/#environment), deployments, backups, logs, cron, and more.

</Steps>

## What just happened

Acquia's accounts service exchanged your account-level API Key and Secret for an access token via the OAuth 2.0 `client_credentials` grant, and the Cloud Platform API at `cloud.acquia.com/api` accepted that token in an `Authorization: Bearer` header. The token expires after 300 seconds, so real automation wraps the exchange in a helper and re-runs it per batch of calls. The API is rate limited to **100 calls per minute** per user, enough for CI tasks, but worth knowing before you write a polling loop.

## When something goes wrong

**`{ "error": "invalid_client", "error_description": "The client secret supplied for a confidential client is invalid." }`**: either the key/secret is wrong, or the token request wasn't form-encoded. The token endpoint only accepts `Content-Type: application/x-www-form-urlencoded` (which `--data-urlencode` sets); re-check the credentials from [cloud.acquia.com/a/profile/tokens](https://cloud.acquia.com/a/profile/tokens) and keep the request shape from step 3. If the secret is lost, create a new token; secrets are shown once.

**`{ "errorCode": "E0000021", "errorSummary": "Bad request. Accept and/or Content-Type headers likely do not match supported values." }`**: you sent the token request as JSON. The OAuth 2.0 framework doesn't support `application/json` here; send the fields form-encoded as in step 3.

**`403 Forbidden` with `{ "error": "unauthorized", "message": "The access token has expired." }` on the API call itself**: the access token expired; it only lives 300 seconds. Re-run the token exchange and retry. (A malformed or revoked token returns the same `403` with `"The access token provided is invalid."` instead.)

**`HTTP/1.1 429 Too Many Requests`**: you exceeded the 100-calls-per-minute rate limit. The response carries a `Retry-After` header with the number of seconds until calls are unblocked:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 57
```

The fix: back off, wait the `Retry-After` seconds, then retry, and add that backoff to any loop that made this happen.

## Next steps

- [Cloud Platform API reference (cloudapi-docs.acquia.com)](https://cloudapi-docs.acquia.com/): the canonical OpenAPI docs for all 200+ endpoints.
- [Acquia APIs directory](/start-here/acquia-apis/): every other Acquia API, and where each is documented.
- [Everyday CLI workflows](/cloud-platform/cli/everyday-workflows/): the `acli` commands that wrap these endpoints, for the tasks that don't need raw `curl`.
