# Update an API key

> PATCH /v1/api-keys/{id}. Change a key's label, scopes, allowlist, expiry or, for an agent key, its tools, connections, budget and cap.

Source: https://emailmovers.com/mydatahub/docs/reference/rest/update-api-key

`PATCH /v1/api-keys/{id}`

Change a key's label, scopes, allowlist, expiry or, for an agent key, its tools, connections, budget and cap. Only a signed-in person, through OAuth, may: no key changes its own limits, and an agent key never takes admin.

| Property | Value |
| --- | --- |
| Endpoint | `PATCH https://mydatahub.emailmovers.com/v1/api-keys/{id}` |
| Operation id | `updateApiKey` |
| Cost | Free (0 credits) |
| Access | A signed-in person only: an OAuth 2.1 token with the scope `admin`. API keys are refused. |
| Tag | Keys |

> **Not yet live.** MyDataHub v2 is built and tested locally and is not yet live. The v2 hosts open at launch, date to confirm. This page describes the contract the API is tested against.

## Request

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Format: an API key id: key_ followed by a 26 character ULID in upper case Crockford base32, such as key_01JB3Z4Q2V7XN3M9R6T1Y5W8ZA. |
| `MyDataHub-Version` | header | string | no | The dated API version, pinned per key when left out. Non-breaking changes ship as new dates; the path major changes only for breaking changes, with a 12 month deprecation window. |

### Body

| Member | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `scopes` | array of enum: `account:read`, `counts:read`, `previews:read`, `audiences:read`, `audiences:write`, `quotes:read`, `orders:read`, `orders:write`, and 8 more | no | Replacement scopes. An agent key never takes admin, and no key takes account:write, which only a signed-in owner uses. |
| `ip_allowlist` | array of string | no | CIDR ranges the key may be used from; empty allows any address. |
| `expires_at` | string (date-time) or null | no |  |
| `allowed_tools` | array of enum: `get_plans`, `get_account`, `list_filters`, `search_filter_values`, `expand_job_titles`, `resolve_location`, `compile_audience`, `count_audience`, and 14 more | no |  |
| `allowed_connection_ids` | array of string | no |  |
| `monthly_budget_credits` | integer (0 or more) | no |  |
| `per_order_max_credits` | integer (0 or more) | no |  |
| `webhook_url` | string (uri) or null | no |  |
| `disclosure` | enum: `account`, `anonymous` | no |  |
| `licence_accepted_version` | string | no | The Data Licence and Sharing Agreement version the person changing the key accepts: required when the change lets an agent key spend on its own (create_order or enrich_contact, a budget and a cap above 0), or widens what it spends (a spending tool added, a larger budget or cap), and the key has not accepted the version in force. Sent alone, it renews the key's acceptance. |

## Example request

```bash
curl -X PATCH "https://mydatahub.emailmovers.com/v1/api-keys/key_01JB3Z9R6T1Y5W8ZAK8Q2V7XN3" \
  -H "Authorization: Bearer $MYDATAHUB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "monthly_budget_credits": 2000
}'
```

## Responses

### 200: The updated key.

Headers: `RateLimit`, `RateLimit-Policy`.

#### StandardApiKey

| Member | Type | Always present | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Format: an API key id: key_ followed by a 26 character ULID in upper case Crockford base32, such as key_01JB3Z4Q2V7XN3M9R6T1Y5W8ZA. |
| `label` | string | yes |  |
| `mode` | enum: `live`, `test` | yes | live keys (mdh_live_) spend real credits; test keys (mdh_test_) return synthetic data and never charge. |
| `prefix` | string | yes | The first characters of the secret, such as mdh_live_7Kq2. |
| `scopes` | array of enum: `account:read`, `counts:read`, `previews:read`, `audiences:read`, `audiences:write`, `quotes:read`, `orders:read`, `orders:write`, and 8 more | yes |  |
| `ip_allowlist` | array of string | yes | CIDR ranges the key may be used from; empty allows any address. |
| `created_at` | string (date-time) | yes |  |
| `created_by` | string | yes | Format: a user id: usr_ followed by a 26 character ULID in upper case Crockford base32, such as usr_01JA0Q3V7XN3M9R6T1Y5W8ZAK8. |
| `last_used_at` | string (date-time) or null | yes |  |
| `expires_at` | string (date-time) or null | yes |  |
| `revoked_at` | string (date-time) or null | yes |  |
| `kind` | `standard` | yes |  |

