From 48f46ad410896f76e9c906fa5036d2b71c5fbb90 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Sun, 20 Sep 2026 20:49:19 -0700 Subject: [PATCH 1/4] docs(api-reference): fix 13 audit findings in developer-portal.json - Finding 1 (CRITICAL): add security: [{bearerAuth: []}] to send-notification, create-action/{app_id}, and transaction/debug operations, plus documented 401 responses for missing API key - Finding 2 (HIGH): add "sandbox" to the environment enum in all four VerifyV4*Request/Response schemas to match live /api/v4/verify behavior - Finding 3 (HIGH): add "face" to VerifyProofRequest.verification_level enum, matching the field's own description and live server behavior - Finding 4 (HIGH): note in the GraphQL proxy operation description that the example query requires authentication and that unauthenticated calls return 200 with a GraphQL-level error, not a 401 - Finding 5 (HIGH): mark the precheck 200 example app_id as illustrative-only and document the real 404 (app not found/inactive) response - Finding 6 (HIGH): add additionalProperties: false to both SendNotificationRequest oneOf branches so legacy and localized payloads are mutually exclusive - Finding 7 (HIGH): document accepted mini_app_path deeplink formats and replace the placeholder example value with a valid worldapp:// deeplink - Finding 8 (MEDIUM): document the 426 AU-001 app-version gate on /public/v1/miniapps/prices - Finding 9 (MEDIUM): document 400 (invalid transaction ID) on transaction/{transaction_id} and 401 (missing API key) on transaction/debug - Finding 10 (MEDIUM): document the distinct 404 "API key not found" response on create-action and send-notification, noting how to disambiguate from an unrecognized app_id via the attribute field - Finding 11 (MEDIUM): add ErrorResponse schema + example to the user-grant-cycle 400 response, consistent with the rest of the file - Finding 12 (LOW): remove orphaned CreditBorrower/CreditError schemas that no path in this spec references - Finding 13 (LOW): document JWKS caching/rate-limit behavior on /api/v1/jwks --- openapi/developer-portal.json | 193 +++++++++++++++++++++++++++------- 1 file changed, 154 insertions(+), 39 deletions(-) diff --git a/openapi/developer-portal.json b/openapi/developer-portal.json index 8acb393..3f468ba 100644 --- a/openapi/developer-portal.json +++ b/openapi/developer-portal.json @@ -132,6 +132,7 @@ "x-mint": { "href": "/api-reference/developer-portal/send-notification" }, + "security": [{ "bearerAuth": [] }], "requestBody": { "required": true, "content": { @@ -144,7 +145,7 @@ "wallet_addresses": ["0x123", "0x456"], "title": "title", "message": "Hello ${username}, your transaction is complete!", - "mini_app_path": "mini_app_path" + "mini_app_path": "worldapp://mini-app?app_id=app_id" } } } @@ -159,6 +160,36 @@ } } } + }, + "401": { + "description": "Missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "unauthorized", + "detail": "API key is required.", + "attribute": "api_key" + } + } + } + }, + "404": { + "description": "API key not recognized. Reuses the `not_found` code family also used for an unrecognized `app_id`; check the `attribute` field (`api_key` vs `app_id`) to distinguish the two cases.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "not_found", + "detail": "API key not found.", + "attribute": "api_key" + } + } + } } } } @@ -210,6 +241,21 @@ } } } + }, + "400": { + "description": "Invalid transaction ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "invalid_parameter", + "detail": "Invalid transaction ID", + "attribute": "transaction_id" + } + } + } } } } @@ -313,6 +359,7 @@ "x-mint": { "href": "/api-reference/developer-portal/get-transaction-debug-url" }, + "security": [{ "bearerAuth": [] }], "parameters": [ { "name": "app_id", @@ -346,6 +393,21 @@ } } } + }, + "401": { + "description": "Missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "unauthorized", + "detail": "API key is required.", + "attribute": "api_key" + } + } + } } } } @@ -392,6 +454,21 @@ } } } + }, + "426": { + "description": "Rejected by an app-version/client gate on this backend. This endpoint expects to be called from an approved client (e.g. World App); the exact header(s) required to pass the gate are not publicly documented.", + "content": { + "application/json": { + "example": { + "allowRetry": false, + "error": { + "success": false, + "code": "AU-001", + "message": "App update required" + } + } + } + } } } } @@ -437,7 +514,19 @@ } }, "400": { - "description": "User not found / app not installed / no active cycles" + "description": "User not found / app not installed / no active cycles", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "validation_error", + "detail": "app_id is required", + "attribute": "app_id" + } + } + } } } } @@ -449,6 +538,7 @@ "x-mint": { "href": "/api-reference/developer-portal/create-incognito-action" }, + "security": [{ "bearerAuth": [] }], "parameters": [ { "name": "app_id", @@ -479,6 +569,36 @@ } } } + }, + "401": { + "description": "Missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "unauthorized", + "detail": "API key is required.", + "attribute": "api_key" + } + } + } + }, + "404": { + "description": "API key not recognized. Reuses the `not_found` code family also used for an unrecognized `app_id`; check the `attribute` field (`api_key` vs `app_id`) to distinguish the two cases.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "not_found", + "detail": "API key not found.", + "attribute": "api_key" + } + } + } } } } @@ -532,7 +652,7 @@ }, "responses": { "200": { - "description": "OK", + "description": "OK. The `app_id` in the example below is illustrative only and may not correspond to a live app; requests for a nonexistent or inactive app_id return 404 (see below).", "content": { "application/json": { "schema": { @@ -576,6 +696,22 @@ } } } + }, + "404": { + "description": "App not found or inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "not_found", + "detail": "We couldn't find an app with this ID. Action may be inactive.", + "attribute": null, + "app_id": "app_staging_4cfd049031b0da1e8b62084b09a9f430" + } + } + } } } } @@ -583,7 +719,7 @@ "/api/v1/jwks": { "get": { "summary": "Get JWK Keys", - "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests.", + "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. This response is not edge-cached (no `Cache-Control`, `ETag`, or `Expires` headers are set) and no dedicated rate limit is published for this endpoint. Clients should cache the JWKS response client-side and only refetch on a `kid` cache miss, rather than re-fetching on every signature verification.", "x-mint": { "href": "/api-reference/developer-portal/get-jwk-keys" }, @@ -626,7 +762,7 @@ "/api/v1/graphql": { "post": { "summary": "GraphQL Proxy", - "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys.", + "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys. The example query below requires an authenticated request (JWT or API key) to resolve; an unauthenticated request still returns HTTP 200 with a GraphQL-level error (for example `no_queries_available`) rather than a 401.", "x-mint": { "href": "/api-reference/developer-portal/graphql-proxy" }, @@ -859,6 +995,7 @@ "secure_document", "document", "device", + "face", "selfie" ], "description": "The legacy verification level. Use `selfie` for Selfie Check (Beta). The historical `face` value remains accepted for backward compatibility and behaves the same as `selfie`." @@ -908,12 +1045,14 @@ "maxLength": 200 }, "mini_app_path": { - "type": "string" + "type": "string", + "description": "Deeplink to route the user to when they tap the notification. Must be one of: a WorldApp or World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, or `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" } - } + }, + "additionalProperties": false }, { "type": "object", @@ -940,12 +1079,14 @@ "description": "Localized notification content. Include at least `en`." }, "mini_app_path": { - "type": "string" + "type": "string", + "description": "Deeplink to route the user to when they tap the notification. Must be one of: a WorldApp or World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, or `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" } - } + }, + "additionalProperties": false } ], "description": "Supports legacy payload (`title` + `message`) and localized payload (`localisations`). If both are provided, localized payload is used." @@ -1185,32 +1326,6 @@ } } }, - "CreditBorrower": { - "type": "object", - "required": ["state", "score"], - "properties": { - "state": { - "type": "string", - "enum": ["INACTIVE", "ACTIVE", "DEFAULTED"], - "description": "Borrower status on Credit" - }, - "score": { - "type": "integer", - "format": "int32", - "minimum": 0, - "description": "The borrower's credit score" - } - } - }, - "CreditError": { - "type": "object", - "properties": { - "error": { - "type": "string", - "description": "Error message" - } - } - }, "ErrorResponse": { "type": "object", "properties": { @@ -1300,7 +1415,7 @@ }, "environment": { "type": "string", - "enum": ["production", "staging"], + "enum": ["production", "staging", "sandbox"], "default": "production" }, "responses": { @@ -1337,7 +1452,7 @@ }, "environment": { "type": "string", - "enum": ["production", "staging"], + "enum": ["production", "staging", "sandbox"], "default": "production" }, "responses": { @@ -1373,7 +1488,7 @@ }, "environment": { "type": "string", - "enum": ["production", "staging"], + "enum": ["production", "staging", "sandbox"], "default": "production" }, "responses": { @@ -1536,7 +1651,7 @@ }, "environment": { "type": "string", - "enum": ["production", "staging"] + "enum": ["production", "staging", "sandbox"] }, "session_id": { "type": "string", From 8c2fb8fb12b9f9aef2e638d1be8704c5657aed08 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Mon, 21 Sep 2026 10:20:21 -0700 Subject: [PATCH 2/4] polish: tighten wording, match house style Trims over-explaining response/description text added in this PR (precheck 200/404, jwks, graphql, notification 404s, 426 gate) to the repo's terser Mintlify style, and drops the undeclared `app_id` field from the precheck 404 example so it matches the ErrorResponse schema. --- openapi/developer-portal.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openapi/developer-portal.json b/openapi/developer-portal.json index 3f468ba..a0ff7c3 100644 --- a/openapi/developer-portal.json +++ b/openapi/developer-portal.json @@ -177,7 +177,7 @@ } }, "404": { - "description": "API key not recognized. Reuses the `not_found` code family also used for an unrecognized `app_id`; check the `attribute` field (`api_key` vs `app_id`) to distinguish the two cases.", + "description": "API key not recognized. Also returned for an unrecognized `app_id`; check `attribute` (`api_key` vs `app_id`).", "content": { "application/json": { "schema": { @@ -456,7 +456,7 @@ } }, "426": { - "description": "Rejected by an app-version/client gate on this backend. This endpoint expects to be called from an approved client (e.g. World App); the exact header(s) required to pass the gate are not publicly documented.", + "description": "Rejected by an app-version/client gate. Expects an approved client (e.g. World App); required header(s) aren't publicly documented.", "content": { "application/json": { "example": { @@ -586,7 +586,7 @@ } }, "404": { - "description": "API key not recognized. Reuses the `not_found` code family also used for an unrecognized `app_id`; check the `attribute` field (`api_key` vs `app_id`) to distinguish the two cases.", + "description": "API key not recognized. Also returned for an unrecognized `app_id`; check `attribute` (`api_key` vs `app_id`).", "content": { "application/json": { "schema": { @@ -652,7 +652,7 @@ }, "responses": { "200": { - "description": "OK. The `app_id` in the example below is illustrative only and may not correspond to a live app; requests for a nonexistent or inactive app_id return 404 (see below).", + "description": "OK. The `app_id` in the example is illustrative; a nonexistent or inactive app_id returns 404.", "content": { "application/json": { "schema": { @@ -707,8 +707,7 @@ "example": { "code": "not_found", "detail": "We couldn't find an app with this ID. Action may be inactive.", - "attribute": null, - "app_id": "app_staging_4cfd049031b0da1e8b62084b09a9f430" + "attribute": null } } } @@ -719,7 +718,7 @@ "/api/v1/jwks": { "get": { "summary": "Get JWK Keys", - "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. This response is not edge-cached (no `Cache-Control`, `ETag`, or `Expires` headers are set) and no dedicated rate limit is published for this endpoint. Clients should cache the JWKS response client-side and only refetch on a `kid` cache miss, rather than re-fetching on every signature verification.", + "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. Not edge-cached, and no dedicated rate limit is published; cache client-side and refetch only on a `kid` miss.", "x-mint": { "href": "/api-reference/developer-portal/get-jwk-keys" }, @@ -762,7 +761,7 @@ "/api/v1/graphql": { "post": { "summary": "GraphQL Proxy", - "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys. The example query below requires an authenticated request (JWT or API key) to resolve; an unauthenticated request still returns HTTP 200 with a GraphQL-level error (for example `no_queries_available`) rather than a 401.", + "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys. The example query requires authentication; an unauthenticated request returns HTTP 200 with a GraphQL-level error (e.g. `no_queries_available`), not a 401.", "x-mint": { "href": "/api-reference/developer-portal/graphql-proxy" }, @@ -1046,7 +1045,7 @@ }, "mini_app_path": { "type": "string", - "description": "Deeplink to route the user to when they tap the notification. Must be one of: a WorldApp or World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, or `worldidstg://verify?t=deepface`)." + "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" @@ -1080,7 +1079,7 @@ }, "mini_app_path": { "type": "string", - "description": "Deeplink to route the user to when they tap the notification. Must be one of: a WorldApp or World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, or `worldidstg://verify?t=deepface`)." + "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" From fc942350cbb52eea83302cdf7f83b301184e5d23 Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Mon, 21 Sep 2026 17:00:20 -0700 Subject: [PATCH 3/4] fix: address 2 Codex review findings on PR #194 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JWKS caching guidance (P1): refetch-on-kid-miss alone never detects a key that was revoked/rotated out — a client that already has it cached would trust it indefinitely, since removal never produces a miss. Added a bounded TTL requirement alongside the kid-miss refetch. - SendNotificationRequest oneOf (P2): the schema's own description says a payload with both title/message and localisations uses the localized content, but additionalProperties: false on both oneOf branches meant such a payload satisfied NEITHER branch. Added title/message as optional (ignored) properties on the localized branch so it's the sole match, matching the documented precedence. Verified with a real Draft7 jsonschema validator: legacy-only, localized-only, and combined payloads each now validate with zero errors against exactly one branch. --- openapi/developer-portal.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openapi/developer-portal.json b/openapi/developer-portal.json index 9b7cb20..0588b26 100644 --- a/openapi/developer-portal.json +++ b/openapi/developer-portal.json @@ -718,7 +718,7 @@ "/api/v1/jwks": { "get": { "summary": "Get JWK Keys", - "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. Not edge-cached, and no dedicated rate limit is published; cache client-side and refetch only on a `kid` miss.", + "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. Not edge-cached, and no dedicated rate limit is published; cache client-side, refetch on a `kid` miss, and also enforce a bounded TTL (e.g. 1 hour) so a revoked or rotated-out key isn't trusted indefinitely by a client that already has it cached — a miss alone never fires for a key that's been removed, only for one that's unknown.", "x-mint": { "href": "/api-reference/developer-portal/get-jwk-keys" }, @@ -1103,6 +1103,16 @@ }, "description": "Localized notification content. Include at least `en`." }, + "title": { + "type": "string", + "maxLength": 30, + "description": "Optional legacy title, ignored when `localisations` is present. Allowed here so callers may include both without failing validation — see this schema's top-level description." + }, + "message": { + "type": "string", + "maxLength": 200, + "description": "Optional legacy message, ignored when `localisations` is present. Allowed here so callers may include both without failing validation — see this schema's top-level description." + }, "mini_app_path": { "type": "string", "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." @@ -1114,7 +1124,7 @@ "additionalProperties": false } ], - "description": "Supports legacy payload (`title` + `message`) and localized payload (`localisations`). If both are provided, localized payload is used." + "description": "Supports legacy payload (`title` + `message`) and localized payload (`localisations`). If both are provided, localized payload is used — include `localisations` alongside `title`/`message` rather than choosing one shape, since the legacy branch's `additionalProperties: false` rejects a `localisations` field." }, "SendNotificationResultItem": { "type": "object", From 564e80fa270963a85afe46e56c390b5319b39c5d Mon Sep 17 00:00:00 2001 From: Soam Desai Date: Wed, 23 Sep 2026 15:00:23 -0700 Subject: [PATCH 4/4] docs(api-reference): align Developer Portal spec with route behavior - send-notification, create-action, transaction/debug: document 400 api_key_inactive, 403 invalid_app/invalid_api_key, and the real 404 causes - user-grant-cycle: add bearerAuth security and its API key responses - graphql: fix the unauthenticated example error, relabel 401 as a rejected api_ key, replace 415 with the real 400s - SendNotificationRequest: exclude localisations from the legacy branch with not/required instead of additionalProperties: false; reword the precedence note - mini_app_path: app_id must match the request's app_id - Use 42-character example wallet addresses; fix the user-grant-cycle and precheck 400 examples - Tighten the JWKS caching note; add optional app_id/team_id to ErrorResponse --- openapi/developer-portal.json | 331 +++++++++++++++++++++++++++++++--- 1 file changed, 301 insertions(+), 30 deletions(-) diff --git a/openapi/developer-portal.json b/openapi/developer-portal.json index 0588b26..bc10a79 100644 --- a/openapi/developer-portal.json +++ b/openapi/developer-portal.json @@ -142,7 +142,10 @@ }, "example": { "app_id": "app_id", - "wallet_addresses": ["0x123", "0x456"], + "wallet_addresses": [ + "0x1234567890123456789012345678901234567890", + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" + ], "title": "title", "message": "Hello ${username}, your transaction is complete!", "mini_app_path": "worldapp://mini-app?app_id=app_id" @@ -161,6 +164,32 @@ } } }, + "400": { + "description": "Invalid request, inactive API key, or the app can't send notifications (`validation_error`, `api_key_inactive`, `external_app_not_allowed`, `unverified_app_limit_reached`, `not_allowed`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "api_key_inactive": { + "value": { + "code": "api_key_inactive", + "detail": "API key is inactive.", + "attribute": "api_key" + } + }, + "unverified_app_limit_reached": { + "value": { + "code": "unverified_app_limit_reached", + "detail": "Unverified app limit reached", + "attribute": "notifications" + } + } + } + } + } + }, "401": { "description": "Missing API key", "content": { @@ -176,8 +205,34 @@ } } }, + "403": { + "description": "API key rejected: `invalid_app` when the key's team doesn't own `app_id`, `invalid_api_key` when the secret doesn't match.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_app": { + "value": { + "code": "invalid_app", + "detail": "API key is not valid for this app.", + "attribute": "api_key" + } + }, + "invalid_api_key": { + "value": { + "code": "invalid_api_key", + "detail": "API key is not valid.", + "attribute": "api_key" + } + } + } + } + } + }, "404": { - "description": "API key not recognized. Also returned for an unrecognized `app_id`; check `attribute` (`api_key` vs `app_id`).", + "description": "API key not found (`not_found`), or the app is banned or has no metadata (`app_not_found`, attribute `app`).", "content": { "application/json": { "schema": { @@ -394,6 +449,21 @@ } } }, + "400": { + "description": "Invalid `app_id` (`validation_error`) or inactive API key (`api_key_inactive`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "api_key_inactive", + "detail": "API key is inactive.", + "attribute": "api_key" + } + } + } + }, "401": { "description": "Missing API key", "content": { @@ -408,6 +478,58 @@ } } } + }, + "403": { + "description": "API key rejected: `invalid_app` when the key's team doesn't own `app_id`, `invalid_api_key` when the secret doesn't match.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_app": { + "value": { + "code": "invalid_app", + "detail": "API key is not valid for this app.", + "attribute": "api_key" + } + }, + "invalid_api_key": { + "value": { + "code": "invalid_api_key", + "detail": "API key is not valid.", + "attribute": "api_key" + } + } + } + } + } + }, + "404": { + "description": "API key not found (`not_found`), or no debug URL is available yet (`debug_url_not_available`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "not_found": { + "value": { + "code": "not_found", + "detail": "API key not found.", + "attribute": "api_key" + } + }, + "debug_url_not_available": { + "value": { + "code": "debug_url_not_available", + "detail": "Debug URL is not yet available. Please try again later.", + "attribute": "debug" + } + } + } + } + } } } } @@ -480,6 +602,7 @@ "x-mint": { "href": "/api-reference/developer-portal/get-user-grant-cycle" }, + "security": [{ "bearerAuth": [] }], "parameters": [ { "name": "wallet_address", @@ -514,16 +637,83 @@ } }, "400": { - "description": "User not found / app not installed / no active cycles", + "description": "Invalid query (`validation_error`), inactive API key (`api_key_inactive`), or user not found / app not installed / no active cycles (`user_not_found_for_wallet_address`, `app_not_installed`, `no_active_grant_cycles`, attribute `user_grant_cycle`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "validation_error": { + "value": { + "code": "validation_error", + "detail": "app_id is required", + "attribute": "app_id" + } + }, + "api_key_inactive": { + "value": { + "code": "api_key_inactive", + "detail": "API key is inactive.", + "attribute": "api_key" + } + } + } + } + } + }, + "401": { + "description": "Missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "code": "unauthorized", + "detail": "API key is required.", + "attribute": "api_key" + } + } + } + }, + "403": { + "description": "API key rejected: `invalid_app` when the key's team doesn't own `app_id`, `invalid_api_key` when the secret doesn't match.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_app": { + "value": { + "code": "invalid_app", + "detail": "API key is not valid for this app.", + "attribute": "api_key" + } + }, + "invalid_api_key": { + "value": { + "code": "invalid_api_key", + "detail": "API key is not valid.", + "attribute": "api_key" + } + } + } + } + } + }, + "404": { + "description": "API key not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { - "code": "validation_error", - "detail": "app_id is required", - "attribute": "app_id" + "code": "not_found", + "detail": "API key not found.", + "attribute": "api_key" } } } @@ -570,6 +760,32 @@ } } }, + "400": { + "description": "Invalid request (`validation_error`), inactive API key (`api_key_inactive`), or the action already exists (`constraint-violation`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "api_key_inactive": { + "value": { + "code": "api_key_inactive", + "detail": "API key is inactive.", + "attribute": "api_key" + } + }, + "constraint-violation": { + "value": { + "code": "constraint-violation", + "detail": "Action already exists.", + "attribute": "action" + } + } + } + } + } + }, "401": { "description": "Missing API key", "content": { @@ -585,8 +801,34 @@ } } }, + "403": { + "description": "API key rejected: `invalid_app` when the key's team doesn't own `app_id`, `invalid_api_key` when the secret doesn't match.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_app": { + "value": { + "code": "invalid_app", + "detail": "API key is not valid for this app.", + "attribute": "api_key" + } + }, + "invalid_api_key": { + "value": { + "code": "invalid_api_key", + "detail": "API key is not valid.", + "attribute": "api_key" + } + } + } + } + } + }, "404": { - "description": "API key not recognized. Also returned for an unrecognized `app_id`; check `attribute` (`api_key` vs `app_id`).", + "description": "API key not found.", "content": { "application/json": { "schema": { @@ -691,7 +933,7 @@ }, "example": { "code": "required", - "detail": "This attribute is required.", + "detail": "No action found for this app.", "attribute": "action" } } @@ -718,7 +960,7 @@ "/api/v1/jwks": { "get": { "summary": "Get JWK Keys", - "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. Not edge-cached, and no dedicated rate limit is published; cache client-side, refetch on a `kid` miss, and also enforce a bounded TTL (e.g. 1 hour) so a revoked or rotated-out key isn't trusted indefinitely by a client that already has it cached — a miss alone never fires for a key that's been removed, only for one that's unknown.", + "description": "Retrieve JWKs (public keys) used to verify JWT signatures for verification requests. Responses aren't cached at the edge, so cache keys client-side: refetch when a token's `kid` is unknown, and expire the cache after a bounded TTL (e.g. 1 hour) so removed keys stop being trusted.", "x-mint": { "href": "/api-reference/developer-portal/get-jwk-keys" }, @@ -761,7 +1003,7 @@ "/api/v1/graphql": { "post": { "summary": "GraphQL Proxy", - "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys. The example query requires authentication; an unauthenticated request returns HTTP 200 with a GraphQL-level error (e.g. `no_queries_available`), not a 401.", + "description": "Proxy endpoint for Developer Portal GraphQL queries and mutations. Supports user JWTs and API keys. The example query requires authentication; an unauthenticated request returns HTTP 200 with a GraphQL `validation-failed` error (`field 'app' not found in type: 'query_root'`), not a 401.", "x-mint": { "href": "/api-reference/developer-portal/graphql-proxy" }, @@ -791,22 +1033,54 @@ } } }, - "401": { - "description": "Unauthenticated request", + "400": { + "description": "Body isn't JSON: wrong `Content-Type` (`invalid_content_type`) or malformed JSON (`invalid_json`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_content_type": { + "value": { + "code": "invalid_content_type", + "detail": "Content-Type must be application/json.", + "attribute": "content-type" + } + }, + "invalid_json": { + "value": { + "code": "invalid_json", + "detail": "Request body must be valid JSON.", + "attribute": null + } + } } } } }, - "415": { - "description": "Invalid JSON payload", + "401": { + "description": "Rejected `api_` API key: not found, inactive, or wrong secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "invalid_or_inactive": { + "value": { + "code": "unauthenticated", + "detail": "Invalid or inactive API key.", + "attribute": null + } + }, + "invalid_secret": { + "value": { + "code": "unauthenticated", + "detail": "Invalid API key secret.", + "attribute": null + } + } } } } @@ -1071,13 +1345,13 @@ }, "mini_app_path": { "type": "string", - "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." + "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`, where `app_id` must match the request's `app_id`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" } }, - "additionalProperties": false + "not": { "required": ["localisations"] } }, { "type": "object", @@ -1103,28 +1377,17 @@ }, "description": "Localized notification content. Include at least `en`." }, - "title": { - "type": "string", - "maxLength": 30, - "description": "Optional legacy title, ignored when `localisations` is present. Allowed here so callers may include both without failing validation — see this schema's top-level description." - }, - "message": { - "type": "string", - "maxLength": 200, - "description": "Optional legacy message, ignored when `localisations` is present. Allowed here so callers may include both without failing validation — see this schema's top-level description." - }, "mini_app_path": { "type": "string", - "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." + "description": "Deeplink to open when the user taps the notification: a WorldApp/World ID mini app deeplink (`worldapp://mini-app?app_id=...` or `worldid://mini-app?app_id=...`, where `app_id` must match the request's `app_id`), a Deep Face Universal Link (`https://world.org/verify?t=deepface`), or a Deep Face deeplink (`worldapp://verify?t=deepface`, `worldid://verify?t=deepface`, `worldidstg://verify?t=deepface`)." }, "app_id": { "type": "string" } - }, - "additionalProperties": false + } } ], - "description": "Supports legacy payload (`title` + `message`) and localized payload (`localisations`). If both are provided, localized payload is used — include `localisations` alongside `title`/`message` rather than choosing one shape, since the legacy branch's `additionalProperties: false` rejects a `localisations` field." + "description": "Supports legacy payload (`title` + `message`) and localized payload (`localisations`). If `localisations` is present, the localized payload is used and `title`/`message` are ignored." }, "SendNotificationResultItem": { "type": "object", @@ -1373,6 +1636,14 @@ "attribute": { "type": "string", "nullable": true + }, + "app_id": { + "type": "string", + "description": "Included when the request identifies an app." + }, + "team_id": { + "type": "string", + "description": "Included when the route has resolved the app's team." } } },