Quick Reference

🔑

Header

Name: Idempotency-Key

Format: up to 255 characters

Recommended: UUID v4

Generation: caller-side, one key per logical operation

🎯

Scope

Methods: POST, PUT, DELETE, PATCH

Excluded paths: /user/login, /common/lookup, /common/HealthCheck

Isolation: keys are scoped to your 7G instance

⏱️

Replay Window

TTL: 24 hours

Cached: 2xx responses only

Replay signal: Idempotent-Replayed: true response header

Why This Matters

Network failures and load-balancer retries can leave a write in an uncertain state - the request may or may not have reached the server. Idempotency lets you retry write requests safely, returning the original response on retry instead of executing the operation twice.

🚦

When This Applies To You

Two behaviours, one per instance status

Instance Status With Idempotency-Key Without Idempotency-Key
Not enrolled (default) Header is silently ignored. No replay protection. Unchanged behaviour.
Enrolled Replay protection active for 24 hours. HTTP 400. The header is mandatory.

If 7G has not explicitly confirmed enrolment for your instance, you are not enrolled.

🔁

How It Works

Three observable outcomes once enrolled

1

First request with a new key

The API processes the request normally. On a 2xx response, the status, body, and a SHA-256 hash of method + path + query string + body are stored under your instance ID and the supplied key for 24 hours. Non-2xx responses are not stored.

Concurrent requests with the same key - whether from multiple threads or multiple application pods - are serialised by the API. One executes; the others wait and receive the cached response, so you do not need client-side coordination.

2

Retry with the same key and same payload

The API returns the original cached response verbatim - same status, body, and content type - without re-running the operation. The response carries one additional header:

Idempotent-Replayed: true
3

Same key, different payload

If method, path, query string, or body hash differ from the original, the API rejects the request:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "result": false,
  "message": "Idempotency key was reused with a different request payload or endpoint.",
  "data": {}
}

Generate a new key for the new operation.

📤

Request Format

Header rules, covered methods, and matching algorithm

The header name is case-insensitive (per HTTP spec). The server treats the value as an opaque string and does not interpret its contents - only equality matters. Refer to the Quick Reference for length and format.


Methods Covered

Idempotency applies to all four write methods: POST, PUT, DELETE, and PATCH. POST, PUT, and DELETE are in active use across the 7G API today; PATCH is reserved for future endpoints - if one is added, enrolled instances will need to attach the header to PATCH calls too.


Excluded Paths

The following endpoints are always exempt, regardless of enrolment:

  • POST /user/login
  • GET /common/lookup
  • GET /common/HealthCheck

Note: POST /User/RefreshToken is not excluded. Enrolled instances must attach the header to refresh calls too - if you rely on the SDK's automatic token refresh, use the DelegatingHandler pattern shown in Integration Patterns below.


Matching Algorithm

Two requests are considered identical when all four of the following match:

  • HTTP method
  • Request path
  • Query string
  • SHA-256 hash of the request body

Any difference while the key is reused triggers the 422 response described above.

Large bodies: request bodies above 1 MB are hashed by length only rather than by content. Two same-length oversized payloads sent under one key would be treated as identical - use distinct keys for large uploads (e.g. POST /Document/File) to avoid an accidental replay.

bash
curl -X POST 'https://test-api.7g.com.au/BizTransaction/Deposit' \
  -H 'Authorization: eyJxxx_your_access_token_here' \
  -H 'Version: 2.0' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 9f8c0e2a-1b3d-4c5f-8e7a-2d4b6f0a1c3e' \
  -d '{
    "accountID": 12345,
    "investmentID": 25,
    "amount": 50000.00,
    "paymentMethodID": 1,
    "transactionDate": "2026-05-13"
  }'
📥

Response Signals

Distinguishing executed requests from replays

Replayed responses carry one additional header. It is absent on first-time responses.

Idempotent-Replayed: true

What Gets Cached

Response Status Cached? Retry Semantics
2xx Yes - 24 hours Same key returns the cached response without re-execution.
4xx No Fix the underlying issue and retry with the same key.
5xx No Retry with the same key. The next attempt executes for real.

Because only successful responses are cached, a failed write is always safe to retry with the same key.

json
HTTP/1.1 200 OK
Content-Type: application/json

