# SMS API purchases: idempotency keys, price ceilings and timeout recovery

Persist purchase intent, approve a USD price ceiling and reconcile SMS.Red timeouts with one idempotency key instead of buying twice.

Author: SMS.Red
Published: 2026-10-06
Updated: 2026-10-06
Canonical: https://sms.red/en/blog/sms-api-idempotency-and-price-limits

For a reliable SMS.Red purchase integration, save one idempotency key and the exact request body before sending the purchase. Include an approved maximum total USD price, then reconcile uncertain responses using that same key. A network timeout does not prove that the supplier call or order allocation failed.

The most costly retry mistake is generating a new key after a slow response. A new key represents a new intended purchase. If the first request succeeded, the second can buy another number rather than recover the first result.

## Quote the exact product before buying

Use the account's API key on its matching store origin. Browse services for the desired product type and country, then request the selected service's duration quotes. Choose a returned term with positive availability and inspect its total USD price. A service-list minimum price is a summary, not the exact quote for every term.

The [SMS.Red quickstart](https://sms.red/docs/ai/quickstart.md) shows the public developer endpoints. Use returned service IDs rather than inventing IDs from names. Keep prices as decimal strings and durations as milliseconds throughout your client contract.

## Persist the purchase intent

Create an application record containing the intended service, country, duration, approved maxPrice, idempotency key and current attempt state. Save it before dispatch. A crash between sending and saving should not leave your application unable to identify the operation it started.

The documented key format is 8–128 letters, digits, underscores or hyphens. An illustrative key such as demo_purchase_0001 is a shape example, not a key to reuse across real customers. Generate a distinct persisted key for each genuinely new purchase intent.

## Set a real price ceiling

The required maxPrice is the maximum total customer charge in USD, represented with no more than two decimal places. A synthetic example could approve "1.50" for a specific quoted verification term. It should reflect what the customer or budget actually permits, not an exaggerated value chosen to make all purchases succeed.

If the quote exceeds that ceiling, ask the application's owner to approve a new intent or choose a different supported term. Do not silently raise the ceiling during retry. Reusing a key with a changed request is also a conflict, not an update to the original purchase.

## Reconcile a lost response

1. Mark the result uncertain instead of recording a confirmed failure.
2. Read the purchase attempt using the persisted key at GET /api/v1/developer/purchase-attempts/{key}.
3. If the attempt provides an activation ID, read that verification or rental and retain its assigned number and expiry.
4. If the result is pending or ambiguous, keep reconciling within a bounded policy; do not create a replacement key.
5. If a retry is appropriate, use the same key and identical body.

A 404 immediately after a timeout is not proof that a new key is safe: the first request may still be in flight. The [error-and-recovery contract](https://sms.red/docs/ai/errors-and-retries.md) explains this edge case. Treat mutation retries differently from ordinary read retries.

## A concrete failure scenario

Your worker sends a purchase, the supplier allocates a number, and the connection closes before the worker sees the response. If the job restarts with its persisted key, it can inspect the attempt and recover the allocated order. If the job generates a fresh key on restart, it can initiate a second purchase while the first number remains allocated.

This scenario does not require a malicious user or a broken supplier. A slow network is enough. Design the database record and job lifecycle so the recovery path is the normal response to uncertainty.

## After the order is known

Use the returned order ID to read messages and the actual expiresAt to schedule the receiving deadline. Keep the purchase key linked to the order for support and reconciliation. Do not expose API keys, complete message bodies or real codes in ordinary job logs.

For event delivery, see [webhooks and polling](https://sms.red/en/blog/sms-webhooks-vs-polling). For the complete endpoint and field definitions, use the [public API reference](https://sms.red/docs/ai/api-reference.md).

## Frequently asked questions

### Is maxPrice the cheapest price I hope to get?

No. It is the approved maximum total charge for that purchase. Obtain the actual term quote before choosing it.

### Can I retry a timed-out purchase with a new key?

That creates a new intent and can buy again. Reconcile the original key and use identical input for any appropriate retry.

### Does the requested duration determine the exact expiry?

Use the assigned order's expiresAt. The provider may return less receiving time than the requested maximum.