#### AgentKey

| Member | Type | Always present | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Format: an API key id: key_ followed by a 26 character ULID in upper case Crockford base32, such as key_01JB3Z4Q2V7XN3M9R6T1Y5W8ZA. |
| `label` | string | yes |  |
| `mode` | enum: `live`, `test` | yes | live keys (mdh_live_) spend real credits; test keys (mdh_test_) return synthetic data and never charge. |
| `prefix` | string | yes | The first characters of the secret, such as mdh_live_7Kq2. |
| `scopes` | array of enum: `account:read`, `counts:read`, `previews:read`, `audiences:read`, `audiences:write`, `quotes:read`, `orders:read`, `orders:write`, and 8 more | yes |  |
| `ip_allowlist` | array of string | yes | CIDR ranges the key may be used from; empty allows any address. |
| `created_at` | string (date-time) | yes |  |
| `created_by` | string | yes | Format: a user id: usr_ followed by a 26 character ULID in upper case Crockford base32, such as usr_01JA0Q3V7XN3M9R6T1Y5W8ZAK8. |
| `last_used_at` | string (date-time) or null | yes |  |
| `expires_at` | string (date-time) or null | yes |  |
| `revoked_at` | string (date-time) or null | yes |  |
| `kind` | `agent` | yes |  |
| `allowed_tools` | array of enum: `get_plans`, `get_account`, `list_filters`, `search_filter_values`, `expand_job_titles`, `resolve_location`, `compile_audience`, `count_audience`, and 14 more | yes | The tools the key may call; a spending tool (create_order, enrich_contact) only if listed. |
| `allowed_connection_ids` | array of string | yes | The CRM connections push_to_crm may push to live; empty allows dry runs only. |
| `monthly_budget_credits` | integer (0 or more) | yes |  |
| `per_order_max_credits` | integer (0 or more) | yes |  |
| `budget` | `Budget` | yes | The live spend meter. |
| `budget.monthly_credits` | integer (0 or more) | yes |  |
| `budget.spent` | number (0 or more) | yes | Credits, to one decimal place: whole at 1 credit a record, fractional only where a rule is (0.1 company-only, 1.8 outright, both proposed). |
| `budget.remaining` | number (0 or more) | yes | Credits, to one decimal place: whole at 1 credit a record, fractional only where a rule is (0.1 company-only, 1.8 outright, both proposed). |
| `budget.resets_at` | string (date-time) | yes |  |
| `webhook_url` | string (uri) or null | yes |  |
| `disclosure` | enum: `account`, `anonymous` | yes | account: counts as the organisation's own; anonymous: every anonymous visitor's rule, for a public assistant. |

#### Example response

```json
{
  "id": "key_01JB3Z9R6T1Y5W8ZAK8Q2V7XN3",
  "kind": "agent",
  "label": "SDR agent",
  "mode": "live",
  "prefix": "mdh_live_7Kq2",
  "scopes": [
    "counts:read",
    "quotes:read",
    "orders:write",
    "orders:read",
    "records:read",
    "pushes:write"
  ],
  "ip_allowlist": [],
  "created_at": "2026-09-20T09:00:00Z",
  "created_by": "usr_01JA0Q3V7XN3M9R6T1Y5W8ZAK8",
  "last_used_at": "2026-09-26T02:00:00Z",
  "expires_at": "2027-03-31T00:00:00Z",
  "revoked_at": null,
  "allowed_tools": [
    "count_audience",
    "quote_order",
    "create_order",
    "fetch_order_records",
    "push_to_crm"
  ],
  "allowed_connection_ids": ["conn_01JB3Y2X6Y7Z8A1B2C3D4E5F6G"],
  "monthly_budget_credits": 2000,
  "per_order_max_credits": 500,
  "budget": {
    "monthly_credits": 2000,
    "spent": 400,
    "remaining": 1600,
    "resets_at": "2026-10-01T00:00:00Z"
  },
  "webhook_url": "https://agent.example.com/mydatahub/events",
  "disclosure": "account"
}
```

