{"openapi":"3.1.0","info":{"title":"UPI Station API","version":"1.0.0","description":"UPI Station merchant API for creating and querying UPI payment\nrequests. Every request is authenticated with `x-key-id` and an HMAC\n`x-signature` over the raw body (see the `apiKeySignature` security scheme).\nFull integration guide and examples: https://staging.upistation.com/docs"},"servers":[{"url":"https://staging.upistation.com"}],"tags":[{"name":"Payment Request API","description":"Create and query UPI payment requests. All operations are idempotent on\n`client_request_id` — retrying a create with the same id returns the\nexisting request instead of creating a duplicate."},{"name":"Webhooks","description":"Asynchronous, signed notifications delivered to your `webhook_url` as a\nrequest changes status. See **request.status.changed** for the payload\nand delivery semantics."}],"paths":{"/api/v1/payment/requests":{"post":{"operationId":"createPaymentRequest","summary":"Create a Payment Request","tags":["Payment Request API"],"description":"Create an idempotent UPI payment request and obtain a hosted payment link.","security":[{"apiKeyId":[],"apiKeySignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePaymentRequest"}}}},"responses":{"200":{"description":"The created (or existing idempotent) payment request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequest"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid signature / key id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/payment/requests/query":{"post":{"operationId":"queryPaymentRequest","summary":"Payment Request Status","tags":["Payment Request API"],"description":"Fetch the current status of a previously created payment request.","security":[{"apiKeyId":[],"apiKeySignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"The current state of the payment request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequest"}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid signature / key id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Payment request not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"request.status.changed":{"post":{"operationId":"onRequestStatusChanged","summary":"Request status changed","tags":["Webhooks"],"description":"Sent to your configured `webhook_url` whenever a payment request changes\nstatus. The POST body is signed with `x-signature` using the same scheme as\nAPI requests (see the `apiKeySignature` security scheme); verify it with your\nkey secret over the raw body.\n\nDelivery is **at-least-once**: any non-2xx response is retried up to 10\ntimes with backoff, so handlers must be idempotent.\n\nNote the field naming differs from the query response: webhooks use\n`payment_url` (not `payment_link`) and `expires_at` (not `expired_at`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Acknowledged. Any non-2xx triggers redelivery (up to 10 retries)."}}}}},"components":{"securitySchemes":{"apiKeyId":{"type":"apiKey","in":"header","name":"x-key-id","description":"Your API key id (the `usk_…` value). **Required on every request**\nalongside `x-signature` — sending only the signature returns\n`401 Missing API signature headers`."},"apiKeySignature":{"type":"apiKey","in":"header","name":"x-signature","description":"Every request MUST send two headers: `x-key-id` (your API key id) and\n`x-signature`, an HMAC-SHA256 signature of the raw request body.\n\nFormat: `x-signature: v1=<base64url>`.\n\n- `signingKey = SHA256(\"upi-station.api-signing-key.v1\\0\" + key_secret)`\n- `preimage` is the UTF-8 string:\n  ```\n  upi-station.api-signature.v1\n  key-id:<x-key-id>\n  body-length:<utf8 byte length of raw body>\n\n  <raw request body>\n  ```\n- `signature = base64url(HMAC-SHA256(signingKey, preimage))`, sent prefixed\n  with `v1=`.\n\nThe signature binds to the exact raw body bytes and key id, so it is resistant\nto JSON re-serialisation and canonicalisation ambiguities. Compute it\nserver-side only — never ship your key secret to a browser or mobile client."}},"schemas":{"CreatePaymentRequest":{"type":"object","required":["client_request_id","client_customer_id","payment_system","amount"],"properties":{"client_request_id":{"type":"string","description":"Caller-supplied idempotency key for this request.","example":"order-2026-0001"},"client_customer_id":{"type":"string","description":"Your stable identifier for the paying customer.","example":"cust_8842"},"payment_system":{"type":"string","description":"Target payment system.","example":"PAYTM"},"amount":{"type":["string","number"],"description":"Amount to collect. Accepts a string or number; stored as a decimal string.","example":"100.00"},"currency":{"type":"string","default":"INR","example":"INR"},"redirect_success_url":{"type":"string","format":"uri"},"redirect_return_url":{"type":"string","format":"uri"},"webhook_url":{"type":"string","format":"uri","description":"Endpoint that receives `request.status.changed` webhooks."},"notes":{"type":["object","null"],"additionalProperties":true,"description":"Arbitrary metadata echoed back on the request and webhooks."},"expires_in_minutes":{"type":"integer","minimum":1,"description":"Minutes until the request expires."}}},"PaymentRequest":{"type":"object","required":["service_request_id","client_customer_id","client_request_id","payment_system","status","amount","payment_link","status_updated_at"],"properties":{"service_request_id":{"type":"string","format":"uuid"},"client_customer_id":{"type":"string"},"client_request_id":{"type":"string"},"payment_system":{"type":"string","example":"PAYTM"},"status":{"type":"string","enum":["PENDING","PAID","FAILED","EXPIRED"]},"amount":{"type":"string","example":"100.00"},"amount_paid":{"type":["string","null"],"example":"100.00"},"payment_info":{"anyOf":[{"$ref":"#/components/schemas/PaymentInfo"},{"type":"null"}]},"payment_link":{"type":"string","format":"uri"},"intent_url":{"type":["string","null"],"example":"upi://pay?pa=merchant@upi&pn=Merchant&am=279.00&tr=UPIS26060111324737A973","description":"Raw UPI intent deeplink for apps that drive their own checkout instead of redirecting the customer to `payment_link`."},"app_intents":{"anyOf":[{"$ref":"#/components/schemas/AppIntents"},{"type":"null"}]},"status_updated_at":{"type":"string","format":"date-time"},"expired_at":{"type":["string","null"],"format":"date-time"},"notes":{"type":["object","null"],"additionalProperties":true}}},"PaymentInfo":{"type":"object","description":"Settlement details, populated once a payment is captured.","required":["amount","payee_upi_id","payment_at","rrn"],"properties":{"amount":{"type":"string","example":"100.00"},"payee_upi_id":{"type":"string","example":"merchant@paytm"},"payer_upi_id":{"type":["string","null"],"example":"customer@okhdfcbank"},"payment_at":{"type":"string","format":"date-time"},"rrn":{"type":"string","description":"Bank retrieval reference number.","example":"401512345678"}}},"AppIntents":{"type":"object","description":"Per-app UPI deeplinks derived from `intent_url`. Use these to open a specific app: Android shows a chooser for the generic `upi://` scheme, but iOS does not, so a custom checkout must open an app via its own scheme. Always pair with a QR / copy-VPA fallback — a web page cannot tell which apps are installed.","required":["google_pay","phonepe","paytm","bhim"],"properties":{"google_pay":{"type":"string","example":"tez://upi/pay?pa=merchant@upi&am=279.00&tr=…"},"phonepe":{"type":"string","example":"phonepe://pay?pa=merchant@upi&am=279.00&tr=…"},"paytm":{"type":"string","example":"paytmmp://pay?pa=merchant@upi&am=279.00&tr=…"},"bhim":{"type":"string","example":"bhim://upi/pay?pa=merchant@upi&am=279.00&tr=…"}}},"QueryRequest":{"type":"object","required":["service_request_id"],"properties":{"service_request_id":{"type":"string","format":"uuid"}}},"WebhookEvent":{"type":"object","required":["service_request_id","client_customer_id","client_request_id","payment_system","status","amount","payment_url","status_updated_at"],"properties":{"service_request_id":{"type":"string","format":"uuid"},"client_customer_id":{"type":"string"},"client_request_id":{"type":"string"},"payment_system":{"type":"string","example":"PAYTM"},"status":{"type":"string","enum":["PENDING","PAID","FAILED","EXPIRED"]},"amount":{"type":"string","example":"100.00"},"amount_paid":{"type":["string","null"]},"payment_info":{"anyOf":[{"$ref":"#/components/schemas/PaymentInfo"},{"type":"null"}]},"payment_url":{"type":"string","format":"uri","description":"Webhook-only field; the query response calls this `payment_link`."},"intent_url":{"type":["string","null"],"example":"upi://pay?pa=merchant@upi&pn=Merchant&am=279.00&tr=UPIS26060111324737A973","description":"Raw UPI intent deeplink for apps that drive their own checkout. Same value the query response returns as `intent_url`."},"app_intents":{"anyOf":[{"$ref":"#/components/schemas/AppIntents"},{"type":"null"}]},"status_updated_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time","description":"Webhook-only field; the query response calls this `expired_at`."},"notes":{"type":["object","null"],"additionalProperties":true}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message."}}}}}}