Akedly

Transaction status lookup

Ask Akedly for the current result of a verification by transactionID or attemptId, one at a time or up to 50 per call. Use it when a webhook is late, missing, or arrived unsigned.


Make a lookup

There are two calls. Both take JSON only, and both match ids exactly as you send them, with no trimming. The base URL is https://api.akedly.io.

CallPathBody
Single lookupPOST /api/v1/transactions/statusExactly one of transactionID or attemptId, a non-empty string
Batch lookupPOST /api/v1/transactions/status/batchtransactionIDs and/or attemptIds, arrays of non-empty strings, 1 to 50 positions in total. Duplicates count.

Single. Send one transactionID or one attemptId, never both. Use attemptId for a V2 widget attempt. Use transactionID for V1 and V1.2 transactions. A passkey sign-in's requestID also goes in transactionID.

Batch. Send transactionIDs, attemptIds, or both. The call takes 1 to 50 positions in total, and a repeated id counts once per position.

A body that is not valid JSON gets a plain 400 that is not part of this contract. Send valid JSON.

Look up one transaction

POST
/api/v1/transactions/status
curl -X POST https://api.akedly.io/api/v1/transactions/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "transactionID": "ae2eacaebe3ed78b105498d5d0cfe54f11b4130a0847d67b36927752148679e5" }'

Look up one attempt

POST
/api/v1/transactions/status
curl -X POST https://api.akedly.io/api/v1/transactions/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "attemptId": "attempt_a1b2c3d4e5f67890" }'

Look up a batch

POST
/api/v1/transactions/status/batch
curl -X POST https://api.akedly.io/api/v1/transactions/status/batch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionIDs": [
      "ae2eacaebe3ed78b105498d5d0cfe54f11b4130a0847d67b36927752148679e5",
      "67d1b8d2f4a10c2b6e8f9a31c1d7e4b6a2f9c0d8e5b3a1f6c4d2e8b7a5c3d9e0"
    ],
    "attemptIds": ["attempt_a1b2c3d4e5f67890"]
  }'

Authentication

Send your API key in one of three places:

  • the body field APIKey (apiKey also works)
  • the header Authorization: Bearer YOUR_API_KEY
  • the header x-api-key

Never put the key in the URL. A request that carries two different keys answers 401. Use a current key: a revoked or expired key answers 401.

Two 403 answers can follow a valid key. ACCOUNT_NOT_ACTIVE means the account is deactivated. API_KEY_SCOPE_DENIED means the key is limited to some pipelines and the record sits outside them.


Limits and charging

Each account can look up 300 ids per minute. The limit is shared by both calls and by every key on the account. It resets in fixed 60-second windows. Past the limit, the call answers 429, and Retry-After gives the seconds until the next window.

What happensCounted against the 300?
A single lookupCounts 1
A batchCounts every position, duplicates included. All or nothing.
An id that is not foundCounted. It returns 404.
A request that fails validation (400)Not counted
A 429Nothing is charged. A smaller batch may still fit in the window.
A retryable 503 from the counter storeUnknown whether it was counted
An item you retryCharged again

Charges are never refunded.


Single and batch responses

A single lookup that works answers 200 with { "status": "success", "data": ... }. An error answers with { "status": "error", "code": ..., "message": ... }, and sometimes retryable.

A batch answers 200 with data.transactions and data.attempts. Order is kept within each array only, so match items by their own id and not by position across the two arrays. Within one array, a repeated id gets the same answer; on V2 answers that includes one shared timestamp. An id that appears in both arrays is looked up once for each array.

Each batch item is either status: "success" with data, or status: "error" with code and message, and sometimes retryable. A per-item error is the normal outcome and not a failed call. Rarely, the whole call fails with a 500 STATUS_LOOKUP_FAILED or a retryable 503 STATUS_LOOKUP_UNAVAILABLE. The charge is kept in both cases.

An item in a batch can answer a retryable 503 in two cases. The batch did not start it, because the batch halted or the 5000 ms start window passed. Or it was attempted but unavailable. Retry just those ids. The 5000 ms applies only to when a batch starts its lookups. It says nothing about how long the call takes.

Branch on code, never on message or on key order.


Error codes

Every error carries a code. retryable: true means the same request can succeed if you try again.

CodeHTTPWhat to do
INVALID_API_KEY401Fix the key. A revoked or expired key answers 401, and so does a request carrying two different keys.
API_KEY_INDEX_UNAVAILABLE503Retryable (retryable: true). Try again shortly.
ACCOUNT_NOT_ACTIVE403The account is deactivated. Contact support.
INVALID_LOOKUP_REQUEST400, not countedFix the body. A single call takes exactly one id. A batch takes 1 to 50 positions.
STATUS_LOOKUP_RATE_LIMITED429 with Retry-AfterWait the number of seconds in Retry-After, then retry. Nothing was charged.
TRANSACTION_NOT_FOUND, ATTEMPT_NOT_FOUND404No record this account can read. An id that belongs to another account answers the same.
API_KEY_SCOPE_DENIED403The key's pipelines do not cover this record. Use a key whose scope covers it.
TRANSACTION_REPLAY_UNAVAILABLE409, transactionID lookups onlyretryable: false. Treat it as "cannot confirm now".
STATUS_LOOKUP_UNAVAILABLE503Retryable. In a batch it can also mean the item was not attempted in this batch. Retry just those ids.
STATUS_LOOKUP_FAILED500A generic message with no detail. Retry later.

What data holds

The layout depends on the API version. Field meanings follow Webhooks → Verification result payloads.

V1, V1.2 OTP and V1.2 passkey return mainTransaction and transactionReq.

  • Name
    mainTransaction
    Type
    object
    Description

    The transaction record. Its status is Pending, Successful, Failed or Skipped.

  • Name
    transactionReq
    Type
    object | null
    Description

    The code-check or sign-in record. It is null when there is no request. Its status is Pending, Successful or Failed. Absent values are omitted. For OTP it includes inputOTP, the code the user typed. The code that was sent is OTP on mainTransaction (on transaction for V2). Keep the raw response body out of your logs.

V2 returns the same data shape as the V2 webhook, with no error object.

  • Name
    status
    Type
    string
    Description

    success, failed or pending. This is the result.

  • Name
    timestamp
    Type
    string
    Description

    Set again on every lookup. Do not use it as a key. Nothing promises it differs between lookups.

  • Name
    widgetAttempt
    Type
    object
    Description

    The attempt, with a masked phone number. Its status is pending, captcha_solved, otp_requested, verified, failed or expired. It shows progress and is never the result. Read the top-level status instead.

  • Name
    transaction
    Type
    object | null
    Description

    The transaction record.

  • Name
    transactionReq
    Type
    object | null
    Description

    The code-check or sign-in record.

  • Name
    publicMetadata, privateMetadata
    Type
    object | null
    Description

    The metadata you attached to the attempt.

verificationDate is the time of the last code check.

WhatsApp. On OTP layouts, whatsapp: { status } sits beside transactionReq. Its status is pending, sent, delivered, read or failed, or null. The status comes from the newest message linked to that exact transactionReq; it is null when there is no linked message or no request. A linked row with no stored value reads pending. null does not tell you the channel was unused. Passkey answers omit whatsapp.

Fields the webhook field list does not show. The send-record field list in Webhooks → Verification result payloads does not include these two. They can sit on both mainTransaction (or transaction on V2) and transactionReq.

  • Name
    verificationMethod
    Type
    string
    Description

    passkey on passkey answers: on both objects for V1.2, on transaction for V2.

  • Name
    reason
    Type
    string
    Description

    On transaction, expired on an expired V1.2 or V2 passkey sign-in, or temporarily_unavailable on V1.2 only. On V1.2, transactionReq.reason copies transaction.reason. Otherwise both are absent.


Attempt lookups

An attempt lookup answers for a V2 widget attempt and returns entries: up to 10, newest first. Each entry has status, transaction, transactionReq and, on OTP entries, whatsapp. Entries carry no timestamp.

For V2 webhook handling, see Step 4: Handle Callbacks.

hasMoreEntries: false means every eligible record the lookup found is listed. Records left out under the best-effort rules below do not count. hasMoreEntries: true means more may exist, and fewer than 10 may be listed.

