diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index edd1c5018..d78362e98 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -8913,6 +8913,222 @@ paths: application/json: schema: $ref: '#/components/schemas/Error501' + /cards/{id}/pin: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Set a card PIN + description: |- + Set or replace the card's PIN from a PIN block your client encrypted for the card processor. Grid forwards the block verbatim and never sees the PIN: it is not stored, not logged, and not readable by Grid at any point. + + Use this when you operate your own PIN-entry UI. To hand PIN entry to the processor's hosted iframe instead, use `POST /cards/{id}/pin/session` — the two are alternatives, and a card may use either at any time. + + Setting a PIN clears a `BLOCKED` PIN, so a cardholder locked out by incorrect attempts can recover by choosing a new PIN without a separate unblock. On success the card's `pinStatus` becomes `OK`. + + Every call is audit-logged with the requesting actor. + operationId: setCardPin + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSetRequest' + responses: + '204': + description: PIN set. + '400': + description: Bad request. Returned with `INVALID_INPUT` when `encryptedPinBlock` is absent, is not valid base64, or is rejected by the card processor as a malformed or mis-encrypted PIN block. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the change against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/pin/session: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Create a PIN-entry session + description: |- + Mint a short-lived session token for the card processor's PIN-entry iframe, so the cardholder can choose their own PIN without the PIN crossing your servers or Grid's. + + Request the session right before rendering the iframe and hand the returned `sessionToken` to the processor's embed SDK immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log it. Grid returns no URL — the processor's embed SDK builds one from the token. + + PIN entry is fixed at 4 digits, matching `POST /cards/{id}/pin`. + + Grid is not told whether the cardholder completed PIN entry. Read `pinStatus` on the `Card` after the session to confirm the outcome. + + If you operate your own PIN-entry UI, use `POST /cards/{id}/pin` instead. + + Every session minted is audit-logged with the requesting actor. + operationId: createCardPinSession + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSessionRequest' + responses: + '200': + description: PIN-entry session minted. + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSessionResponse' + '400': + description: Bad request. Returned with `INVALID_INPUT` when `targetOrigin` is absent or is not a canonical HTTPS origin. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the mint against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/pin/unblock: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Unblock a card PIN + description: |- + Clear a `BLOCKED` PIN, restoring the cardholder's existing PIN without changing it. A PIN blocks after three consecutive incorrect entries; until it is cleared, PIN-verified transactions decline. + + Unblocking is idempotent and safe to call on a card whose `pinStatus` is already `OK`. It has no effect on a card with no PIN set, which returns `409` — set a PIN with `POST /cards/{id}/pin` instead. + + Every call is audit-logged with the requesting actor. + operationId: unblockCardPin + tags: + - Cards + security: + - BasicAuth: [] + responses: + '204': + description: PIN unblocked. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the change against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, with `PIN_NOT_SET` for a card that has no PIN to unblock, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' /sandbox/cards/{id}/simulate/authorization: post: summary: Simulate a card authorization @@ -25915,6 +26131,22 @@ components: description: | Physical form factor of the card. Only `VIRTUAL` is supported in v1; `PHYSICAL` will be added in a later release. + CardPinStatus: + type: string + enum: + - NOT_SET + - OK + - BLOCKED + example: OK + description: | + State of the card's PIN. Absent on cards whose processor does not offer + PIN management. + + | Status | Description | + |--------|-------------| + | `NOT_SET` | No PIN has been set on the card. PIN-verified transactions decline. Set one with `POST /cards/{id}/pin` or `POST /cards/{id}/pin/session`. | + | `OK` | A PIN is set and usable. | + | `BLOCKED` | The PIN was entered incorrectly three consecutive times and is refused until cleared with `POST /cards/{id}/pin/unblock`, or replaced with `POST /cards/{id}/pin`. | Card: type: object required: @@ -25952,6 +26184,8 @@ components: $ref: '#/components/schemas/CardBrand' form: $ref: '#/components/schemas/CardForm' + pinStatus: + $ref: '#/components/schemas/CardPinStatus' last4: type: string description: Last four digits of the card PAN. @@ -26137,6 +26371,52 @@ components: format: date-time description: When the signed URL stops loading. Request a new reveal rather than re-rendering an expired URL. example: '2026-05-08T14:16:00Z' + CardPinSetRequest: + type: object + required: + - encryptedPinBlock + properties: + encryptedPinBlock: + type: string + description: |- + The cardholder's PIN, encrypted for the card processor. Grid forwards this value to the processor verbatim and can neither read nor validate it — no PIN, in plaintext or ciphertext, is ever stored or logged by Grid. + + Build it in the client, immediately after PIN entry, so the PIN never reaches your servers either: serialize `{"nonce": , "pin": "<4-digit PIN>"}` as UTF-8, encrypt it under the processor's public key, and base64-encode the ciphertext. Generate a fresh `nonce` per request — it is what makes a captured block useless to replay. + + A block the processor rejects returns `400 INVALID_INPUT`; Grid cannot distinguish a malformed block from a mis-encrypted one, so verify your encryption against the processor's documentation rather than against Grid's response. + minLength: 16 + maxLength: 1024 + example: SGVsbG8sIHRoaXMgaXMgYSBiYXNlNjQtZW5jb2RlZCBjaXBoZXJ0ZXh0Li4u + CardPinSessionRequest: + type: object + required: + - targetOrigin + properties: + targetOrigin: + type: string + format: uri + description: Canonical HTTPS origin of the page that will frame the returned `pinEmbedUrl`. The processor refuses to render the iframe on any other origin, so this must match the embedding page's origin exactly — scheme and host, no path or trailing slash. + example: https://app.example.com + CardPinSessionResponse: + type: object + required: + - sessionToken + - expiresAt + properties: + sessionToken: + type: string + description: |- + Opaque, short-lived credential that loads the card processor's PIN-entry iframe. Hand it to the processor's embed SDK in your client: the cardholder types the PIN directly into the processor's iframe, so it crosses neither your servers nor Grid's. + + Treat it as a bearer secret — use it immediately, and never store, cache, or log it. It is a session credential, not a URL; Grid returns no URL because the processor's embed SDK builds one from this token. + + Grid does not observe whether the cardholder completed PIN entry. Read `pinStatus` on the `Card` afterwards to confirm. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + expiresAt: + type: string + format: date-time + description: When the session token stops working. Request a new session rather than reusing an expired token. + example: '2026-05-08T14:16:00Z' SandboxCardAuthorizationRequest: type: object required: diff --git a/openapi.yaml b/openapi.yaml index edd1c5018..d78362e98 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -8913,6 +8913,222 @@ paths: application/json: schema: $ref: '#/components/schemas/Error501' + /cards/{id}/pin: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Set a card PIN + description: |- + Set or replace the card's PIN from a PIN block your client encrypted for the card processor. Grid forwards the block verbatim and never sees the PIN: it is not stored, not logged, and not readable by Grid at any point. + + Use this when you operate your own PIN-entry UI. To hand PIN entry to the processor's hosted iframe instead, use `POST /cards/{id}/pin/session` — the two are alternatives, and a card may use either at any time. + + Setting a PIN clears a `BLOCKED` PIN, so a cardholder locked out by incorrect attempts can recover by choosing a new PIN without a separate unblock. On success the card's `pinStatus` becomes `OK`. + + Every call is audit-logged with the requesting actor. + operationId: setCardPin + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSetRequest' + responses: + '204': + description: PIN set. + '400': + description: Bad request. Returned with `INVALID_INPUT` when `encryptedPinBlock` is absent, is not valid base64, or is rejected by the card processor as a malformed or mis-encrypted PIN block. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the change against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/pin/session: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Create a PIN-entry session + description: |- + Mint a short-lived session token for the card processor's PIN-entry iframe, so the cardholder can choose their own PIN without the PIN crossing your servers or Grid's. + + Request the session right before rendering the iframe and hand the returned `sessionToken` to the processor's embed SDK immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log it. Grid returns no URL — the processor's embed SDK builds one from the token. + + PIN entry is fixed at 4 digits, matching `POST /cards/{id}/pin`. + + Grid is not told whether the cardholder completed PIN entry. Read `pinStatus` on the `Card` after the session to confirm the outcome. + + If you operate your own PIN-entry UI, use `POST /cards/{id}/pin` instead. + + Every session minted is audit-logged with the requesting actor. + operationId: createCardPinSession + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSessionRequest' + responses: + '200': + description: PIN-entry session minted. + content: + application/json: + schema: + $ref: '#/components/schemas/CardPinSessionResponse' + '400': + description: Bad request. Returned with `INVALID_INPUT` when `targetOrigin` is absent or is not a canonical HTTPS origin. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the mint against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/pin/unblock: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Unblock a card PIN + description: |- + Clear a `BLOCKED` PIN, restoring the cardholder's existing PIN without changing it. A PIN blocks after three consecutive incorrect entries; until it is cleared, PIN-verified transactions decline. + + Unblocking is idempotent and safe to call on a card whose `pinStatus` is already `OK`. It has no effect on a card with no PIN set, which returns `409` — set a PIN with `POST /cards/{id}/pin` instead. + + Every call is audit-logged with the requesting actor. + operationId: unblockCardPin + tags: + - Cards + security: + - BasicAuth: [] + responses: + '204': + description: PIN unblocked. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the change against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, with `PIN_NOT_SET` for a card that has no PIN to unblock, and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does not offer PIN management. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' /sandbox/cards/{id}/simulate/authorization: post: summary: Simulate a card authorization @@ -25915,6 +26131,22 @@ components: description: | Physical form factor of the card. Only `VIRTUAL` is supported in v1; `PHYSICAL` will be added in a later release. + CardPinStatus: + type: string + enum: + - NOT_SET + - OK + - BLOCKED + example: OK + description: | + State of the card's PIN. Absent on cards whose processor does not offer + PIN management. + + | Status | Description | + |--------|-------------| + | `NOT_SET` | No PIN has been set on the card. PIN-verified transactions decline. Set one with `POST /cards/{id}/pin` or `POST /cards/{id}/pin/session`. | + | `OK` | A PIN is set and usable. | + | `BLOCKED` | The PIN was entered incorrectly three consecutive times and is refused until cleared with `POST /cards/{id}/pin/unblock`, or replaced with `POST /cards/{id}/pin`. | Card: type: object required: @@ -25952,6 +26184,8 @@ components: $ref: '#/components/schemas/CardBrand' form: $ref: '#/components/schemas/CardForm' + pinStatus: + $ref: '#/components/schemas/CardPinStatus' last4: type: string description: Last four digits of the card PAN. @@ -26137,6 +26371,52 @@ components: format: date-time description: When the signed URL stops loading. Request a new reveal rather than re-rendering an expired URL. example: '2026-05-08T14:16:00Z' + CardPinSetRequest: + type: object + required: + - encryptedPinBlock + properties: + encryptedPinBlock: + type: string + description: |- + The cardholder's PIN, encrypted for the card processor. Grid forwards this value to the processor verbatim and can neither read nor validate it — no PIN, in plaintext or ciphertext, is ever stored or logged by Grid. + + Build it in the client, immediately after PIN entry, so the PIN never reaches your servers either: serialize `{"nonce": , "pin": "<4-digit PIN>"}` as UTF-8, encrypt it under the processor's public key, and base64-encode the ciphertext. Generate a fresh `nonce` per request — it is what makes a captured block useless to replay. + + A block the processor rejects returns `400 INVALID_INPUT`; Grid cannot distinguish a malformed block from a mis-encrypted one, so verify your encryption against the processor's documentation rather than against Grid's response. + minLength: 16 + maxLength: 1024 + example: SGVsbG8sIHRoaXMgaXMgYSBiYXNlNjQtZW5jb2RlZCBjaXBoZXJ0ZXh0Li4u + CardPinSessionRequest: + type: object + required: + - targetOrigin + properties: + targetOrigin: + type: string + format: uri + description: Canonical HTTPS origin of the page that will frame the returned `pinEmbedUrl`. The processor refuses to render the iframe on any other origin, so this must match the embedding page's origin exactly — scheme and host, no path or trailing slash. + example: https://app.example.com + CardPinSessionResponse: + type: object + required: + - sessionToken + - expiresAt + properties: + sessionToken: + type: string + description: |- + Opaque, short-lived credential that loads the card processor's PIN-entry iframe. Hand it to the processor's embed SDK in your client: the cardholder types the PIN directly into the processor's iframe, so it crosses neither your servers nor Grid's. + + Treat it as a bearer secret — use it immediately, and never store, cache, or log it. It is a session credential, not a URL; Grid returns no URL because the processor's embed SDK builds one from this token. + + Grid does not observe whether the cardholder completed PIN entry. Read `pinStatus` on the `Card` afterwards to confirm. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + expiresAt: + type: string + format: date-time + description: When the session token stops working. Request a new session rather than reusing an expired token. + example: '2026-05-08T14:16:00Z' SandboxCardAuthorizationRequest: type: object required: diff --git a/openapi/components/schemas/cards/Card.yaml b/openapi/components/schemas/cards/Card.yaml index f0628123c..e7775b0a2 100644 --- a/openapi/components/schemas/cards/Card.yaml +++ b/openapi/components/schemas/cards/Card.yaml @@ -38,6 +38,8 @@ properties: $ref: ./CardBrand.yaml form: $ref: ./CardForm.yaml + pinStatus: + $ref: ./CardPinStatus.yaml last4: type: string description: Last four digits of the card PAN. diff --git a/openapi/components/schemas/cards/CardPinSessionRequest.yaml b/openapi/components/schemas/cards/CardPinSessionRequest.yaml new file mode 100644 index 000000000..aaf4e4a94 --- /dev/null +++ b/openapi/components/schemas/cards/CardPinSessionRequest.yaml @@ -0,0 +1,13 @@ +type: object +required: + - targetOrigin +properties: + targetOrigin: + type: string + format: uri + description: >- + Canonical HTTPS origin of the page that will frame the returned + `pinEmbedUrl`. The processor refuses to render the iframe on any other + origin, so this must match the embedding page's origin exactly — + scheme and host, no path or trailing slash. + example: https://app.example.com diff --git a/openapi/components/schemas/cards/CardPinSessionResponse.yaml b/openapi/components/schemas/cards/CardPinSessionResponse.yaml new file mode 100644 index 000000000..d5d73256a --- /dev/null +++ b/openapi/components/schemas/cards/CardPinSessionResponse.yaml @@ -0,0 +1,29 @@ +type: object +required: + - sessionToken + - expiresAt +properties: + sessionToken: + type: string + description: >- + Opaque, short-lived credential that loads the card processor's + PIN-entry iframe. Hand it to the processor's embed SDK in your client: + the cardholder types the PIN directly into the processor's iframe, so + it crosses neither your servers nor Grid's. + + + Treat it as a bearer secret — use it immediately, and never store, + cache, or log it. It is a session credential, not a URL; Grid returns + no URL because the processor's embed SDK builds one from this token. + + + Grid does not observe whether the cardholder completed PIN entry. Read + `pinStatus` on the `Card` afterwards to confirm. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + expiresAt: + type: string + format: date-time + description: >- + When the session token stops working. Request a new session rather + than reusing an expired token. + example: '2026-05-08T14:16:00Z' diff --git a/openapi/components/schemas/cards/CardPinSetRequest.yaml b/openapi/components/schemas/cards/CardPinSetRequest.yaml new file mode 100644 index 000000000..c48452241 --- /dev/null +++ b/openapi/components/schemas/cards/CardPinSetRequest.yaml @@ -0,0 +1,27 @@ +type: object +required: + - encryptedPinBlock +properties: + encryptedPinBlock: + type: string + description: >- + The cardholder's PIN, encrypted for the card processor. Grid forwards + this value to the processor verbatim and can neither read nor validate + it — no PIN, in plaintext or ciphertext, is ever stored or logged by + Grid. + + + Build it in the client, immediately after PIN entry, so the PIN never + reaches your servers either: serialize `{"nonce": , + "pin": "<4-digit PIN>"}` as UTF-8, encrypt it under the processor's + public key, and base64-encode the ciphertext. Generate a fresh `nonce` + per request — it is what makes a captured block useless to replay. + + + A block the processor rejects returns `400 INVALID_INPUT`; Grid cannot + distinguish a malformed block from a mis-encrypted one, so verify your + encryption against the processor's documentation rather than against + Grid's response. + minLength: 16 + maxLength: 1024 + example: SGVsbG8sIHRoaXMgaXMgYSBiYXNlNjQtZW5jb2RlZCBjaXBoZXJ0ZXh0Li4u diff --git a/openapi/components/schemas/cards/CardPinStatus.yaml b/openapi/components/schemas/cards/CardPinStatus.yaml new file mode 100644 index 000000000..91ed0736d --- /dev/null +++ b/openapi/components/schemas/cards/CardPinStatus.yaml @@ -0,0 +1,15 @@ +type: string +enum: + - NOT_SET + - OK + - BLOCKED +example: OK +description: | + State of the card's PIN. Absent on cards whose processor does not offer + PIN management. + + | Status | Description | + |--------|-------------| + | `NOT_SET` | No PIN has been set on the card. PIN-verified transactions decline. Set one with `POST /cards/{id}/pin` or `POST /cards/{id}/pin/session`. | + | `OK` | A PIN is set and usable. | + | `BLOCKED` | The PIN was entered incorrectly three consecutive times and is refused until cleared with `POST /cards/{id}/pin/unblock`, or replaced with `POST /cards/{id}/pin`. | diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index de980b707..616836053 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -348,6 +348,12 @@ paths: $ref: paths/cards/cards_{id}.yaml /cards/{id}/reveal: $ref: paths/cards/cards_{id}_reveal.yaml + /cards/{id}/pin: + $ref: paths/cards/cards_{id}_pin.yaml + /cards/{id}/pin/session: + $ref: paths/cards/cards_{id}_pin_session.yaml + /cards/{id}/pin/unblock: + $ref: paths/cards/cards_{id}_pin_unblock.yaml /sandbox/cards/{id}/simulate/authorization: $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_authorization.yaml /sandbox/cards/{id}/simulate/clearing: diff --git a/openapi/paths/cards/cards_{id}_pin.yaml b/openapi/paths/cards/cards_{id}_pin.yaml new file mode 100644 index 000000000..b6adb860d --- /dev/null +++ b/openapi/paths/cards/cards_{id}_pin.yaml @@ -0,0 +1,94 @@ +parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string +post: + summary: Set a card PIN + description: >- + Set or replace the card's PIN from a PIN block your client encrypted for + the card processor. Grid forwards the block verbatim and never sees the + PIN: it is not stored, not logged, and not readable by Grid at any point. + + + Use this when you operate your own PIN-entry UI. To hand PIN entry to + the processor's hosted iframe instead, use + `POST /cards/{id}/pin/session` — the two are alternatives, and a card may + use either at any time. + + + Setting a PIN clears a `BLOCKED` PIN, so a cardholder locked out by + incorrect attempts can recover by choosing a new PIN without a separate + unblock. On success the card's `pinStatus` becomes `OK`. + + + Every call is audit-logged with the requesting actor. + operationId: setCardPin + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/cards/CardPinSetRequest.yaml + responses: + '204': + description: PIN set. + '400': + description: >- + Bad request. Returned with `INVALID_INPUT` when `encryptedPinBlock` + is absent, is not valid base64, or is rejected by the card + processor as a malformed or mis-encrypted PIN block. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '403': + description: >- + Forbidden. The session has no attributable actor to audit the change + against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error403.yaml + '404': + description: Card not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, + and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does + not offer PIN management. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml + '501': + description: >- + Not implemented in this environment. Cards are not enabled for + every Grid deployment; environments without a configured card + issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error501.yaml diff --git a/openapi/paths/cards/cards_{id}_pin_session.yaml b/openapi/paths/cards/cards_{id}_pin_session.yaml new file mode 100644 index 000000000..39eb6622c --- /dev/null +++ b/openapi/paths/cards/cards_{id}_pin_session.yaml @@ -0,0 +1,104 @@ +parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string +post: + summary: Create a PIN-entry session + description: >- + Mint a short-lived session token for the card processor's PIN-entry + iframe, so the cardholder can choose their own PIN without the PIN + crossing your servers or Grid's. + + + Request the session right before rendering the iframe and hand the + returned `sessionToken` to the processor's embed SDK immediately; it + expires at `expiresAt` (within minutes). Never store, cache, or log it. + Grid returns no URL — the processor's embed SDK builds one from the + token. + + + PIN entry is fixed at 4 digits, matching `POST /cards/{id}/pin`. + + + Grid is not told whether the cardholder completed PIN entry. Read + `pinStatus` on the `Card` after the session to confirm the outcome. + + + If you operate your own PIN-entry UI, use `POST /cards/{id}/pin` + instead. + + + Every session minted is audit-logged with the requesting actor. + operationId: createCardPinSession + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/cards/CardPinSessionRequest.yaml + responses: + '200': + description: PIN-entry session minted. + content: + application/json: + schema: + $ref: ../../components/schemas/cards/CardPinSessionResponse.yaml + '400': + description: >- + Bad request. Returned with `INVALID_INPUT` when `targetOrigin` is + absent or is not a canonical HTTPS origin. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '403': + description: >- + Forbidden. The session has no attributable actor to audit the mint + against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error403.yaml + '404': + description: Card not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, + and with `UNSUPPORTED_CARD_OPERATION` when the card's processor does + not offer PIN management. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml + '501': + description: >- + Not implemented in this environment. Cards are not enabled for + every Grid deployment; environments without a configured card + issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error501.yaml diff --git a/openapi/paths/cards/cards_{id}_pin_unblock.yaml b/openapi/paths/cards/cards_{id}_pin_unblock.yaml new file mode 100644 index 000000000..af7c0bf18 --- /dev/null +++ b/openapi/paths/cards/cards_{id}_pin_unblock.yaml @@ -0,0 +1,74 @@ +parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string +post: + summary: Unblock a card PIN + description: >- + Clear a `BLOCKED` PIN, restoring the cardholder's existing PIN without + changing it. A PIN blocks after three consecutive incorrect entries; + until it is cleared, PIN-verified transactions decline. + + + Unblocking is idempotent and safe to call on a card whose `pinStatus` is + already `OK`. It has no effect on a card with no PIN set, which returns + `409` — set a PIN with `POST /cards/{id}/pin` instead. + + + Every call is audit-logged with the requesting actor. + operationId: unblockCardPin + tags: + - Cards + security: + - BasicAuth: [] + responses: + '204': + description: PIN unblocked. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '403': + description: >- + Forbidden. The session has no attributable actor to audit the change + against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error403.yaml + '404': + description: Card not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Conflict. Returned with `CARD_ALREADY_CLOSED` for a `CLOSED` card, + with `PIN_NOT_SET` for a card that has no PIN to unblock, and with + `UNSUPPORTED_CARD_OPERATION` when the card's processor does not + offer PIN management. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml + '501': + description: >- + Not implemented in this environment. Cards are not enabled for + every Grid deployment; environments without a configured card + issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error501.yaml