EmailmoversMyDataHub docs
Browse the docs

Guide

Spending safely

Three layers guard every spend: a machine quote first, a person's confirmation or a pre-authorised agent key, and hard limits. A flag set by a model is never consent.

Updated 4 October 2026View as Markdown

No order is placed without a live quote and a person's say-so, and an agent can spend only inside limits a human set. Two actions spend credits: create_order (POST /v1/orders) and enrich_contact (POST /v1/contacts/enrich).

A live CRM push needs the same approval, though it costs nothing. Everything else is free, so an agent can explore as much as it likes.

The three layers

  1. Quote first. An order without a live quote_id fails with "Call quote_order first; a quote is valid for 24 hours" and nothing is charged. It is idempotent on the quote plus the Idempotency-Key, so a retry never double-charges.
  2. Human proof. A person confirms the spend, through a form, through an approval page, or because they set up an agent key.
  3. Hard limits. A pre-authorised agent key skips the human step only while its per-order cap covers the quote and monthly budget remains.

Money never moves inside a Claude connector or a ChatGPT app. Credits are deducted in the tool. Top-ups are planned to happen on mydatahub.emailmovers.com through Stripe Checkout, which is not built yet.

A flag set by a model is never consent. There is no confirmed, confirm or consent member anywhere.

The MCP input schemas refuse unknown members, and the REST order body is strict, so a model cannot set one. A standard API key or an OAuth token alone never spends either. What a person decides never travels in the arguments a model writes.

How an order is confirmed

The order records the method in confirmed_by.method.

MethodWho confirmsHow
appA person ordering in the app itselfTheir signed-in session
approval_urlA person on the approval pageA confirmation_token you send with the order
elicitation_formA person behind an MCP hostA form the host shows
agent_keyA person who set the key's limits earlierThe order runs inside the key's cap and budget

A third-party OAuth client or a standard API key alone never counts as app, so there is no API key method.

The approval page and the confirmation token

With no confirmation, POST /v1/orders answers 403 confirmation-required and nothing is charged. The problem carries the quote_id, an expires_at and an approval_url such as https://mydatahub.emailmovers.com/quotes/{id}/approve.

{
  "type": "https://emailmovers.com/mydatahub/docs/errors/confirmation-required",
  "title": "A person must confirm this spend",
  "status": 403,
  "code": "confirmation-required",
  "action": "order",
  "quote_id": "qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3",
  "approval_url": "https://mydatahub.emailmovers.com/quotes/qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3/approve"
}

Give the link to the person. They sign in and approve, and the page returns a confirmation_token.

Send it as confirmation_token in the order body. The token is bound to that person's sub and to this spend, and it is redeemable once.

It lasts 15 minutes at most, and never longer than the quote, so retry the order promptly after approval because an expired token is refused. The approval page can also place the order itself when the person approves.

The MCP form

On a host with elicitation, create_order returns resultType input_required with a form of one boolean named approve, off by default. Its label states the spend:

Spend 5,000 credits (£1,249.00 ex VAT bought the cheapest way) for 5,000 contacts, 12 month multi-use licence

The requestState is sealed with AEAD, following the multi round trip pattern. It holds the principal, the quote id, a digest of the arguments and a 15 minute expiry, and it is redeemable once. The host asks the person and retries the same call with the answer and the state.

  • Accepted. The server mints the same confirmation token the approval page would, runs the order, and confirmed_by.method reads elicitation_form.
  • Declined, cancelled or left unticked. Nothing is placed and nothing is charged. The result is confirmation-required with the quote kept, so the agent does not ask again.
  • Hosts without forms. The server uses URL-mode elicitation to the approval page, which returns the token.
  • Nobody to ask. The tool returns the confirmation-required problem with the approval_url.

Agent keys and budgets

An agent key is an API key with kind agent, made by a signed-in person with a label, allowed_tools, monthly_budget_credits, per_order_max_credits, an expiry and an optional webhook_url. It never holds the admin scope.

CheckOutcome
The tool is not in allowed_tools403 insufficient-scope, except for the three spends, which ask a person instead of shutting the key out
The quote exceeds per_order_max_credits or the remaining monthly budget403 agent-budget-exceeded
The balance is short402 insufficient-credits
All limits cover itThe order is placed, with confirmed_by.method agent_key

Limits are checked before the balance, so an agent over its cap sees the 403 even when the balance is also short. Two orders cannot both fit the same budget, because the check runs inside the ledger transaction. A refund counts in the month of the charge it reverses, so a late refund cannot buy back a later month's budget.

When a key spends on the authority of a person who is later removed or disabled, its next order, enrichment or live push answers confirmation-required and needs a person again.

The agent-budget-exceeded problem carries the budget, the quote, an approval_url for the key owner and a top_up_url.

The owner is told only when the key has a webhook_url and an endpoint is registered at that exact URL, so the notice can be signed. See the webhook_url row in Standard keys and agent keys.

{
  "budget": { "monthly_credits": 1000, "spent": 200, "remaining": 800, "resets_at": "2026-10-01T00:00:00Z" },
  "per_order_max_credits": 500,
  "quote_id": "qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3",
  "credits_required": 5000
}

When the balance is short

POST /v1/orders answers 402 insufficient-credits with the quote and a top_up_url on mydatahub.emailmovers.com, where a Stripe Checkout page is planned and not built yet. Once a top-up is possible, retry with the same quote_id.

Stripe Shared Payment Tokens for agent wallets are planned and not built. Machine Payments Protocol on the order endpoint is planned for phase 2.

Retries and idempotency

Idempotency-Key is required on POST /v1/orders and POST /v1/contacts/enrich, and accepted on every other creating POST. Over MCP, create_order takes it as the idempotency_key argument.

  • The same key with the same request returns the stored response, with Idempotent-Replayed: true. Keys are kept for the life of the organisation.
  • The same key with a different request is 409 idempotency-key-reused.
  • A refusal (402, 403 or 422) is not stored, so the same key works after you fix the cause.
  • The ledger has its own key built from the thing being paid for, so a different Idempotency-Key still cannot charge one quote twice.

No human, no key

When no person is present and no key covers the spend, no order is created. The agent keeps the quote_id, a human approves it later in the app, and order.ready fires when the records are built.

A colleague's agent in the same organisation can read the quote with GET /v1/quotes/{id} and order it under its own key. Outside the organisation the audience is quoted again on the payer's plan, and the original quote is never charged.

Next steps