## Errors

Every error is a problem object with a `code` and a `next_step`. Every response carries the `RateLimit` and `RateLimit-Policy` headers; see [Rate limits](https://emailmovers.com/mydatahub/docs/rate-limits.md).

| Status | Problem codes | When |
| --- | --- | --- |
| 400 | [`invalid-request`](https://emailmovers.com/mydatahub/docs/errors/invalid-request.md), [`idempotency-key-required`](https://emailmovers.com/mydatahub/docs/errors/idempotency-key-required.md), [`unsupported-version`](https://emailmovers.com/mydatahub/docs/errors/unsupported-version.md) | The request could not be read, or an Idempotency-Key is missing. |
| 401 | [`authentication-required`](https://emailmovers.com/mydatahub/docs/errors/authentication-required.md), [`account-required`](https://emailmovers.com/mydatahub/docs/errors/account-required.md) | Sign in (OAuth) or send an API key (authentication-required), or create a free account for what the anonymous tier does not cover (account-required). |
| 403 | [`insufficient-scope`](https://emailmovers.com/mydatahub/docs/errors/insufficient-scope.md), [`confirmation-required`](https://emailmovers.com/mydatahub/docs/errors/confirmation-required.md), [`business-details-required`](https://emailmovers.com/mydatahub/docs/errors/business-details-required.md), [`licence-acceptance-required`](https://emailmovers.com/mydatahub/docs/errors/licence-acceptance-required.md), [`dpa-acceptance-required`](https://emailmovers.com/mydatahub/docs/errors/dpa-acceptance-required.md), [`agent-budget-exceeded`](https://emailmovers.com/mydatahub/docs/errors/agent-budget-exceeded.md) | The token lacks a scope (insufficient-scope, with required_scope and a WWW-Authenticate step-up challenge); no person confirmed the spend and no agent key covers it (confirmation-required, with an approval_url); an agent key's per-order cap or monthly budget does not cover it (agent-budget-exceeded); an account made by self-serve sign-up has not yet confirmed its business name and that it is a UK business, which its first order needs (business-details-required); the order needs the Data Licence and Sharing Agreement accepted at the version in force (licence-acceptance-required); or a suppression upload comes before the organisation has accepted the Article 28 data processing agreement in force (dpa-acceptance-required). Nothing is charged. |
| 404 | [`not-found`](https://emailmovers.com/mydatahub/docs/errors/not-found.md) | No such object for this organisation. |
| 422 | [`validation-failed`](https://emailmovers.com/mydatahub/docs/errors/validation-failed.md), [`quote-required`](https://emailmovers.com/mydatahub/docs/errors/quote-required.md), [`dataset-unavailable`](https://emailmovers.com/mydatahub/docs/errors/dataset-unavailable.md) | The request is not valid. errors lists every problem with a JSON pointer and did_you_mean. |
| 429 | [`rate-limited`](https://emailmovers.com/mydatahub/docs/errors/rate-limited.md) | Too many requests. Wait for Retry-After. |
| Any other failure | [`internal-error`](https://emailmovers.com/mydatahub/docs/errors/internal-error.md), [`service-unavailable`](https://emailmovers.com/mydatahub/docs/errors/service-unavailable.md), [`tps-screen-expired`](https://emailmovers.com/mydatahub/docs/errors/tps-screen-expired.md) | Something failed on our side (500 internal-error) or the service is briefly unavailable (503 service-unavailable, with Retry-After). Nothing was charged; retry with the same Idempotency-Key. A count, sample, quote or order with telephone in play is also refused with 503 tps-screen-expired, with no Retry-After, while the dataset version's TPS and CTPS screen is more than 28 days old. |
