# Quote an order

> POST /v1/quotes. A signed machine quote for a count, field set, quantity and licence: records after overlap and suppression, credits per record, credits, the GBP price, the balance after, the licence and warranty, and blockers.

Source: https://emailmovers.com/mydatahub/docs/reference/rest/create-quote

`POST /v1/quotes`

A signed machine quote for a count, field set, quantity and licence: records after overlap and suppression, credits per record, credits, the GBP price, the balance after, the licence and warranty, and blockers. Valid 24 hours and bound to a dataset version and the caller's organisation. Free, and needs no account. This is not a human sales quote, which is a 14 day PDF made in the app.

| Property | Value |
| --- | --- |
| Endpoint | `POST https://mydatahub.emailmovers.com/v1/quotes` |
| Operation id | `createQuote` |
| Cost | Free (0 credits) |
| Access | No account needed. An API key or OAuth token with the scope `quotes:read` is optional and adds your organisation's own figures. |
| Idempotency-Key | Optional. Makes a retry safe. |
| MCP tool | [`quote_order`](https://emailmovers.com/mydatahub/docs/reference/mcp/quote_order.md) |
| Tag | Quotes |

> **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 |
| --- | --- | --- | --- | --- |
| `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. |
| `Idempotency-Key` | header | string | no | Makes the request safe to retry: the same key returns the stored response. Keys are kept for the life of the organisation. |

### Body

| Member | Type | Required | Description |
| --- | --- | --- | --- |
| `count_id` | string | yes | A count id from POST /v1/counts or count_audience. Either case is accepted; responses use upper case. |
| `field_set` | string | yes | A field set id from GET /v1/field-sets, such as standard. |
| `limit` | integer (1 to 1000000) | no | Records to buy; left out, the whole count. |
| `dedupe_against` | enum: `none`, `previous_orders`, `crm`, `previous_orders_and_crm` | no | Remove records you already hold: from earlier orders, from a connected CRM, or both. Default previous_orders when signed in. crm and previous_orders_and_crm need connection_id. Default `previous_orders`. |
| `connection_id` | string | no | The CRM connection to check, only with dedupe_against crm or previous_orders_and_crm. |
| `licence` | enum: `multi_use_12m`, `outright` | no | multi_use_12m (12 month multi-use, every plan) or outright (1.8 credits a record, proposed). Default `multi_use_12m`. |

## Example request

```bash
curl -X POST "https://mydatahub.emailmovers.com/v1/quotes" \
  -H "Content-Type: application/json" \
  --data '{
  "count_id": "cnt_7K2PQ4MAXN3R6TBY5WDZ",
  "field_set": "standard",
  "limit": 5000,
  "dedupe_against": "previous_orders",
  "licence": "multi_use_12m"
}'
```

## Responses

### 201: The machine quote.

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

| Member | Type | Always present | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Format: a quote id: qte_ followed by a 26 character ULID in upper case Crockford base32, such as qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3. |
| `kind` | `machine` | yes |  |
| `organisation_id` | string or null | yes | The organisation the quote belongs to; create_order settles it only there. null for a quote made without an account, which the first organisation to confirm it takes. |
| `count_id` | string | yes | The count the quote was priced from. |
| `dataset` | enum: `uk_contacts`, `ie_contacts`, `global_contacts` | no | The dataset an audience draws from. uk_contacts is live; ie_contacts and global_contacts are listed but not yet served. |
| `dataset_version` | string | yes | The immutable weekly build a count is bound to, such as 2026-W39. The last 52 stay queryable. |
| `field_set` | string | no |  |
| `records` | integer (0 or more) | yes | Records after overlap and suppression; what you would receive. |
| `removed` | object | yes | Records left out and never charged. |
| `removed.already_held` | integer (0 or more) | yes |  |
| `removed.suppressed` | integer (0 or more) | yes |  |
| `credits_per_record` | number (0 or more) | yes | 1 for a complete UK record; 1.8 outright (proposed). |
| `credits_required` | number (0 or more) | yes | records times credits_per_record (creditsFor). |
| `credits_available` | number (0 or more) or null | yes | Your balance; null when signed out. |
| `balance_after` | number or null | yes | credits_available less credits_required; negative means a top-up is needed; null when signed out. |
| `gbp_ex_vat` | number (0 or more) | yes | What credits_required cost bought the cheapest way, at the per-contact scale or with credit packs, excluding VAT. The same for every caller; purchase says what this caller needs to buy. |
| `purchase` | `Purchase` | yes | How to buy the credits the order needs, worked out by one function (cheapestPurchase) everywhere a price is shown. The caller's own, like the balance: read fresh on every read of the quote. |
| `purchase.credits_to_buy` | integer (0 or more) | yes | credits_required less the balance: all of it when signed out, 0 when the balance covers the order. |
| `purchase.cheapest` | string or null | yes | The id of the cheapest option; null when nothing needs buying. |
| `purchase.gbp_ex_vat` | number (0 or more) | yes | What the cheapest option costs, excluding VAT; 0 when nothing needs buying. |
| `purchase.options` | array of `PurchaseOption` | yes | Cheapest first: the per-contact scale, and the credit pack (with any rest at the scale) when it is on sale. At the same price, the option that buys more credits comes first. |
| `licence` | `Licence` | yes | Licence terms as data, on every quote, order, records page and receipt. Records stay usable, pushable and re-downloadable free until reuse_until. |
| `licence.type` | enum: `multi_use_12m`, `outright` | yes | multi_use_12m (12 month multi-use, every plan) or outright (1.8 credits a record, proposed). |
| `licence.reuse_until` | string (date) or null | yes | Order date plus 12 months; null for outright. |
| `licence.resale_prohibited` | boolean | yes |  |
| `licence.permitted_channels` | array of enum: `email`, `telephone`, `post` | yes | email, telephone and post on standard terms. |
| `licence.warranty_url` | string (uri) | yes |  |
| `licence.terms_version` | string | yes | The version of the Data Licence and Sharing Agreement the licence is under, such as 1.0. |
| `licence.terms_url` | string (uri) | no | Where that version of the Data Licence and Sharing Agreement is published. |
| `warranty` | `Warranty` | yes | The bounce warranty that covers delivered records, with the threshold it was issued under. |
| `warranty.policy` | enum: `replace_over_threshold_hard_bounce`, `replace_any_hard_bounce_30d` | yes | replace_over_threshold_hard_bounce is the current policy: records are replaced when hard bounces exceed threshold_percent of an order. replace_any_hard_bounce_30d is proposed and not yet in force. The current policy's id replaced one that named the threshold, a change to this public enum made before v2 went live. |
| `warranty.threshold_percent` | number (0 to 100) | no | replace_over_threshold_hard_bounce only, and required there: the share of an order, in percent, that hard bounces must exceed before records are replaced. It is the threshold the document was issued under. |
| `warranty.url` | string (uri) | yes |  |
| `created_at` | string (date-time) | no |  |
| `expires_at` | string (date-time) | yes | 24 hours after creation. |
| `blockers` | array of `QuoteBlocker` | yes |  |
| `blockers[].code` | enum: `insufficient_credits`, `account_required`, `agent_budget_exceeded`, `dataset_unavailable`, `count_expired` | yes |  |
| `blockers[].detail` | string | yes |  |
| `blockers[].top_up_url` | string (uri) | no |  |
| `signature` | `QuoteSignature` | yes | The signature over the machine quote's price, so an agent can hand the quote to another agent and both can check it. Verify it with the key at jwks_url. |
| `signature.alg` | `ES256` | yes |  |
| `signature.kid` | string | yes | The signing key id in jwks_url. |
| `signature.jwks_url` | string (uri) | yes |  |
| `signature.jws` | string | yes | A compact JWS whose payload is the quote's id, organisation_id, count_id, dataset_version, records, credits_required, gbp_ex_vat, licence, expires_at. |

#### Example response

```json
{
  "id": "qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3",
  "kind": "machine",
  "dataset_version": "2026-W39",
  "records": 5000,
  "removed": {
    "already_held": 312,
    "suppressed": 41
  },
  "credits_required": 5000,
  "credits_available": 6200,
  "gbp_ex_vat": 1249,
  "licence": {
    "type": "multi_use_12m",
    "reuse_until": "2027-09-26",
    "resale_prohibited": true,
    "permitted_channels": ["email", "telephone", "post"],
    "warranty_url": "https://emailmovers.com/trust/warranty",
    "terms_version": "2026-09"
  },
  "warranty": {
    "policy": "replace_over_threshold_hard_bounce",
    "threshold_percent": 10,
    "url": "https://emailmovers.com/trust/warranty"
  },
  "expires_at": "2026-09-27T10:14:00Z",
  "blockers": [],
  "organisation_id": "org_01JA0Q2V7XN3M9R6T1Y5W8ZAK8",
  "count_id": "cnt_7K2PQ4MAXN3R6TBY5WDZ",
  "credits_per_record": 1,
  "balance_after": 1200,
  "purchase": {
    "credits_to_buy": 0,
    "cheapest": null,
    "gbp_ex_vat": 0,
    "options": []
  },
  "signature": {
    "alg": "ES256",
    "kid": "qte-2026-09",
    "jwks_url": "https://mydatahub.emailmovers.com/.well-known/jwks.json",
    "jws": "eyJhbGciOiJFUzI1NiIsImtpZCI6InF0ZS0yMDI2LTA5In0.eyJpZCI6InF0ZV8wMUpCNE0yUjZUMVk1VzhaQUs4UTJWN1hOMyJ9.MEUCIQDexampleSignatureOnly"
  }
}
```

## 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). |
| 409 | [`idempotency-key-reused`](https://emailmovers.com/mydatahub/docs/errors/idempotency-key-reused.md), [`quote-expired`](https://emailmovers.com/mydatahub/docs/errors/quote-expired.md), [`order-not-ready`](https://emailmovers.com/mydatahub/docs/errors/order-not-ready.md), [`order-not-cancellable`](https://emailmovers.com/mydatahub/docs/errors/order-not-cancellable.md) | An Idempotency-Key reused with different parameters, an expired machine quote, or an object in the wrong state. |
| 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. |
