Idempotency

Network calls fail in ambiguous ways. A connection drops, a gateway times out, your worker is restarted mid-request — and you are left not knowing whether the request reached Oyster. Retrying risks creating the same resource (e.g. engagement, expense, or webhook) twice.

An idempotency key removes that ambiguity. You attach one to a write request, and Oyster records it. If the same key arrives again, Oyster refuses to run the write a second time and tells you where to find the result of the first attempt.

⚠️

Oyster does not replay the original response

A repeated idempotency key does not return the original response as though the retry had succeeded. A repeated key is rejected with 409 Conflict, and the response tells you which operation already holds the key so you can look up what happened. Treating that 409 as a failure is the most common mistake — see Resolving a conflict.

Sending an idempotency key

Add an Idempotency-Key header to any write request. Generate a fresh value, such as a UUID, for each distinct operation you want to perform.

curl --request POST \
  --url https://api.oysterhr.com/v1/webhooks \
  --header 'Authorization: Bearer <your-access-token>' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 8f14e45f-ceea-467a-9f1b-2c3d4e5f6a7b' \
  --data '{
    "url": "https://example.com/hooks/oyster",
    "sharedSecret": "a-shared-secret"
  }'

The header applies to write requests only. GET and HEAD requests ignore it, so sending a key while reading a resource has no effect and does not reserve the key.

While you are testing, the same behaviour is available in the sandbox at https://api-sandbox.oysterhr.com.

Every response carries an operation key

Each API response includes an X-Oyster-Operation-Key header identifying the operation Oyster recorded for that request:

X-Oyster-Operation-Key: 0196e3b1-4c7f-7a31-bb0c-9d2e5f8a1c44

Storing this value alongside your own record of the request is worthwhile. If you already hold the operation key for an attempt, you can look up its outcome directly and skip the conflict round trip described below.

If an idempotency key is reused

If a write arrives with a key that an earlier request already claimed, Oyster rejects it with 409 Conflict and does not perform the write:

{
  "error": {
    "message": "A request with this idempotency key has already been received"
  },
  "meta": {
    "operationKey": "0196e3b1-4c7f-7a31-bb0c-9d2e5f8a1c44"
  }
}

The meta.operationKey identifies the operation that holds the key — that is, your original request.

Resolving a conflict

A 409 tells you that your first request was received. It does not tell you whether that request succeeded. Oyster claims the key before running the write, so the key stays claimed even if the write subsequently failed.

To find out what actually happened, fetch the operation named in meta.operationKey. This endpoint accepts a token with read access (the read or manage scope):

curl --request GET \
  --url https://api.oysterhr.com/v1/meta/operations/0196e3b1-4c7f-7a31-bb0c-9d2e5f8a1c44 \
  --header 'Authorization: Bearer <your-access-token>'
{
  "request": {
    "method": "POST",
    "path": "/v1/webhooks",
    "queryParams": {
      "apiVersion": "v1",
      "idempotencyKey": "8f14e45f-ceea-467a-9f1b-2c3d4e5f6a7b"
    },
    "responseCode": 200
  },
  "data": {},
  "meta": {
    "recordId": "wH7kP2qa",
    "completed": true,
    "success": true
  }
}

Read it as follows:

FieldMeaning
meta.completedWhether the operation has finished. false means the work is still in progress.
meta.successWhether the completed operation succeeded.
request.responseCodeThe HTTP status the original request returned.
meta.recordIdThe identifier of the record the request created or updated.
dataThe result, for operations that complete in the background. Synchronous writes report their record through meta.recordId instead and leave this empty.
errorsPresent when the original request failed.

To retrieve the record itself after a conflict, use meta.recordId against the relevant endpoint — for the example above, GET /v1/webhooks.

So a 409 followed by "success": true means your retry was unnecessary and the work is done. A 409 followed by "success": false means the original attempt failed, and you should retry with a new idempotency key.

Operations are scoped to the OAuth application that created them. Any token belonging to that application can fetch them; a token from a different application receives 404 Not Found.

Rules and limits

Keys are claimed before the write runs. A claimed key means "this request was received", not "this request succeeded". Always check the operation before deciding what a 409 means.

Keys are scoped to your OAuth application. Two applications can use the same key value without colliding. Within one application, a key is claimed the moment it is first used on a write.

Keys never expire. A claim is permanent, so never recycle a key for a different payload. If you need to perform the same logical action again — for example, after a genuine failure — generate a new key.

Keys are limited to 255 characters. A longer key is rejected with 400 Bad Request and the write does not happen:

{
  "errors": [
    {
      "message": "Idempotency key is too long (maximum is 255 characters)"
    }
  ]
}

A UUID is well within the limit and is the recommended format.

Asynchronous endpoints

Some endpoints accept your request and finish the work in the background. These respond with 202 Accepted and an operation key rather than a result:

{
  "meta": {
    "operationKey": "0196e3b1-4c7f-7a31-bb0c-9d2e5f8a1c44"
  }
}

Idempotency keys work the same way here: the key is claimed when the request is accepted, so a retry is rejected with 409 whether or not the background work has finished. Poll GET /v1/meta/operations/{operationKey} until meta.completed is true, then read meta.success to find the outcome.

A retry pattern that works

  1. Generate one idempotency key per logical operation, and persist it with your own record of that operation before sending the request.
  2. Send the request with the key. Store the returned X-Oyster-Operation-Key against your record.
  3. If the response is lost or the request times out, retry using the same key.
  4. If the retry returns 409, fetch the operation from meta.operationKey rather than treating the request as failed.
  5. If that operation succeeded, you are done. If it failed, address the cause and retry with a new key.

API versions

The examples above use v1. The v0.1 endpoints behave identically, including the operations endpoint at GET /v0.1/meta/operations/{operationKey}.


Did this page help you?