Veefore docs

Idempotency

Make a retry safe. Send a key, derive it from the operation, and understand what Veefore guarantees versus what your downstream must.

A protected action can time out after the downstream has already applied its effect. Without an idempotency key, the retry refunds the customer twice.

#Send a key

ts
await agent.act({
  tool: 'refund_customer',
  capability: 'refund.create',
  parameters: { orderId: 'ord_8812' },
  amount: 250_00,
  currency: 'INR',
  idempotencyKey: 'refund-ord_8812',
});
http
POST /gateway/v1/actions
authorization: Bearer aos_at_…
content-type: application/json
idempotency-key: refund-ord_8812

{ "tool": "refund_customer", "parameters": { "orderId": "ord_8812" }, "amount": 25000 }

Control plane mutations accept it too, as the Idempotency-Key header. Which endpoints is in the API reference — they are marked accepts Idempotency-Key.

#Derive it from the operation

The key must identify the operation, not the attempt.

Key Verdict
refund-ord_8812 Correct. One refund per order
refund-ord_8812-2026-10-03 Correct, if one refund per order per day is the rule
crypto.randomUUID() Wrong. A new key per attempt defeats the whole mechanism
Date.now() Wrong. Same reason, less obviously
refund Wrong. Collides across every order

A fresh UUID per attempt is the same as no key at all

It is the most common mistake here, and it looks correct in review because there is a key. But the retry carries a different key, so it is a different operation, so the refund happens twice.

Derive the key from the business operation. If you cannot name the operation, you cannot make it idempotent.

A useful construction: a stable prefix, the resource id, and a window if repeats are legitimate.

ts
const idempotencyKey = `refund-${orderId}-${claimId}`;

#What a reused key does

Situation Result
Same key, same body The original result is returned. The action does not run again
Same key, different body 409 IDEMPOTENCY_KEY_REUSED
Different key, same body A new, separate action

The middle row is the protection that matters. It stops a key from being accidentally recycled for a different operation, which would return the wrong result and look like success.

#Across an approval resume

This is where it earns its keep.

ts
const idempotencyKey = `refund-${orderId}`;

// First attempt → APPROVAL_REQUIRED
try {
  await agent.act({ tool: 'refund_customer', parameters: { orderId }, amount, idempotencyKey });
} catch (error) {
  if (error instanceof VeeforeApprovalRequiredError) {
    await jobs.enqueue('resume', { orderId, approvalId: error.approvalId, idempotencyKey });
  }
}

// Twenty minutes later, after a human approves
await agent.act({
  tool: 'refund_customer',
  parameters: { orderId },
  amount,
  idempotencyKey,          // the same key
  approvalId,
});

The key is stable across both. If the resume job itself gets retried — which queues do — the refund still happens once.

Note that the approval has its own binding guarantee: it is tied to the exact request that created it, and a changed payload is refused with APPROVAL_NOT_GRANTED. The two mechanisms are independent and you want both.

#What Veefore guarantees, and what it does not

Veefore guarantees that a repeated key with the same body does not re-run the pipeline or re-execute the tool. It returns the recorded result.

Veefore forwards your key to the downstream as the idempotency-key header on real HTTP egress.

Veefore cannot guarantee that your downstream honours it. If your refund service ignores the header, a retry that reaches it will refund twice regardless of what Veefore does.

Make your downstream idempotent too

Veefore's window protects you from re-running the pipeline. It does not protect you from a downstream that was mid-write when the connection dropped and has no deduplication of its own.

EXECUTION_TIMEOUT is the case to think about: Veefore does not know whether the effect was applied. If your downstream deduplicates on the forwarded key, the retry is safe. If it does not, you must check state before retrying.

#When not to retry at all

Even with a key:

  • Never retry a 403. The pipeline refused. The answer will not change.
  • Never retry IDEMPOTENCY_KEY_REUSED. Your keys collide. Fix the derivation.
  • Retry EXECUTION_TIMEOUT only if the downstream deduplicates. Otherwise read state first.

See Errors and retries.

#Reads need no key

Idempotency-Key is for mutations. A GET is already idempotent and the header is ignored.

Identity, authority, policy and enforcement for autonomous AI agents.