Menu

Concepts

Idempotency

Safe retries with Idempotency-Key, replays, unknown outcomes and how to reconcile them.

Networks fail after a request was sent and before its answer arrived. Retrying a write blindly could then create a second lead, start a second discovery run, or run the same import twice. An Idempotency-Key makes the retry safe: the API runs the request once and answers every retry with the same result.

bash
curl -X POST https://api.dmhero.ai/v1/leads \
  -H "Authorization: Bearer $DMHERO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c9a2e-3b4d-4c5e-8f9a-0b1c2d3e4f5a" \
  -d '{ "ig_handle": "jane.doe" }'

The rules

  • Generate one key per logical action, a UUID v4 is ideal, and reuse it for every retry of that action. A new action gets a new key.
  • Keys are 1 to 255 printable ASCII characters. Keys starting with mcp: are reserved for the MCP server.
  • A key lives 24 hours. After that the same key starts a new request.
  • A key belongs to the API key that used it and to that key's rotations: a retry after you rotated the key still deduplicates, while another key of the same workspace can never replay your result.
  • A retry must be the same request: same operation, path and body, from an API key with the same reps (a rotated key keeps them). The same Idempotency-Key with a different request answers 422 API_IDEMPOTENCY_MISMATCH_422.
  • GET and DELETE need no key: they are naturally safe to repeat (a second DELETE answers 404).

What a retry gets back

SituationAnswer
The first request finishedIts result again, with the header DMHero-Idempotent-Replay: true. The resource is read fresh, so a lead deleted since answers 404.
The first request finished with a 4xxThe same error again.
The first request is still running409 API_IDEMPOTENCY_IN_PROGRESS_409. Wait a moment and retry with the same key.
The first request failed on our side (5xx)For most operations the key is released and the retry runs the request.
Same key, different request422 API_IDEMPOTENCY_MISMATCH_422.

Validation, authentication and rate limit refusals happen before the key is recorded, so a malformed request never uses up a key.

When the outcome is unknown

A few operations act on the world outside DM Hero, where a half finished attempt cannot be undone: reserving discovery tokens, starting a bulk change, an import or an export. If our server stopped after acting and before recording the result, we cannot know whether it acted. A retry with the same key then answers 409 API_IDEMPOTENCY_OUTCOME_UNKNOWN_409 with claimed_at, the time the first attempt started, and does not act again.

Reconcile, then decide:

  • A discovery run: list GET /v1/discovery/runs?created_after= with claimed_at.
  • A job (import, export, bulk change): list GET /v1/jobs.

If nothing happened, send the request again with a new key. A new key is your explicit decision to try again; the API never makes it for you.

Warning: Never generate a fresh key on every retry. That turns idempotency off and is exactly how duplicates are made.

Which operations take a key

Required operations refuse a request without a key (400 API_IDEMPOTENCY_KEY_REQUIRED_400). Accepted means optional but recommended for anything you retry automatically.

OperationMethod and pathIdempotency-Key
conversations.drafts.approvePOST /v1/conversations/{conversation_id}/drafts/{draft_id}/approveRequired
discovery.runs.createPOST /v1/discovery/runsRequired
leads.bulk.confirmPOST /v1/leads/bulk-actions/{job_id}/confirmRequired
leads.exports.createPOST /v1/leads/exportsRequired
leads.imports.createPOST /v1/leads/importsRequired
campaign_leads.upsert_enrollPOST /v1/campaign-leadsAccepted
campaigns.archivePOST /v1/campaigns/{campaign_id}/archiveAccepted
campaigns.clonePOST /v1/campaigns/{campaign_id}/cloneAccepted
campaigns.createPOST /v1/campaignsAccepted
campaigns.pausePOST /v1/campaigns/{campaign_id}/pauseAccepted
campaigns.resumePOST /v1/campaigns/{campaign_id}/resumeAccepted
campaigns.startPOST /v1/campaigns/{campaign_id}/startAccepted
conversations.archivePOST /v1/conversations/{conversation_id}/archiveAccepted
conversations.drafts.dismissPOST /v1/conversations/{conversation_id}/drafts/{draft_id}/dismissAccepted
conversations.mark_readPOST /v1/conversations/{conversation_id}/readAccepted
conversations.setter.handPOST /v1/conversations/{conversation_id}/setter/handAccepted
conversations.setter.resumePOST /v1/conversations/{conversation_id}/setter/resumeAccepted
conversations.starPOST /v1/conversations/{conversation_id}/starAccepted
conversations.unarchivePOST /v1/conversations/{conversation_id}/unarchiveAccepted
conversations.unstarPOST /v1/conversations/{conversation_id}/unstarAccepted
custom_fields.createPOST /v1/custom-fieldsAccepted
discovery.candidates.promotePOST /v1/discovery/runs/{run_id}/promoteAccepted
discovery.handles.resolvePOST /v1/discovery/handles/resolveAccepted
discovery.recipes.createPOST /v1/discovery/recipesAccepted
discovery.runs.cancelPOST /v1/discovery/runs/{run_id}/cancelAccepted
discovery.runs.preview_decisionPOST /v1/discovery/runs/{run_id}/preview-decisionAccepted
enrollments.createPOST /v1/campaigns/{campaign_id}/enrollmentsAccepted
enrollments.pausePOST /v1/enrollments/{enrollment_id}/pauseAccepted
enrollments.resumePOST /v1/enrollments/{enrollment_id}/resumeAccepted
folders.createPOST /v1/foldersAccepted
jobs.cancelPOST /v1/jobs/{job_id}/cancelAccepted
leads.bulk.previewPOST /v1/leads/bulk-actionsAccepted
leads.tags.addPOST /v1/leads/{lead_id}/tagsAccepted
leads.upsertPOST /v1/leadsAccepted
reps.excluded.addPOST /v1/reps/{rep_id}/excluded-handlesAccepted
setter.knowledge.createPOST /v1/reps/{rep_id}/setter/knowledgeAccepted
setter.variants.archivePOST /v1/setter/variants/{variant_id}/archiveAccepted
setter.variants.createPOST /v1/reps/{rep_id}/setter/variantsAccepted
setter.variants.promotePOST /v1/setter/variants/{variant_id}/promoteAccepted
setter.variants.publishPOST /v1/setter/variants/{variant_id}/publishAccepted
setter.variants.start_testPOST /v1/setter/variants/{variant_id}/start-testAccepted
suppressions.addPOST /v1/suppressionsAccepted
templates.createPOST /v1/templatesAccepted
templates.spintax.proposePOST /v1/templates/spintax-proposalsAccepted
webhooks.deliveries.redeliverPOST /v1/webhook-deliveries/{delivery_id}/redeliverAccepted
webhooks.endpoints.createPOST /v1/webhook-endpointsAccepted
webhooks.endpoints.rotate_secretPOST /v1/webhook-endpoints/{endpoint_id}/rotate-secretAccepted
webhooks.endpoints.testPOST /v1/webhook-endpoints/{endpoint_id}/testAccepted
Idempotency | DM Hero Developers