Merchant API
version: 1.0.0
A public address for your backend is required to use the Merchant API. Specify the address in the Callback URL field in the Yandex Split console on the developer page.
Your backend must authorize the request before it starts processing it.
Warning
Do not bind your logic to specific IP addresses — they may change. For request authorization, run JWT token validation with public keys.
Authorizing Yandex requests
In the response body, Yandex delivers a JWT token signed by the ES256 algorithm.
The token consists of three parts: the header, the payload, and the signature
concatenated with a '.': <base64-encoded headers JSON>.<base64-encoded payload JSON>.<signature>.
The base64-decoded header is a JSON document with the following structure:
{
"alg": "ES256", // Signature algorithm
"kid": "key-id", // Key ID; used when identifying a public key
"iat": "1639408318", // UNIX time when the token was issued
"exp": "1639429918", // UNIX time; when the token expires, it's invalidated
"typ": "JWT" // Token type
}
The payload is also JSON, with the specific field structure determined by the called method. The signature is needed to validate the JWT token.
Warning
Before deserializing a token, you need to validate it.
Token validation algorithm
To validate the JWT token, use one of the standard libraries from the list: https://jwt.io/libraries. Avoid manual implementation of such checks.
Generally speaking, the algorithm for validating a JWT token via a library includes:
- Validating the signature of a JWT token using public JSON Web Keys (JWK) available at:
https://sandbox.pay.yandex.uz/api/jwks for the test environment and https://pay.yandex.uz/api/jwks
for the production environment. The public key used for validating the signature of a specific JWT token is selected
based on the
algandkidin the token's header. - Checking standard requirements for the JWT token header:
alg,typ,iat,exp, etc.
Only if both checks succeed can the recipient deserialize the JSON payload of the JWT token. After that, the recipient should additionally check that merchantId value in the token's payload is the same as the merchant ID in Yandex Split. Otherwise, the token cannot be used.
If any of the checks fails, the token is considered invalid and the request is considered unauthenticated. Such a request should be rejected. The error reasonCode depends on the cause:
TOKEN_EXPIRED: The token has expired.UNAUTHORIZED: Couldn't validate the token's signature.OTHER: Other validation errors.
Response examples:
{
"status": "fail",
"reasonCode": "TOKEN_EXPIRED",
"reason": "JWT token has expired"
}
{
"status": "fail",
"reasonCode": "UNAUTHORIZED",
"reason": "Invalid token signature"
}
{
"status": "fail",
"reasonCode": "OTHER",
"reason": "Invalid merchantId"
}
Caching of public JWK keys
You may cache JWK keys used to validate JWT tokens in the store backend. For such a cache, the TTL has to be set to avoid token validation errors when JWKs are rotated on the Yandex Split side. For practical reasons, some libraries set a TTL of 10 minutes for such a cache by default. If you cache JWK keys, implement the following validation scenarios.
Scenario А: Successful validation of a token using cached JWKs.
- The backend receives a request with the token whose
kidis found among the cached JWKs and whose TTL for the cache hasn't expired. - The token's signature is validated successfully.
- The token is validated.
Scenario B: Unsuccessful validation of a token using cached JWKs.
- The backend receives a request with the token whose
kidis found among the cached JWKs and whose TTL for the cache hasn't expired. - The token's signature validation fails.
- The token is invalidated, the request is rejected.
Scenario C: Token validation with the JWK cache reset.
- The backend receives a request with a token whose
kidisn't among the cached JWKs or the TTL for the cache has expired. - The merchant has to reset the JWK cache and request a fresh complete list of active JWKs from the address corresponding to the environment.
- The validation process continues by Scenario A or Scenario B.
Same as with token validation, we recommend using standard libraries and avoiding manual implementation of the scenarios.
Code example of JWT token validation
Below is an example of successful validation of a JWT token issued in the test environment.
To validate tokens in the production environment, use public keys available at https://pay.yandex.uz/api/jwks.
import json
from urllib.request import urlopen
from jose import jwt
YANDEX_JWK_ENDPOINT = "https://sandbox.pay.yandex.uz/api/jwks"
JWT_TOKEN = (
"eyJhbGciOiJFUzI1NiIsImlhdCI6MTY1MDE5Njc1OCwia2lkIjoidGVzdC1rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXJ0Ijp7Iml0ZW1zIjpbeyJwcm9kdWN"
"0SWQiOiJwMSIsInF1YW50aXR5Ijp7ImNvdW50IjoiMSJ9fV19LCJjdXJyZW5jeUNvZGUiOiJSVUIiLCJtZXJjaGFudElkIjoiMjc2Y2YxZjEtZjhlZC00N"
"GZlLTg5ZTMtNWU0MTEzNDZkYThkIn0.YmQjHlh3ddLWgBexQ3QrwtbgAA3u1TVnBl1qnfMIvToBwinH3uH92KGB15m4NAQXdz5nhkjPZZu7RUStJt40PQ"
)
with urlopen(YANDEX_JWK_ENDPOINT) as response:
public_jwks = json.load(response)
payload = jwt.decode(JWT_TOKEN, public_jwks, algorithms=["ES256"])
print(json.dumps(payload, indent=2))
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$sandboxJwk = 'https://sandbox.pay.yandex.uz/api/jwks';
$JWT_TOKEN =
"eyJhbGciOiJFUzI1NiIsImlhdCI6MTY1MDE5Njc1OCwia2lkIjoidGVzdC1rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXJ0Ijp7Iml0ZW1zIjpbeyJwcm9kdWN"
. "0SWQiOiJwMSIsInF1YW50aXR5Ijp7ImNvdW50IjoiMSJ9fV19LCJjdXJyZW5jeUNvZGUiOiJSVUIiLCJtZXJjaGFudElkIjoiMjc2Y2YxZjEtZjhlZC00N"
. "GZlLTg5ZTMtNWU0MTEzNDZkYThkIn0.YmQjHlh3ddLWgBexQ3QrwtbgAA3u1TVnBl1qnfMIvToBwinH3uH92KGB15m4NAQXdz5nhkjPZZu7RUStJt40PQ";
$client = new GuzzleHttp\Client();
$keysJson = $client->get($sandboxJwk)->getBody();
$keysData = json_decode($keysJson, JSON_OBJECT_AS_ARRAY);
$keys = JWK::parseKeySet($keysData);
$payload = JWT::decode($JWT_TOKEN, $keys);
print_r($payload);
To run the example, install the required dependencies:
composer require firebase/php-jwt
composer require guzzlehttp/guzzle
Troubleshooting webhook issues
If no notifications are received, check the following:
-
Whether the right backend URL is specified
Notifications may be sent to an unexpected location. Be sure to specify the Callback URL without
/v1/webhook— this path is added automatically. For example:Callback URL
Where the request will be sent
https://example.merchant.uzhttps://example.merchant.uz/v1/webhookhttps://example.merchant.uz/v1/webhookhttps://example.merchant.uz/v1/webhook/v1/webhook -
Handling
Content-TypeMake sure that your store's backend can accept messages with the
Content-Type: application/octet-streamheader. -
SSL certificate
The system can't read self-signed SSL certificates. Use a certificate issued by a trusted certification authority.
-
Firewall settings
Check that the firewall doesn't block incoming requests or truncate the request body.
Endpoints
Specification
Open API
{
"components": {
"schemas": {
"MerchantErrorResponse": {
"properties": {
"reason": {
"description": "Error cause description.",
"type": "string"
},
"reasonCode": {
"description": "Error code:\n\n- `FORBIDDEN`: The order exists, but it was not paid for via Yandex Split.\n- `ORDER_NOT_FOUND`: No order found in the merchant system.\n- `ORDER_AMOUNT_MISMATCH`: The order amount does not match the amount in the merchant system.\n- `ORDER_DETAILS_MISMATCH`: The order details differ from those in the merchant system.\n- `OTHER`: Generic error.\n- `UNAUTHORIZED`: JWT signature validation failed.\n- `TOKEN_EXPIRED`: The JWT token has expired.\n- `CONFLICT`: The notification data conflicts with the order state in the merchant system. For example, a notification about successful payment for a canceled order is received.",
"enum": [
"FORBIDDEN",
"ITEM_NOT_FOUND",
"ORDER_NOT_FOUND",
"ORDER_AMOUNT_MISMATCH",
"ORDER_DETAILS_MISMATCH",
"OUT_OF_INVENTORY",
"PICKUP_POINT_NOT_FOUND",
"SHIPPING_DETAILS_MISMATCH",
"OTHER",
"UNAUTHORIZED",
"TOKEN_EXPIRED",
"CONFLICT"
],
"type": "string"
},
"status": {
"default": "fail",
"type": "string"
}
},
"required": [
"reasonCode"
],
"type": "object"
},
"MerchantSuccessResponse": {
"properties": {
"status": {
"default": "success",
"type": "string"
}
},
"type": "object"
},
"MerchantWebhookV1Request": {
"properties": {
"event": {
"description": "Event type:\n- `ORDER_STATUS_UPDATED`: Update of the order status.\n- `OPERATION_STATUS_UPDATED`: Status updated for charge, refund, or cancellation operations.",
"enum": [
"TRANSACTION_STATUS_UPDATE",
"ORDER_STATUS_UPDATED",
"OPERATION_STATUS_UPDATED",
"SUBSCRIPTION_STATUS_UPDATED"
],
"type": "string"
},
"eventTime": {
"description": "Event time in `RFC 3339`: `YYYY-MM-DDThh:mm:ssTZD` format.",
"example": "2025-05-26T21:00:36.08847+00:00",
"format": "date-time",
"type": "string"
},
"merchantId": {
"description": "Merchant ID.",
"format": "uuid",
"type": "string"
},
"operation": {
"allOf": [
{
"properties": {
"externalOperationId": {
"description": "Operation ID in the merchant system. Make sure it's unique.\n\nPass this parameter to track a specific operation by calling the [v1/operations/{external_operation_id}](../yandex-pay-api/operation/merchant_v1_operations-get) method.",
"type": "string"
},
"operationId": {
"description": "Operation ID.",
"example": "5d32f295-8723-457d-81f9-ab13f17b7bd6",
"format": "uuid",
"type": "string"
},
"operationType": {
"description": "Operation type. To learn more about operation types, see [Operation statuses](../../../payments/statuses).",
"enum": [
"AUTHORIZE",
"BIND_CARD",
"REFUND",
"CAPTURE",
"VOID",
"RECURRING",
"PREPAYMENT",
"SUBMIT"
],
"type": "string"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"status": {
"description": "Operation status. To learn more about operation statuses, see [Operation statuses](../../../payments/statuses).",
"enum": [
"PENDING",
"SUCCESS",
"FAIL"
],
"type": "string"
}
},
"required": [
"operationId",
"operationType",
"orderId",
"status"
],
"type": "object"
}
],
"description": "Operation details. Received along with the `OPERATION_STATUS_UPDATED` event"
},
"order": {
"allOf": [
{
"properties": {
"cartUpdated": {
"description": "Shows whether the cart has been updated. Returned if the order is paid for with Plus points. If set to `true`, the current cart info is provided.",
"type": "boolean"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"paymentStatus": {
"description": "Order status. For more information, see [Order status](../../../payments/statuses.md).",
"enum": [
"PENDING",
"AUTHORIZED",
"CAPTURED",
"VOIDED",
"REFUNDED",
"CONFIRMED",
"PARTIALLY_REFUNDED",
"FAILED"
],
"type": "string",
"x-enumDescriptions": {
"AUTHORIZED": "Платеж за заказ авторизован. Средства заблокированы на счету плательщика",
"CAPTURED": "Заказ успешно оплачен. Средства списаны со счета плательщика",
"CONFIRMED": "Заказ успешно оформлен",
"FAILED": "Заказ не был успешно оплачен",
"PARTIALLY_REFUNDED": "Совершён частичный возврат средств за заказ",
"PENDING": "Ожидается оплата",
"REFUNDED": "Совершён возврат средств за заказ",
"VOIDED": "Оплата отменена (voided). Списание средств не производилось"
}
}
},
"required": [
"orderId",
"paymentStatus"
],
"type": "object"
}
],
"description": "Order details. Received along with the `ORDER_STATUS_UPDATED` event"
},
"subscription": {
"allOf": [
{
"properties": {
"customerSubscriptionId": {
"description": "Subscription ID. The SDK returns it when the subscription is created successfully. You can also save a subscription when you get the first notification on it. The same value will arrive in this field every time the subscription is updated.",
"format": "uuid",
"type": "string"
},
"nextWriteOff": {
"description": "Date of the next attempt to debit a subscription fee",
"format": "date-time",
"type": "string"
},
"status": {
"description": "Subscription type",
"enum": [
"NEW",
"ACTIVE",
"CANCELLED",
"EXPIRED"
],
"type": "string"
},
"subscriptionPlanId": {
"description": "ID of the subscription plan created in the dashboard or over the API.",
"format": "uuid",
"type": "string"
}
},
"required": [
"customerSubscriptionId",
"status",
"subscriptionPlanId"
],
"type": "object"
}
],
"description": "Subscription status."
}
},
"required": [
"event",
"eventTime",
"merchantId"
],
"type": "object"
},
"OperationWebhookData": {
"properties": {
"externalOperationId": {
"description": "Operation ID in the merchant system. Make sure it's unique.\n\nPass this parameter to track a specific operation by calling the [v1/operations/{external_operation_id}](../yandex-pay-api/operation/merchant_v1_operations-get) method.",
"type": "string"
},
"operationId": {
"description": "Operation ID.",
"example": "5d32f295-8723-457d-81f9-ab13f17b7bd6",
"format": "uuid",
"type": "string"
},
"operationType": {
"description": "Operation type. To learn more about operation types, see [Operation statuses](../../../payments/statuses).",
"enum": [
"AUTHORIZE",
"BIND_CARD",
"REFUND",
"CAPTURE",
"VOID",
"RECURRING",
"PREPAYMENT",
"SUBMIT"
],
"type": "string"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"status": {
"description": "Operation status. To learn more about operation statuses, see [Operation statuses](../../../payments/statuses).",
"enum": [
"PENDING",
"SUCCESS",
"FAIL"
],
"type": "string"
}
},
"required": [
"operationId",
"operationType",
"orderId",
"status"
],
"type": "object"
},
"OrderWebhookData": {
"properties": {
"cartUpdated": {
"description": "Shows whether the cart has been updated. Returned if the order is paid for with Plus points. If set to `true`, the current cart info is provided.",
"type": "boolean"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"paymentStatus": {
"description": "Order status. For more information, see [Order status](../../../payments/statuses.md).",
"enum": [
"PENDING",
"AUTHORIZED",
"CAPTURED",
"VOIDED",
"REFUNDED",
"CONFIRMED",
"PARTIALLY_REFUNDED",
"FAILED"
],
"type": "string",
"x-enumDescriptions": {
"AUTHORIZED": "Платеж за заказ авторизован. Средства заблокированы на счету плательщика",
"CAPTURED": "Заказ успешно оплачен. Средства списаны со счета плательщика",
"CONFIRMED": "Заказ успешно оформлен",
"FAILED": "Заказ не был успешно оплачен",
"PARTIALLY_REFUNDED": "Совершён частичный возврат средств за заказ",
"PENDING": "Ожидается оплата",
"REFUNDED": "Совершён возврат средств за заказ",
"VOIDED": "Оплата отменена (voided). Списание средств не производилось"
}
}
},
"required": [
"orderId",
"paymentStatus"
],
"type": "object"
},
"SubscriptionWebhookData": {
"properties": {
"customerSubscriptionId": {
"description": "Subscription ID. The SDK returns it when the subscription is created successfully. You can also save a subscription when you get the first notification on it. The same value will arrive in this field every time the subscription is updated.",
"format": "uuid",
"type": "string"
},
"nextWriteOff": {
"description": "Date of the next attempt to debit a subscription fee",
"format": "date-time",
"type": "string"
},
"status": {
"description": "Subscription type",
"enum": [
"NEW",
"ACTIVE",
"CANCELLED",
"EXPIRED"
],
"type": "string"
},
"subscriptionPlanId": {
"description": "ID of the subscription plan created in the dashboard or over the API.",
"format": "uuid",
"type": "string"
}
},
"required": [
"customerSubscriptionId",
"status",
"subscriptionPlanId"
],
"type": "object"
}
}
},
"info": {
"description": "A public address for your backend is required to use the Merchant API. [Specify the address](../../../console/settings-pay-split.md#callback-settings) in the **Callback URL** field in the Yandex Split console on the [developer page](https://console.pay.yandex.uz/settings).\n\nYour backend must authorize the request before it starts processing it.\n\n{% note warning %}\n\nDo not bind your logic to specific IP addresses — they may change. For request authorization, run [JWT token validation](#validate) with public keys.\n\n{% endnote %}\n\n## Authorizing Yandex requests {#auth}\n\nIn the response body, Yandex delivers a JWT token signed by the ES256 algorithm.\nThe token consists of three parts: the header, the payload, and the signature \nconcatenated with a '.': `<base64-encoded headers JSON>.<base64-encoded payload JSON>.<signature>`.\nThe base64-decoded header is a JSON document with the following structure:\n\n```json\n{\n \"alg\": \"ES256\", // Signature algorithm\n \"kid\": \"key-id\", // Key ID; used when identifying a public key\n \"iat\": \"1639408318\", // UNIX time when the token was issued\n \"exp\": \"1639429918\", // UNIX time; when the token expires, it's invalidated\n \"typ\": \"JWT\" // Token type\n}\n```\n\nThe payload is also JSON, with the specific field structure determined by\nthe called method. The signature is needed to validate the JWT token.\n\n{% note warning %}\n\nBefore deserializing a token, you need to validate it.\n\n{% endnote %}\n\n### Token validation algorithm {#validate}\n\nTo validate the JWT token, use one of the standard libraries\nfrom the list: <https://jwt.io/libraries>. Avoid manual implementation of such checks.\n\nGenerally speaking, the algorithm for validating a JWT token via a library includes:\n\n1. Validating the signature of a JWT token using public JSON Web Keys (JWK) available at:\n<https://sandbox.pay.yandex.uz/api/jwks> for the test environment and <https://pay.yandex.uz/api/jwks>\nfor the production environment. The public key used for validating the signature of a specific JWT token is selected\nbased on the `alg` and `kid` in the token's header.\n1. Checking standard requirements for the JWT token header: `alg`, `typ`, `iat`, `exp`, etc.\n\nOnly if both checks succeed can the recipient deserialize the JSON payload of the JWT token. After that, the recipient should additionally check that `merchantId` value in the token's payload is the same as the merchant ID in Yandex Split. Otherwise, the token cannot be used.\n\nIf any of the checks fails, the token is considered invalid and the request is considered unauthenticated. Such a request should be rejected. The error `reasonCode` depends on the cause:\n\n- `TOKEN_EXPIRED`: The token has expired.\n- `UNAUTHORIZED`: Couldn't validate the token's signature.\n- `OTHER`: Other validation errors.\n\nResponse examples:\n\n```json\n{\n \"status\": \"fail\",\n \"reasonCode\": \"TOKEN_EXPIRED\",\n \"reason\": \"JWT token has expired\"\n}\n```\n\n```json\n{\n \"status\": \"fail\",\n \"reasonCode\": \"UNAUTHORIZED\",\n \"reason\": \"Invalid token signature\"\n}\n```\n\n```json\n{\n \"status\": \"fail\",\n \"reasonCode\": \"OTHER\",\n \"reason\": \"Invalid merchantId\"\n}\n```\n\n#### Caching of public JWK keys {#jwk-cache}\n\nYou may cache JWK keys used to validate JWT tokens in the store backend.\nFor such a cache, the TTL has to be set to avoid token validation errors\nwhen JWKs are rotated on the Yandex Split side. For practical reasons,\n[some libraries](https://github.com/auth0/node-jwks-rsa#caching) set\na TTL of 10 minutes for such a cache by default. If you cache JWK keys, implement the following\nvalidation scenarios.\n\n**Scenario А:** Successful validation of a token using cached JWKs.\n\n1. The backend receives a request with the token whose `kid` is found among the cached JWKs and whose TTL for the cache hasn't expired.\n1. The token's signature is validated successfully.\n1. The token is validated.\n\n**Scenario B:** Unsuccessful validation of a token using cached JWKs.\n\n1. The backend receives a request with the token whose `kid` is found among the cached JWKs and whose TTL for the cache hasn't expired.\n1. The token's signature validation fails.\n1. The token is invalidated, the request is rejected.\n\n**Scenario C:** Token validation with the JWK cache reset.\n\n1. The backend receives a request with a token whose `kid` isn't among the cached JWKs or the TTL for the cache has expired.\n1. The merchant has to reset the JWK cache and request a fresh complete list of active JWKs from the address corresponding to the environment.\n1. The validation process continues by Scenario A or Scenario B.\n\nSame as with token validation, we recommend using standard libraries and avoiding\nmanual implementation of the scenarios.\n\n### Code example of JWT token validation {#jwk-verify-example}\n\nBelow is an example of successful validation of a JWT token issued in the test environment.\nTo validate tokens in the production environment, use public keys available at `https://pay.yandex.uz/api/jwks`.\n\n{% list tabs %}\n\n- Python\n\n ```python\n import json\n from urllib.request import urlopen\n\n from jose import jwt\n\n YANDEX_JWK_ENDPOINT = \"https://sandbox.pay.yandex.uz/api/jwks\"\n\n JWT_TOKEN = (\n \"eyJhbGciOiJFUzI1NiIsImlhdCI6MTY1MDE5Njc1OCwia2lkIjoidGVzdC1rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXJ0Ijp7Iml0ZW1zIjpbeyJwcm9kdWN\"\n \"0SWQiOiJwMSIsInF1YW50aXR5Ijp7ImNvdW50IjoiMSJ9fV19LCJjdXJyZW5jeUNvZGUiOiJSVUIiLCJtZXJjaGFudElkIjoiMjc2Y2YxZjEtZjhlZC00N\"\n \"GZlLTg5ZTMtNWU0MTEzNDZkYThkIn0.YmQjHlh3ddLWgBexQ3QrwtbgAA3u1TVnBl1qnfMIvToBwinH3uH92KGB15m4NAQXdz5nhkjPZZu7RUStJt40PQ\"\n )\n\n with urlopen(YANDEX_JWK_ENDPOINT) as response:\n public_jwks = json.load(response)\n\n payload = jwt.decode(JWT_TOKEN, public_jwks, algorithms=[\"ES256\"])\n print(json.dumps(payload, indent=2))\n ```\n\n- PHP\n\n ```php\n use Firebase\\JWT\\JWT;\n use Firebase\\JWT\\JWK;\n\n $sandboxJwk = 'https://sandbox.pay.yandex.uz/api/jwks';\n\n $JWT_TOKEN =\n \"eyJhbGciOiJFUzI1NiIsImlhdCI6MTY1MDE5Njc1OCwia2lkIjoidGVzdC1rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXJ0Ijp7Iml0ZW1zIjpbeyJwcm9kdWN\"\n . \"0SWQiOiJwMSIsInF1YW50aXR5Ijp7ImNvdW50IjoiMSJ9fV19LCJjdXJyZW5jeUNvZGUiOiJSVUIiLCJtZXJjaGFudElkIjoiMjc2Y2YxZjEtZjhlZC00N\"\n . \"GZlLTg5ZTMtNWU0MTEzNDZkYThkIn0.YmQjHlh3ddLWgBexQ3QrwtbgAA3u1TVnBl1qnfMIvToBwinH3uH92KGB15m4NAQXdz5nhkjPZZu7RUStJt40PQ\";\n\n $client = new GuzzleHttp\\Client();\n $keysJson = $client->get($sandboxJwk)->getBody();\n $keysData = json_decode($keysJson, JSON_OBJECT_AS_ARRAY);\n\n $keys = JWK::parseKeySet($keysData);\n $payload = JWT::decode($JWT_TOKEN, $keys);\n\n print_r($payload);\n ```\n\n To run the example, install the required dependencies:\n\n ```bash\n composer require firebase/php-jwt\n composer require guzzlehttp/guzzle\n ```\n\n{% endlist %}\n\n## Troubleshooting webhook issues {#troubleshooting}\n\nIf no notifications are received, check the following:\n\n- Whether the right backend URL is specified\n\n Notifications may be sent to an unexpected location. [Be sure to specify the Callback URL](../../../console/settings-pay-split.md#callback-settings) without `/v1/webhook` — this path is added automatically. For example:\n\n #|\n || **Callback URL** | **Where the request will be sent** ||\n || `https://example.merchant.uz` | `https://example.merchant.uz/v1/webhook` ||\n || `https://example.merchant.uz/v1/webhook` | `https://example.merchant.uz/v1/webhook/v1/webhook` ||\n |#\n\n- Handling `Content-Type`\n\n Make sure that your store's backend can accept messages with the `Content-Type: application/octet-stream` header.\n\n- SSL certificate\n\n The system can't read self-signed SSL certificates. Use a certificate issued by a trusted certification authority.\n\n- Firewall settings\n\n Check that the firewall doesn't block incoming requests or truncate the request body.",
"title": "Merchant API",
"version": "1.0.0"
},
"openapi": "3.0.3",
"paths": {
"/v1/webhook": {
"post": {
"description": "Notifications about status updates.\n\nA request is sent when the order status or order operation status changes.\n\nSupported events:\n\n- `ORDER_STATUS_UPDATED`: Update of the order status.\n- `OPERATION_STATUS_UPDATED`: Status updated for charge, refund, or cancellation operations.\n<!-- - `SUBSCRIPTION_STATUS_UPDATED` — сейчас этот статус не отдаем? -->\n\n## Request format {#webhook-format}\n\nA request is received in `application/octet-stream` format as a JWT token signed by the ES256 algorithm. Before handling the request, validate it. To learn how to do this, see [Authentication](../merchant-api/index.md).\n\nThe payload of a validated and decoded JWT token contains JSON with event data. See [sample events](#webhook-examples).\n\n{% note warning \"If a token contains no request body\" %}\n\n- Make sure that your store's backend can accept messages with the `Content-Type: application/octet-stream` header.\n- Check that the firewall doesn't block incoming requests or truncate the request body.\n\nSee details of other errors in [Troubleshooting webhook issues](../merchant-api/index.md#troubleshooting).\n\n{% endnote %}\n\n## Operation idempotency {#idempotency}\n\nWhen performing operations with an order, such as making a refund via the [/v2/orders/{order_id}/refund](../yandex-pay-api/order/merchant_v2_refund-post.md) method call, be sure to pass the unique operation ID in the `externalOperationId` parameter.\n\nThis allows you to:\n- Understand what operation you received a notification for.\n- Check the operation status by calling the [v1/operations/{external_operation_id}](../yandex-pay-api/operation/merchant_v1_operations-get.md) method.\n- Prevent duplication.\n\nIf you repeat a request with the same `externalOperationId` value, you'll get the following:\n- Information about the current operation if it's in progress.\n- An error with the `reasonCode: \"DUPLICATE_EXTERNAL_OPERATION_ID\"` if the operation is complete.\n\n## Sample events {#webhook-examples}\n\n### Paying for an order\n\n{% list tabs %}\n\n- Success\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2023-11-26T08:11:09.359370+00:00\",\n \"order\":{\n \"orderId\": \"700aa3f04df64b3b8712d6b51f752e8b\",\n \"paymentStatus\": \"CAPTURED\"\n }\n }\n ```\n\n See JWT token examples on [jwt.io](https://jwt.io/#debugger-io?token=eyJhbGciOiJFUzI1NiIsImV4cCI6MTcwMDk4NzYwMCwiaWF0IjoxNzAwOTg3MzAwLCJraWQiOiIxLW1lcmNoYW50LWFwaSIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoieHh4eHh4eHh4LXh4eC01eHh4LXh4eHh4LXh4eHh4eHh4IiwiZXZlbnQiOiJPUkRFUl9TVEFUVVNfVVBEQVRFRCIsImV2ZW50VGltZSI6IjIwMjMtMTEtMjZUMDg6MTE6MDkuMzU5MzcwKzAwOjAwIiwib3JkZXIiOnsib3JkZXJJZCI6IjcwMGFhM2YwNGRmNjRiM2I4NzEyZDZiNTFmNzUyZThiIiwicGF5bWVudFN0YXR1cyI6IkNBUFRVUkVEIn19.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).\n\n **Sample request from Yandex to the store backend:**\n\n ```(bash)\n curl -X POST https://test.uz/some/prefix/v1/webhook \\\n --header 'User-Agent: YandexPay/1.0' \\\n --header 'Accept: \\*/\\*' \\\n --header 'Content-Type: application/octet-stream' \\\n --header 'X-Request-Id: ff2a54885c4e45309853d2e33af1d63b\\\\_3a70f3062db640fcb2f3c34de1a27bd5' \\\n --header 'X-Request-Timeout: 13970' \\\n --compressed \\\n -d eyJhbGciOiJFUzI1NiIsImV4cCI6MTcwMDk4NzYwMCwiaWF0IjoxNzAwOTg3MzAwLCJraWQiOiIxLW1lcmNoYW50LWFwaSIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoieHh4eHh4eHh4LXh4eC01eHh4LXh4eHh4LXh4eHh4eHh4IiwiZXZlbnQiOiJPUkRFUl9TVEFUVVNfVVBEQVRFRCIsImV2ZW50VGltZSI6IjIwMjMtMTEtMjZUMDg6MTE6MDkuMzU5MzcwKzAwOjAwIiwib3JkZXIiOnsib3JkZXJJZCI6IjcwMGFhM2YwNGRmNjRiM2I4NzEyZDZiNTFmNzUyZThiIiwicGF5bWVudFN0YXR1cyI6IkNBUFRVUkVEIn19.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n ```\n\n- Failure\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-25T07:56:29.974810+00:00\",\n \"order\":{\n \"orderId\": \"253222_1714029088\",\n \"paymentStatus\": \"FAILED\"\n }\n }\n ```\n\n See JWT token examples on [jwt.io](https://jwt.io/#debugger-io?token=eyJhbGciOiJFUzI1NiIsImV4cCI6MTcxNDAzMjIyMSwiaWF0IjoxNzE0MDMxOTIxLCJraWQiOiIxLW1lcmNoYW50LWFwaSIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoieHh4eHh4eHh4LXh4eC01eHh4LXh4eHh4LXh4eHh4eHh4IiwiZXZlbnQiOiJPUkRFUl9TVEFUVVNfVVBEQVRFRCIsImV2ZW50VGltZSI6IjIwMjQtMDQtMjVUMDc6NTY6MjkuOTc0ODEwKzAwOjAwIiwib3JkZXIiOnsib3JkZXJJZCI6IjI1MzIyMl8xNzE0MDI5MDg4IiwicGF5bWVudFN0YXR1cyI6IkZBSUxFRCJ9fQ.v9dw_cR3_b4R5v0D8WRisrSPABxhegSSpEq4kz9s10fr5cUK150yWnwJREYCGQCm5BZK1Yydsquh-WE6OyRR2APOST).\n\n{% endlist %}\n\n### Return\n\n{% note tip %}\n\nFirst, see [/v2/orders/{order_id}/refund](../yandex-pay-api/order/merchant_v2_refund-post.md) to learn how refunds are made.\n\n{% endnote %}\n\n#### Full refund\n\nRegardless of whether the order status changes, two notifications are sent: one for the operation and one for the order.\n\n{% list tabs %}\n\n- Success\n\n 1. `OPERATION_STATUS_UPDATED`: Refund has been successfully completed:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"OPERATION_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T10:27:53.323878+00:00\",\n \"operation\":{\n \"operationId\": \"73dec2cd-db5c-4386-be6d-10c5b5a2ee09\",\n \"orderId\": \"86283\",\n \"status\": \"SUCCESS\",\n \"operationType\": \"REFUND\"\n }\n }\n ```\n\n 2. `ORDER_STATUS_UPDATED`: The order is now in the `REFUNDED` terminal status. You can no longer call refund methods.\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T12:16:28.766392+00:00\",\n \"order\":{\n \"orderId\": \"86283\",\n \"paymentStatus\": \"REFUNDED\"\n }\n }\n ```\n\n See JWT token examples on [jwt.io](https://jwt.io/#debugger-io?token=eyJhbGciOiJFUzI1NiIsImV4cCI6MTcxMzUyOTI4OSwiaWF0IjoxNzEzNTI4OTg5LCJraWQiOiIxLW1lcmNoYW50LWFwaSIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoieHh4eHh4eHh4LXh4eC01eHh4LXh4eHh4LXh4eHh4eHh4IiwiZXZlbnQiOiJPUkRFUl9TVEFUVVNfVVBEQVRFRCIsImV2ZW50VGltZSI6IjIwMjQtMDQtMTlUMTI6MTY6MjguNzY2MzkyKzAwOjAwIiwib3JkZXIiOnsib3JkZXJJZCI6Ijg2MjgzIiwicGF5bWVudFN0YXR1cyI6IlJFRlVOREVEIn19.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).\n\n- Failure\n\n 1. `OPERATION_STATUS_UPDATED`: Refund has failed:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"OPERATION_STATUS_UPDATED\",\n \"eventTime\": \"2024-06-13T22:27:53.323878+00:00\",\n \"operation\":{\n \"operationId\": \"73dec2cd-db5c-4386-be6d-10c5b5a2ee08\",\n \"orderId\": \"9c8aed6d-a8e5-4c6a-acd8-645538173f66\",\n \"status\": \"FAIL\",\n \"operationType\": \"REFUND\"\n }\n }\n ```\n\n See JWT token examples on [jwt.io](https://jwt.io/#debugger-io?token=eyJhbGciOiJFUzI1NiIsImV4cCI6MTcxODMxNzk3NCwiaWF0IjoxNzE4MzE3Njc0LCJraWQiOiIxLW1lcmNoYW50LWFwaSIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoieHh4eHh4eHh4LXh4eC01eHh4LXh4eHh4LXh4eHh4eHh4IiwiZXZlbnQiOiJPUEVSQVRJT05fU1RBVFVTX1VQREFURUQiLCJldmVudFRpbWUiOiIyMDI0LTA2LTEzVDIyOjI3OjUzLjMyMzg3OCswMDowMCIsIm9wZXJhdGlvbiI6eyJvcGVyYXRpb25JZCI6IjczZGVjMmNkLWRiNWMtNDM4Ni1iZTZkLTEwYzViNWEyZWUwOCIsIm9yZGVySWQiOiI5YzhhZWQ2ZC1hOGU1LTRjNmEtYWNkOC02NDU1MzgxNzNmNjYiLCJzdGF0dXMiOiJGQUlMIiwib3BlcmF0aW9uVHlwZSI6IlJFRlVORCJ9fQ.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).\n\n 2. `ORDER_STATUS_UPDATED`: The order is still in the previous `CAPTURED` status:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-06-13T22:27:54.323878+00:00\",\n \"order\":{\n \"orderId\": \"9c8aed6d-a8e5-4c6a-acd8-645538173f66\",\n \"paymentStatus\": \"CAPTURED\"\n }\n }\n ```\n\n{% endlist %}\n\n#### Partial refund\n\nYou can refund the order amount as multiple partial refunds. Once the sum of all refunds reaches the total order value, the order switches to the `REFUNDED` terminal status. After that, you can no longer call refund methods.\n\nRegardless of whether the order status changes, two notifications are sent: one for the operation and one for the order.\n\nLet's take an example of an order of three cartons of juice.\n\n1. A partial refund is made for one carton of juice. You'll get two notifications:\n\n 1. `OPERATION_STATUS_UPDATED`: Refund has been successfully completed:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"OPERATION_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T10:27:53.323878+00:00\",\n \"operation\":{\n \"operationId\": \"73dec3cs-sd5t-4356-ne6d-10c79b5d2ee09\",\n \"externalOperationId\": \"123-partial-refund-1\",\n \"orderId\": \"123\",\n \"status\": \"SUCCESS\",\n \"operationType\": \"REFUND\"\n }\n }\n ```\n\n 2. `ORDER_STATUS_UPDATED`: The order is now in the `PARTIALLY_REFUNDED` status.\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T12:16:28.766392+00:00\",\n \"order\":{\n \"orderId\": \"123\",\n \"paymentStatus\": \"PARTIALLY_REFUNDED\"\n }\n }\n ```\n\n2. Another partial refund is made for one carton of juice. You'll get two notifications:\n\n 1. `OPERATION_STATUS_UPDATED`: Refund has been successfully completed:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"OPERATION_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T13:27:53.323878+00:00\",\n \"operation\":{\n \"operationId\": \"28fba9ds-kl2m-7891-qw3r-45e82c7f1bb12\",\n \"externalOperationId\": \"123-partial-refund-2\",\n \"orderId\": \"123\",\n \"status\": \"SUCCESS\",\n \"operationType\": \"REFUND\"\n }\n }\n ```\n\n 2. `ORDER_STATUS_UPDATED`: The order is still in the `PARTIALLY_REFUNDED` status:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T13:27:54.321878+00:00\",\n \"order\":{\n \"orderId\": \"123\",\n \"paymentStatus\": \"PARTIALLY_REFUNDED\"\n }\n }\n ```\n\n3. A third partial refund is issued. The sum of all refunds is now equal to the total order value. You'll get two notifications:\n\n 1. `OPERATION_STATUS_UPDATED`: Refund has been successfully completed:\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"OPERATION_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T13:40:51.323878+00:00\",\n \"operation\":{\n \"operationId\": \"64abc2ts-rj4y-3187-mf5g-56b71e9a4dd67\",\n \"externalOperationId\": \"123-partial-refund-3\",\n \"orderId\": \"123\",\n \"status\": \"SUCCESS\",\n \"operationType\": \"REFUND\"\n }\n }\n ```\n\n 2. `ORDER_STATUS_UPDATED`: The order is now in the `REFUNDED` terminal status. You can no longer call refund methods.\n\n ```json\n {\n \"merchantId\": \"xxxxxxxxx-xxx-5xxx-xxxxx-xxxxxxxx\",\n \"event\": \"ORDER_STATUS_UPDATED\",\n \"eventTime\": \"2024-04-19T14:16:28.766392+00:00\",\n \"order\":{\n \"orderId\": \"123\",\n \"paymentStatus\": \"REFUNDED\"\n }\n }\n ```",
"operationId": "webhook",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"event": {
"description": "Event type:\n- `ORDER_STATUS_UPDATED`: Update of the order status.\n- `OPERATION_STATUS_UPDATED`: Status updated for charge, refund, or cancellation operations.",
"enum": [
"TRANSACTION_STATUS_UPDATE",
"ORDER_STATUS_UPDATED",
"OPERATION_STATUS_UPDATED",
"SUBSCRIPTION_STATUS_UPDATED"
],
"type": "string"
},
"eventTime": {
"description": "Event time in `RFC 3339`: `YYYY-MM-DDThh:mm:ssTZD` format.",
"example": "2025-05-26T21:00:36.08847+00:00",
"format": "date-time",
"type": "string"
},
"merchantId": {
"description": "Merchant ID.",
"format": "uuid",
"type": "string"
},
"operation": {
"allOf": [
{
"properties": {
"externalOperationId": {
"description": "Operation ID in the merchant system. Make sure it's unique.\n\nPass this parameter to track a specific operation by calling the [v1/operations/{external_operation_id}](../yandex-pay-api/operation/merchant_v1_operations-get) method.",
"type": "string"
},
"operationId": {
"description": "Operation ID.",
"example": "5d32f295-8723-457d-81f9-ab13f17b7bd6",
"format": "uuid",
"type": "string"
},
"operationType": {
"description": "Operation type. To learn more about operation types, see [Operation statuses](../../../payments/statuses).",
"enum": [
"AUTHORIZE",
"BIND_CARD",
"REFUND",
"CAPTURE",
"VOID",
"RECURRING",
"PREPAYMENT",
"SUBMIT"
],
"type": "string"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"status": {
"description": "Operation status. To learn more about operation statuses, see [Operation statuses](../../../payments/statuses).",
"enum": [
"PENDING",
"SUCCESS",
"FAIL"
],
"type": "string"
}
},
"required": [
"operationId",
"operationType",
"orderId",
"status"
],
"type": "object"
}
],
"description": "Operation details. Received along with the `OPERATION_STATUS_UPDATED` event"
},
"order": {
"allOf": [
{
"properties": {
"cartUpdated": {
"description": "Shows whether the cart has been updated. Returned if the order is paid for with Plus points. If set to `true`, the current cart info is provided.",
"type": "boolean"
},
"orderId": {
"description": "Order ID provided in [/v1/orders](../yandex-pay-api/order/merchant_v1_orders-post.md) when creating the order.",
"type": "string"
},
"paymentStatus": {
"description": "Order status. For more information, see [Order status](../../../payments/statuses.md).",
"enum": [
"PENDING",
"AUTHORIZED",
"CAPTURED",
"VOIDED",
"REFUNDED",
"CONFIRMED",
"PARTIALLY_REFUNDED",
"FAILED"
],
"type": "string",
"x-enumDescriptions": {
"AUTHORIZED": "Платеж за заказ авторизован. Средства заблокированы на счету плательщика",
"CAPTURED": "Заказ успешно оплачен. Средства списаны со счета плательщика",
"CONFIRMED": "Заказ успешно оформлен",
"FAILED": "Заказ не был успешно оплачен",
"PARTIALLY_REFUNDED": "Совершён частичный возврат средств за заказ",
"PENDING": "Ожидается оплата",
"REFUNDED": "Совершён возврат средств за заказ",
"VOIDED": "Оплата отменена (voided). Списание средств не производилось"
}
}
},
"required": [
"orderId",
"paymentStatus"
],
"type": "object"
}
],
"description": "Order details. Received along with the `ORDER_STATUS_UPDATED` event"
},
"subscription": {
"allOf": [
{
"properties": {
"customerSubscriptionId": {
"description": "Subscription ID. The SDK returns it when the subscription is created successfully. You can also save a subscription when you get the first notification on it. The same value will arrive in this field every time the subscription is updated.",
"format": "uuid",
"type": "string"
},
"nextWriteOff": {
"description": "Date of the next attempt to debit a subscription fee",
"format": "date-time",
"type": "string"
},
"status": {
"description": "Subscription type",
"enum": [
"NEW",
"ACTIVE",
"CANCELLED",
"EXPIRED"
],
"type": "string"
},
"subscriptionPlanId": {
"description": "ID of the subscription plan created in the dashboard or over the API.",
"format": "uuid",
"type": "string"
}
},
"required": [
"customerSubscriptionId",
"status",
"subscriptionPlanId"
],
"type": "object"
}
],
"description": "Subscription status."
}
},
"required": [
"event",
"eventTime",
"merchantId"
],
"type": "object"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"status": {
"default": "success",
"type": "string"
}
},
"type": "object"
}
}
},
"description": "Webhook received and processed.\nThe response body can be any value. We recommend sending `{\"status\": \"success\"}`.\nIf the status code `200` is received, Yandex stops sending repeat webhooks."
},
"400": {
"content": {
"application/json": {
"schema": {
"properties": {
"reason": {
"description": "Error cause description.",
"type": "string"
},
"reasonCode": {
"description": "Error code:\n\n- `FORBIDDEN`: The order exists, but it was not paid for via Yandex Split.\n- `ORDER_NOT_FOUND`: No order found in the merchant system.\n- `ORDER_AMOUNT_MISMATCH`: The order amount does not match the amount in the merchant system.\n- `ORDER_DETAILS_MISMATCH`: The order details differ from those in the merchant system.\n- `OTHER`: Generic error.\n- `UNAUTHORIZED`: JWT signature validation failed.\n- `TOKEN_EXPIRED`: The JWT token has expired.\n- `CONFLICT`: The notification data conflicts with the order state in the merchant system. For example, a notification about successful payment for a canceled order is received.",
"enum": [
"FORBIDDEN",
"ITEM_NOT_FOUND",
"ORDER_NOT_FOUND",
"ORDER_AMOUNT_MISMATCH",
"ORDER_DETAILS_MISMATCH",
"OUT_OF_INVENTORY",
"PICKUP_POINT_NOT_FOUND",
"SHIPPING_DETAILS_MISMATCH",
"OTHER",
"UNAUTHORIZED",
"TOKEN_EXPIRED",
"CONFLICT"
],
"type": "string"
},
"status": {
"default": "fail",
"type": "string"
}
},
"required": [
"reasonCode"
],
"type": "object"
}
}
},
"description": "Webhook processing error.\nIf no response or any status except `200` is received, Yandex generates a new JWT token and retries the webhook:\n- The first 10 attempts occur every 5 ms.\n- Then the interval increases exponentially up to 15 minutes.\n- After that, retries occur every 15 minutes for 24 hours.\nThe total retry period is 24 hours. After that, the webhook is considered undelivered."
}
},
"summary": "/v1/webhook"
}
}
},
"servers": [
{
"description": "Production",
"url": "https://example.merchant.uz"
},
{
"description": "Sandbox",
"url": "https://sandbox.example.merchant.uz"
}
]
}