{
  "result": true,
  "message": "Deposit processed successfully",
  "recordCount": 1,
  "data": {
    "bizTransactionID": 3001,
    "externalBizTransactionId": "DEP-2026-001",
    "accountID": 12345,
    "investmentID": 25,
    "amount": 50000.00,
    "quantity": 40000.00,
    "transactionDate": "2026-05-13T00:00:00Z",
    "statusName": "Complete"
  }
}
⚠️

Error Responses

Exact messages returned by the middleware

Status Scenario Message Resolution
400 Header missing (enrolled instance) "'Idempotency-Key' header is required for POST requests." (or PUT, DELETE, PATCH) Include the header.
400 Key exceeds 255 characters "'Idempotency-Key' must be 255 characters or fewer." Shorten the key. A UUID v4 is 36 characters.
422 Key reused with different payload "Idempotency key was reused with a different request payload or endpoint." Generate a fresh key.

Errors use the standard APIResponse envelope. See Responses & Error Handling for the full pattern.

🛠️

Integration Patterns

Attaching keys from HTTP/REST and from the .NET SDK

1

HTTP/REST

Generate a UUID per logical operation, send it as a header, and persist it locally so retries reuse the same value.

bash
#!/bin/bash
KEY=$(uuidgen)

for attempt in 1 2 3; do
  STATUS=$(curl -sS -o response.json -w '%{http_code}' \
    -X POST 'https://test-api.7g.com.au/BizTransaction/Deposit' \
    -H "Authorization: $TOKEN" \
    -H 'Version: 2.0' \
    -H 'Content-Type: application/json' \
    -H "Idempotency-Key: $KEY" \
    -d @payload.json)

  [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 300 ] && break
  [ "$STATUS" -ge 400 ] && [ "$STATUS" -lt 500 ] && break
  sleep $((attempt * 2))
done
2

.NET SDK

The SDK does not attach idempotency keys automatically, and SevenGClient does not expose its inner HttpClient. Construct your own HttpClient and pass it to the SevenGClient(HttpClient) constructor - the parameterless SevenGClient(SevenGEnvironment) overload owns its HttpClient internally and gives no header control.

  • DelegatingHandler (recommended) - attaches a key to every write request automatically.
  • Caller-supplied key - required when retrying a logical operation, so the same key is preserved across attempts.
  • Per-call header - simplest, but easy to leave a stale key on DefaultRequestHeaders.

Automatic token refresh needs a key too

The SDK's EnsureValidTokenAsync issues POST /User/RefreshToken silently when the access token nears expiry. That endpoint is not on the excluded list, so enrolled instances must cover it. The DelegatingHandler pattern handles refresh transparently; the Caller-supplied and Per-call patterns do not - if the SDK refreshes between your business calls, the refresh request will hit HTTP 400. Prefer the handler if your session may outlive a single access token.

json
public class IdempotencyHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (request.Method == HttpMethod.Post ||
            request.Method == HttpMethod.Put ||
            request.Method == HttpMethod.Delete ||
            request.Method == HttpMethod.Patch)
        {
            // Preserve caller-set keys (used by retry paths below)
            if (!request.Headers.Contains("Idempotency-Key"))
            {
                request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
            }
        }

        return base.SendAsync(request, cancellationToken);
    }
}

// Hand the SDK an HttpClient with the handler attached
var httpClient = new HttpClient(new IdempotencyHandler
{
    InnerHandler = new HttpClientHandler()
})
{
    BaseAddress = new Uri("https://test-api.7g.com.au")
};

var client = new SevenGClient(httpClient);
await client.Auth.LoginAsync("your-username", "your-password");

// Every write request now carries an Idempotency-Key
var response = await client.BizTransaction.DepositAsync(depositDto);

Best Practices

Guidelines for predictable retry behaviour

Recommended

  • Use UUID v4 for keys
  • Generate one key per logical operation, not per HTTP attempt - retries of the same operation must reuse the same key
  • Persist the key locally before sending, so a crash mid-flight can resume with the same value
  • Log the key alongside your request ID for end-to-end traceability
  • Retry non-2xx responses with the same key - nothing was cached, so the next attempt executes for real
  • Treat HTTP 422 with the reuse message as a code defect, not a transient failure
  • Monitor the Idempotent-Replayed: true response header to track retry behaviour

Avoid

  • Reuse the same key across different operations - this returns HTTP 422
  • Generate a new key on every retry attempt - this defeats deduplication
  • Use sequence numbers, timestamps, or business IDs as keys - they invite collisions
  • Send the header on GET requests - the server ignores it, but it clutters your own client traces
  • Cache responses client-side based on the key - the server already does this

What's Next?

Related concepts: