Action Confirmation Signatures

Exchange a wallet signature for an action token, with EOA, smart-contract wallet and Stellar signers.

Before You Start

Read the following guides before proceeding:

GuideWhy
AuthenticationThe corporation token whose employee_address must sign
Card DetailsThe full sign → exchange → read flow that consumes the action token
3DS AuthenticationThe other consumer of action tokens

Overview

The action confirmation endpoint exchanges a message signed by the employee's wallet for an action token.
The signer can be an EVM EOA, an EVM smart-contract wallet (ERC-1271)
or a Stellar account.

MethodPathPermissions
POST/api/v1/confirmation/signature/verifyCardSu or CardViewDetails
POST/api/v1/confirmation/signature (obsolete — use /verify)CardSu or CardViewDetails

Both accept the corporation token only and behave identically.


Who Signs

The signer is always the address in the corporation token's employee_address claim — the
X-User-Wallet sent to POST /api/v1/corporations/login.

A signature from any other wallet is rejected, including a smart account controlled by that wallet.
If the employee logged in with an EOA that owns a smart account, sign with the EOA. ERC-1271 applies only
when employee_address itself is the contract.

Wallet behind employee_addressSignaturemessage_signature encoding
EVM EOAEIP-191 personal_sign over the message. v as 27/28 or 0/10x hex, 65 bytes
EVM smart-contract walletAny bytes the wallet's isValidSignature(hash, signature) accepts, where hash is the EIP-191 hash of the message0x hex. Passed to the contract exactly as sent
StellarEd25519 over the SHA-256 of the messageStandard base64
XRPLNot accepted on these endpoints—

The message, character for character:

By signing this I confirm that I am executing action <action_type> at <nonce>

Smart-Contract Wallet Conditions

  • The contract must be deployed on the chain stored in the corporation token's chain_id claim — the
    X-Chain-Id sent at login, or the default chain when none was sent. An undeployed (counterfactual) account
    cannot pass.
  • The isValidSignature call has a 3-second timeout. A timeout, a revert, an unreachable node or any return
    value other than 0x1626ba7e rejects the signature as invalid_signature.
  • The ERC-1271 check runs only after EOA recovery fails, so an EOA signature never depends on a node call.

Request

POST /api/v1/confirmation/signature/verify
{
  "action_type": "GetCardDetails",
  "message_signature": "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac81f0c5b0e4a7c1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c1b",
  "nonce": 1761480000
}

Signed message for this request:

By signing this I confirm that I am executing action GetCardDetails at 1761480000
FieldTypeRequiredDescription
action_typestringYesGetCardDetails or 3dsChallenge
message_signaturestringYesSignature over the message, encoded per Who Signs
nonceinteger (int64)YesUnix time in seconds used in the message. Rejected when more than 120 seconds old

Response

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
FieldDescription
tokenAction token for the action_type that was signed

Error Handling

{
  "error_reason": "ErrorInvalidField",
  "error_description": "Invalid signature",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    { "key": "field", "details": "signature" },
    { "key": "issue", "details": "invalid_signature" },
    { "key": "signature", "details": "Invalid signature" }
  ]
}

Validation Errors (400)

Error Reasonfield / issueerror_descriptionReturned when
ErrorMissingFieldaction_type / missingaction type is requiredaction_type is empty
ErrorInvalidFieldaction_type / invalid_valueInvalid value for action typeaction_type is not GetCardDetails or 3dsChallenge
ErrorMissingFieldmessage_signature / missingmessage signature is requiredmessage_signature is empty
ErrorMissingFieldnonce / missingnonce is requirednonce is missing or 0
ErrorInvalidFieldnonce / expiredInvalid noncenonce is more than 120 seconds old
ErrorInvalidFieldsignature / invalid_formatInvalid format for signatureEVM signature is not hex, or is shorter than 65 bytes and the wallet does not accept it over ERC-1271. Stellar signature is not base64
ErrorInvalidFieldsignature / invalid_signatureInvalid signatureThe signature is not employee_address's signature over this exact message: another wallet, another action_type or nonce, a smart-contract wallet that declines it, or a smart-contract wallet that cannot be reached on its chain

Server Errors (500)

Error ReasonDescriptionReturned when
ErrorGeneralUnsupported chain typeemployee_address is an XRPL address
ErrorGeneralFailed to issue action tokenThe signature is valid, but the action token could not be issued

Reading a Signature Error

const res = await fetch(`${baseUrl}/api/v1/confirmation/signature/verify`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${corporationToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ action_type: 'GetCardDetails', message_signature: signature, nonce })
});

if (res.status === 400) {
  const err = await res.json();
  const details = Object.fromEntries((err.error_details ?? []).map(d => [d.key, d.details]));

  if (err.error_reason === 'ErrorInvalidField' && details.field === 'signature') {
    if (details.issue === 'invalid_signature') {
      // Ask the user to sign again with the employee_address wallet, with a fresh nonce
    } else if (details.issue === 'invalid_format') {
      // Encoding defect in the integration
    }
  }
}
res = requests.post(
    f"{base_url}/api/v1/confirmation/signature/verify",
    headers={"Authorization": f"Bearer {corporation_token}"},
    json={"action_type": "GetCardDetails", "message_signature": signature, "nonce": nonce},
)

if res.status_code == 400:
    err = res.json()
    details = {d["key"]: d["details"] for d in err.get("error_details", [])}

    if err["error_reason"] == "ErrorInvalidField" and details.get("field") == "signature":
        if details.get("issue") == "invalid_signature":
            pass  # Ask the user to sign again with the employee_address wallet, with a fresh nonce
        elif details.get("issue") == "invalid_format":
            pass  # Encoding defect in the integration
body, _ := json.Marshal(map[string]any{
    "action_type":       "GetCardDetails",
    "message_signature": signature,
    "nonce":             nonce,
})

req, _ := http.NewRequest("POST", baseURL+"/api/v1/confirmation/signature/verify", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+corporationToken)
req.Header.Set("Content-Type", "application/json")

res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()

if res.StatusCode == http.StatusBadRequest {
    var apiErr struct {
        ErrorReason  string `json:"error_reason"`
        ErrorDetails []struct {
            Key     string `json:"key"`
            Details string `json:"details"`
        } `json:"error_details"`
    }
    json.NewDecoder(res.Body).Decode(&apiErr)

    details := map[string]string{}
    for _, d := range apiErr.ErrorDetails {
        details[d.Key] = d.Details
    }

    if apiErr.ErrorReason == "ErrorInvalidField" && details["field"] == "signature" {
        switch details["issue"] {
        case "invalid_signature":
            // Ask the user to sign again with the employee_address wallet, with a fresh nonce
        case "invalid_format":
            // Encoding defect in the integration
        }
    }
}

Did this page help you?