The top-level status is success if any listed entry is success, else failed if the newest listed entry is failed, else pending. With no entries it is pending. The top-level transaction, transactionReq and whatsapp come from the newest success entry, or else from the newest entry. A top-level whatsapp appears only when that entry is an OTP entry.

Attempt lookups are best effort. Records are not back-filled, and a record that cannot be safely tied to the attempt is left out. An empty pending answer does not tell you nothing happened. The records behind two entries can share a timestamp.

A passkey sign-in submitted twice can read failed while the winning submission is still pending. The losing submission is marked Failed, and when it is the newest entry the top-level status follows it. Treat failed on a passkey attempt as "not verified yet" and look again.

Attempt lookups never answer 409. While the attempt index is not ready, they answer a retryable 503 STATUS_LOOKUP_UNAVAILABLE.


Passkey sign-ins

Two reads exist, and they are separate. The lookup on this page uses your API key and runs on your backend. The ceremony poll, GET /api/v1.2/transactions/passkey/result, is authorized by the ceremony token, is called from the client, and is read-only. See the passkey endpoint reference for the poll.

A passkey requestID answers as a transaction, so put it in transactionID. On V1.2 it comes back in the V1 layout. On V2 it comes back in the V2 layout with transactionReq: null.

Successful appears only once the sign-in is verified and billed. While billing settles, it reads Pending. An expired sign-in reads Failed with reason: "expired".

Dates appear only when they are stored. The V1.2 passkey webhook stamps the time it was sent, so its date can differ from the stored date in a lookup. A V1.2 passkey transactionReq is built from status, verificationMethod and isTest, plus reason and verificationDate when they apply.

A pending sign-in that was abandoned answers 404 after about 5 minutes. Treat that 404 as "cannot confirm now".


What lookups do not cover

  • Listing records by time window.
  • Whether a webhook was delivered.
  • Provider details.
  • A flag that says a result will not change.

A lookup returns the current result. It does not show whether a webhook was sent or received.


Using lookups with webhooks

One function, two ways in. Write one function that applies a verification result to your records. Call it from your webhook route after the signature check, and from a reconciler that calls a lookup with your own API key. Never POST a lookup answer to your own webhook URL. Never run Svix verification on it. Never make up a signature, a svix-id or an event id. The reconciler runs on your server, so the key stays there.

Identity. Your business identity is the id you stored when you created the verification. For V2 that is widgetAttempt.attemptId. For V1 and V1.2 it is mainTransaction.transactionID, and for a V1.2 passkey sign-in it is the passkey requestID. Never key on timestamp, svix-id, updateDate or verificationDate.

What counts as success. For V2, success is data.status === "success": the top level of a transactionID or attempt lookup, or any entry you can see in entries. For V1 and V1.2, check data.mainTransaction.status === "Successful" || data.transactionReq?.status === "Successful". This is a reading rule for your code, not a field Akedly returns.

Success sticks on your record. Once you record a success for an attemptId or a transactionID, never let a later failed or pending result undo it on your record, whether it comes from a webhook or a lookup. Nothing in the API says a status cannot change, and a lookup can disagree later. A V1 parent transaction can read Failed after a success, an attempt can have more than 10 entries, and reads are not taken together as one moment.

Everything else is "not verified yet". Take no irreversible action on a result that is not a success. A V2 wrong code that did not lock the transaction is one example. Its webhook said failed, but a lookup reads pending, and the lookup has no error object. When you can see no success, a 404, 403, 409 or 503, empty entries, hasMoreEntries: true and whatsapp.status: null all mean "cannot confirm now".

A lookup is not a copy of a webhook. It is built from current records. On V2 answers, timestamp is set again on every lookup, so it is not a key. It never shows whether a webhook was sent or received.

Delivery dedupe stays separate. Your webhook handler still skips repeated deliveries. svix-id skips repeated deliveries of one message. It is not a business key, and failure delivery keys carry a random suffix. Key your records on the body fields above. See Webhooks → Retries.

A conditional update plus a side effect. Updating a record only when it is not yet verified, then sending an email, is not a single step. A crash between the update and the email can lose the email, and sending first can repeat it. If the side effect must survive a crash, write it in the same database transaction as the update, or write an outbox row there and send from that. In SQL, run an update limited to rows not yet verified and check the changed-row count. Akedly provides no library, queue or framework for this.

Was this page helpful?