JWT
Verify an ES256/ES384/ES512 JWT signature
https://ecdsa.com/api/v1/jwt/verifyVerifies the token's ECDSA signature against a public key (PEM SPKI, EC JWK, or bare SEC1 point). Non-ECDSA algorithms (HS*, RS*, PS*, EdDSA, ES256K, none) return 400 with code unsupported_algorithm. A signature that fails verification returns valid: false with HTTP 200. Time claims are noted in explanations but do not affect valid.
- API key
- Optional; a key needs the scope api
- Quota cost with a key
- 1 unit per call
- Rate limit
- 60 requests/min per IP
Request
JSON body: JwtVerifyRequest
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | yes | Compact JWS token with alg ES256, ES384 or ES512. |
| publicKey | string | yes | PEM "PUBLIC KEY" (SPKI), EC JWK JSON, or a bare SEC1 point in hex/base64. |
Responses
200Verification outcome.
| Field | Type | Required | Description |
|---|---|---|---|
| valid | boolean | yes | |
| alg | string | yes |
|
| explanations | string[] | yes |
Headers: X-Request-Id · X-RateLimit-Limit · X-RateLimit-Remaining · X-RateLimit-Reset · X-Quota-Limit · X-Quota-Remaining · X-Quota-Reset
| Status | Meaning | Codes |
|---|---|---|
| 400 | Bad request. Input limits: a request body of at most 100 KB (a larger one is answered with 413) and arrays and objects nested at most 64 levels deep, JSON documents inside string fields included. | |
| 401 | Unauthorized. | |
| 403 | Forbidden. | |
| 404 | Not found. | |
| 405 | Method not allowed. | |
| 413 | Payload too large. | |
| 429 | Too many requests. | |
| 500 | Internal server error. | |
| 503 | Service unavailable. | |
| 504 | Gateway timeout. |
Every error answer carries the JSON envelope; the error table says what each code means and what to do.
Example
An ES256 token and its public JWK
curl -sS 'https://ecdsa.com/api/v1/jwt/verify' \
-H 'content-type: application/json' \
-d '{
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNvcHBlci1leGFtcGxlLTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6InVzZXJfNDIiLCJhdWQiOiJlY2RzYSIsImlhdCI6MTc4NzE0MDgwMCwiZXhwIjo0MDcwOTA4ODAwfQ.fQpTQxkahGRlZp-U71-xNk68mpZH23dhn9QoysMLmZfBx_e-vd9sCgh7sJ4eXu0-WbQSNqzmVPyUKErCJx4DHw",
"publicKey": "{\n \"kty\": \"EC\",\n \"crv\": \"P-256\",\n \"x\": \"A4KIXkvTVuOzkR5Mzgsy8n_VSSIlephF1ViMxDYTUx4\",\n \"y\": \"Py8AMAM_6yWsOTbv8eHCvKDfObpZmTHy5P4Jz30iMuU\",\n \"kid\": \"copper-example-1\"\n}"
}'const response = await fetch("https://ecdsa.com/api/v1/jwt/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNvcHBlci1leGFtcGxlLTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6InVzZXJfNDIiLCJhdWQiOiJlY2RzYSIsImlhdCI6MTc4NzE0MDgwMCwiZXhwIjo0MDcwOTA4ODAwfQ.fQpTQxkahGRlZp-U71-xNk68mpZH23dhn9QoysMLmZfBx_e-vd9sCgh7sJ4eXu0-WbQSNqzmVPyUKErCJx4DHw",
"publicKey": "{\n \"kty\": \"EC\",\n \"crv\": \"P-256\",\n \"x\": \"A4KIXkvTVuOzkR5Mzgsy8n_VSSIlephF1ViMxDYTUx4\",\n \"y\": \"Py8AMAM_6yWsOTbv8eHCvKDfObpZmTHy5P4Jz30iMuU\",\n \"kid\": \"copper-example-1\"\n}"
}),
});
console.log(response.status, await response.json());import httpx
response = httpx.post(
"https://ecdsa.com/api/v1/jwt/verify",
json={
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNvcHBlci1leGFtcGxlLTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6InVzZXJfNDIiLCJhdWQiOiJlY2RzYSIsImlhdCI6MTc4NzE0MDgwMCwiZXhwIjo0MDcwOTA4ODAwfQ.fQpTQxkahGRlZp-U71-xNk68mpZH23dhn9QoysMLmZfBx_e-vd9sCgh7sJ4eXu0-WbQSNqzmVPyUKErCJx4DHw",
"publicKey": "{\n \"kty\": \"EC\",\n \"crv\": \"P-256\",\n \"x\": \"A4KIXkvTVuOzkR5Mzgsy8n_VSSIlephF1ViMxDYTUx4\",\n \"y\": \"Py8AMAM_6yWsOTbv8eHCvKDfObpZmTHy5P4Jz30iMuU\",\n \"kid\": \"copper-example-1\"\n}",
},
timeout=30.0,
)
print(response.status_code, response.json()){
"valid": true,
"alg": "ES256",
"explanations": [
"ES256: ECDSA on P-256 with SHA-256; the JWS signature is raw r‖s (64 bytes) over the ASCII bytes of base64url(header).base64url(payload).",
"The signature verifies with the supplied key."
]
}The API’s answer to this example, produced for this page on October 20, 2026; relative times and day counts in it refer to that day.
Error codes
| Code | Status | Meaning |
|---|---|---|
| invalid_json | 400 | The body is not valid JSON or not a JSON object; or the input nests arrays and objects more than 64 levels deep (JSON documents inside string fields count too), or is too complex to inspect for private keys. |
| missing_field | 400 | A required field is absent or empty. |
| invalid_input | 400 | A field could not be parsed — the message explains exactly why. |
| private_key_rejected | 400 | The request contains private-key material (a PEM PRIVATE KEY block, an OpenSSH, SSH2 or PuTTY private key, or a JWK with "d"). Never send private keys anywhere. |
| unsupported_algorithm | 400 | The token's algorithm is not one this operation verifies. |
| unauthorized | 401 | The Authorization or X-API-Key header is malformed, the API key is not valid (wrong checksum, unknown, revoked or expired), or the operation needs a key. |
| forbidden_scope | 403 | The API key is valid but lacks a scope this operation requires; the message names it. |
| not_found | 404 | No operation exists at this path. |
| method_not_allowed | 405 | The path exists but does not accept this HTTP method; see the Allow header. |
| payload_too_large | 413 | The body exceeds 100 KB. |
| rate_limited | 429 | Too many requests — see Retry-After. |
| quota_exceeded | 429 | A daily or monthly quota of the API key's account is used up. X-Quota-Reset (and Retry-After) say when it starts over; retrying earlier cannot succeed. |
| internal_error | 500 | Unexpected server error. |
| service_unavailable | 503 | API keys cannot be verified right now (the key store is unreachable); retry shortly. |
| timeout | 504 | The operation did not finish within its time budget. |
Schemas
Error
The error envelope of every non-2xx response.
| Field | Type | Required | Description |
|---|---|---|---|
| error | object | yes | |
| error.code | string | yes | Machine-readable error code. Each response lists the codes it can carry; new codes may be added within v1, so treat an unknown code by its HTTP status. |
| error.message | string | yes | Human-readable explanation (English). |
| error.requestId | string | no | The X-Request-Id of this request — quote it when reporting a problem. Sent since v1.1. |
| error.detail | object | no | Structured context for the error, such as the offending field; omitted when there is none.
|
| error.docs | string | no | Where the error codes are explained. Sent since v1.1.
|
All operations are listed in the API reference.