{"info":{"_postman_id":"0a887196-9c41-4c91-987b-983aa882d323","name":"CORALIX API","description":"<html><head></head><body><p>Welcome to the <strong>CORALIX API</strong>.</p>\n<p>The CORALIX API lets you take payments, send payouts and issue refunds from your own backend through one REST interface, and tells you the outcome of each through signed webhooks.</p>\n<h3 id=\"base-url\">Base URL</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>https://api.coralix.tech/v1\n</code></pre><p>Every path in this documentation starts with <code>/v1</code>. All traffic is <strong>HTTPS</strong>, and all request and response bodies are <strong>JSON</strong>.</p>\n<h3 id=\"environments\">Environments</h3>\n<p>There is one base URL. <strong>Your API key decides the environment</strong>:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Key prefix</th>\n<th>Environment</th>\n<th>What happens</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>pk_test_…</code></td>\n<td><code>test</code></td>\n<td>Payments go to sandbox providers. No real money moves. Use the cards in <strong>Testing</strong>.</td>\n</tr>\n<tr>\n<td><code>pk_production_…</code></td>\n<td><code>production</code></td>\n<td>Real payments, real money.</td>\n</tr>\n</tbody>\n</table>\n</div><p>The two environments are fully separate. A session, transaction or webhook endpoint created with a test key does not exist for a production key: reading it returns <code>404</code>, the same as an identifier that was never created. Use the same API calls in both; only the key changes.</p>\n<h3 id=\"conventions\">Conventions</h3>\n<ul>\n<li><strong>Field names</strong> are <code>snake_case</code>, in requests and responses.</li>\n<li><strong>Amounts</strong> are decimal major units. Send them as a JSON number (<code>15.20</code>); responses return them as a string with two decimals (<code>\"15.20\"</code>).</li>\n<li><strong>Currencies</strong> are ISO 4217 codes (<code>USD</code>, <code>EUR</code>, …).</li>\n<li><strong>Timestamps</strong> are ISO 8601 in UTC (<code>2026-10-06T14:03:11.000Z</code>).</li>\n<li><strong>Unknown fields</strong> in a request body are rejected with <code>400 validation_failed</code>.</li>\n<li><strong>Errors</strong> always have the same shape, <code>{ \"error_code\", \"message\", \"request_id\" }</code>. See <strong>Error Codes</strong>.</li>\n</ul>\n<h3 id=\"how-this-documentation-is-organized\">How this documentation is organized</h3>\n<p><strong>Get your first test payment working:</strong></p>\n<ol>\n<li><strong>Authentication</strong>: signing every request with your keys.</li>\n<li><strong>Payment Flow</strong>: from creating a session to knowing the outcome.</li>\n<li><strong>Testing</strong>: test cards and scenarios for the <code>test</code> environment.</li>\n</ol>\n<p><strong>Before going live:</strong></p>\n<ol>\n<li><strong>Webhooks</strong>: receiving and verifying events. Required to know the real outcome of a payment.</li>\n<li><strong>Refunds</strong>: giving money back.</li>\n<li><strong>Withdrawals</strong>: sending money out, if payouts are enabled for your account.</li>\n</ol>\n<p><strong>Reference:</strong></p>\n<ol>\n<li><strong>Endpoints</strong>: every endpoint, with examples.</li>\n<li><strong>Error Codes</strong>: everything the API can answer when a request fails.</li>\n<li><strong>Decline Reasons Reference</strong>: why a payment was declined.</li>\n<li><strong>Going Live</strong>: the checklist before switching to production keys.</li>\n</ol>\n<p><strong>Start with Authentication</strong>: every request must be signed.</p>\n</body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[],"owner":"547480","collectionId":"0a887196-9c41-4c91-987b-983aa882d323","publishedId":"2sBXwyGnNB","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-06-29T08:53:48.000Z"},"item":[{"name":"AUTHENTICATION","item":[],"id":"e73419de-30d6-4059-8672-66ec02d790b2","description":"<p>Every request to the CORALIX API is authenticated in its headers. There are no cookies, sessions or OAuth flows: each request carries your public key and an <strong>HMAC-SHA256 signature</strong> made with your secret key. The secret itself is never sent.</p>\n<blockquote>\n<p>Call the API <strong>from your backend only</strong>. Your secret key must never reach a browser or a mobile app.</p>\n</blockquote>\n<hr />\n<h2 id=\"1-your-keys\">1. Your keys</h2>\n<p>Keys are issued in pairs, one pair per environment:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Key</th>\n<th>Prefix</th>\n<th>Where it goes</th>\n<th>Rule</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Public key</strong></td>\n<td><code>pk_test_…</code> / <code>pk_production_…</code></td>\n<td>Sent in the <code>X-API-Key</code> header</td>\n<td>Identifies you and the environment. Not secret.</td>\n</tr>\n<tr>\n<td><strong>Secret key</strong></td>\n<td><code>sk_test_…</code> / <code>sk_production_…</code></td>\n<td><strong>Never sent.</strong> Used only to compute <code>X-Signature</code></td>\n<td>Store it server-side, like a password.</td>\n</tr>\n</tbody>\n</table>\n</div><p>The secret key is shown <strong>only once</strong>, when the pair is created. If it leaks, create a new pair and revoke the old one (see <strong>Endpoints → API keys</strong>).</p>\n<h2 id=\"2-required-headers\">2. Required headers</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Header</th>\n<th>Required</th>\n<th>Example</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>X-API-Key</code></td>\n<td>Yes</td>\n<td><code>pk_test_fecc9278094f9fa2b24197fed9d6647f</code></td>\n<td>Your public key</td>\n</tr>\n<tr>\n<td><code>X-Timestamp</code></td>\n<td>Yes</td>\n<td><code>1759759391</code></td>\n<td>Unix time <strong>in seconds</strong>, when you signed the request</td>\n</tr>\n<tr>\n<td><code>X-Signature</code></td>\n<td>Yes</td>\n<td><code>9f86d081884c7d65…</code></td>\n<td>Lowercase hex HMAC-SHA256 of the canonical string (see §3), keyed with your secret key</td>\n</tr>\n<tr>\n<td><code>Content-Type</code></td>\n<td>With a body</td>\n<td><code>application/json</code></td>\n<td>All bodies are JSON</td>\n</tr>\n<tr>\n<td><code>Idempotency-Key</code></td>\n<td>Recommended on <code>POST</code>; <strong>required</strong> on refunds</td>\n<td><code>order_123-pay-1</code></td>\n<td>Makes retries safe. See §6.</td>\n</tr>\n<tr>\n<td><code>X-Request-Id</code></td>\n<td>Optional</td>\n<td><code>your-trace-id</code></td>\n<td>If you send it, we use it as the <code>request_id</code> of the response</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"3-building-x-signature\">3. Building <code>X-Signature</code></h2>\n<p>The signature is the HMAC-SHA256 of a <strong>canonical string</strong> of four lines joined by <code>\\n</code>:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>METHOD         ← uppercase HTTP verb, e.g. POST\nPATH           ← request path including /v1, WITHOUT the query string\nTIMESTAMP      ← the same value you send in X-Timestamp\nSHA256(body)   ← lowercase hex SHA-256 of the exact raw request body\n</code></pre><pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>canonical = METHOD + \"\\n\" + PATH + \"\\n\" + TIMESTAMP + \"\\n\" + HEX(SHA256(body))\nsignature = HEX(HMAC_SHA256(secret_key, canonical))\n</code></pre><p>These must match exactly, or the request is rejected with <code>401 invalid_authentication</code>:</p>\n<ul>\n<li><strong>METHOD</strong> is uppercase (<code>POST</code>, <code>GET</code>, <code>DELETE</code>).</li>\n<li><strong>PATH</strong> starts with <code>/v1</code> and excludes the scheme, the host and any <code>?query</code> (e.g. <code>/v1/sessions/3b1211a8-7316-44a9-9764-1cc9e2ab1ca3/pay</code>).</li>\n<li><strong>TIMESTAMP</strong> is the same string you put in <code>X-Timestamp</code>.</li>\n<li><strong>The body is hashed byte for byte</strong> as it goes on the wire. Serialize it once, sign that string, and send that same string. Do not re-serialize, reorder keys or change whitespace after signing.</li>\n<li><strong>Requests without a body</strong> (<code>GET</code>, <code>DELETE</code>) hash the empty string, whose SHA-256 is <code>e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</code>.</li>\n<li>Both hashes are <strong>lowercase hex</strong>.</li>\n</ul>\n<h3 id=\"worked-example\">Worked example</h3>\n<p><code>POST https://api.coralix.tech/v1/sessions</code>, signed at <code>1759759391</code>, with the body:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\"order_id\":\"order_123\",\"amount\":15.20,\"currency\":\"USD\"}\n</code></pre>\n<p>The canonical string is:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>POST\n/v1/sessions\n1759759391\n&lt;sha256 hex of the exact body above&gt;\n</code></pre><p>and <code>X-Signature = HMAC_SHA256(sk_test_…, canonical)</code> in lowercase hex.</p>\n<h2 id=\"4-timestamp-and-replay-protection\">4. Timestamp and replay protection</h2>\n<p><code>X-Timestamp</code> must be within <strong>±300 seconds</strong> of our clock. Outside that window the request is rejected with <code>401 timestamp_out_of_window</code>, so a captured request cannot be replayed later.</p>\n<ul>\n<li>Send <strong>seconds</strong>, not milliseconds: <code>Math.floor(Date.now() / 1000)</code>.</li>\n<li>Keep your servers' clocks synchronized (NTP).</li>\n<li>Use the same value in the header and in the canonical string.</li>\n</ul>\n<h2 id=\"5-reference-implementations\">5. Reference implementations</h2>\n<h3 id=\"nodejs\">Node.js</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-javascript\">const crypto = require('crypto');\n\nfunction signCoralixRequest({ method, path, body = '', secretKey }) {\n  const timestamp = Math.floor(Date.now() / 1000).toString();\n  const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');\n  const canonical = [method.toUpperCase(), path, timestamp, bodyHash].join('\\n');\n  const signature = crypto.createHmac('sha256', secretKey).update(canonical).digest('hex');\n  return { 'X-Timestamp': timestamp, 'X-Signature': signature };\n}\n\n// Serialize once, sign that string, send that string.\nconst body = JSON.stringify({ order_id: 'order_123', amount: 15.2, currency: 'USD' });\nconst headers = {\n  'Content-Type': 'application/json',\n  'X-API-Key': process.env.CORALIX_PUBLIC_KEY,\n  ...signCoralixRequest({ method: 'POST', path: '/v1/sessions', body, secretKey: process.env.CORALIX_SECRET_KEY }),\n};\nconst res = await fetch('https://api.coralix.tech/v1/sessions', { method: 'POST', headers, body });\n</code></pre>\n<h3 id=\"postman-pre-request-script\">Postman pre-request script</h3>\n<p>Set the collection variables <code>coralix_public_key</code> and <code>coralix_secret_key</code> to your <strong>test</strong> keys, and put this in the collection's <strong>Pre-request Script</strong> tab so every request is signed:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-javascript\">const publicKey = pm.variables.get('coralix_public_key');\nconst secretKey = pm.variables.get('coralix_secret_key');\nif (!publicKey || !secretKey) {\n  throw new Error('Set the coralix_public_key and coralix_secret_key variables');\n}\nconst method = pm.request.method.toUpperCase();\n// PATH including /v1, WITHOUT the query string\nconst path = pm.variables.replaceIn('/' + pm.request.url.path.join('/'));\nconst rawBody = pm.request.body &amp;&amp; pm.request.body.raw ? pm.request.body.raw : '';\nconst body = rawBody ? pm.variables.replaceIn(rawBody) : '';\nconst timestamp = Math.floor(Date.now() / 1000).toString();\nconst bodyHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);\nconst canonical = [method, path, timestamp, bodyHash].join('\\n');\nconst signature = CryptoJS.HmacSHA256(canonical, secretKey).toString(CryptoJS.enc.Hex);\npm.request.headers.upsert({ key: 'X-API-Key', value: publicKey });\npm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp });\npm.request.headers.upsert({ key: 'X-Signature', value: signature });\n</code></pre>\n<h3 id=\"a-full-signed-request\">A full signed request</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-http\">POST /v1/sessions HTTP/1.1\nHost: api.coralix.tech\nContent-Type: application/json\nIdempotency-Key: order_123-create\nX-API-Key: pk_test_fecc9278094f9fa2b24197fed9d6647f\nX-Timestamp: 1759759391\nX-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n\n{\"order_id\":\"order_123\",\"amount\":15.20,\"currency\":\"USD\",\"success_url\":\"https://yoursite.com/success\",\"failure_url\":\"https://yoursite.com/failure\"}\n</code></pre>\n<h2 id=\"6-idempotency-key\">6. <code>Idempotency-Key</code></h2>\n<p>Send a unique <code>Idempotency-Key</code> on <code>POST /v1/sessions</code>, <code>/pay</code>, <code>/withdrawal</code> and <code>/capture</code> so that a retry after a timeout never runs the operation twice.</p>\n<ul>\n<li><strong>Same key, same request</strong>: you get the stored response, with the same status and body, plus the header <code>Idempotent-Replayed: true</code>. Nothing runs again. This includes a declined payment: retrying it returns the same decline and does not charge again.</li>\n<li><strong>Same key, different request</strong> (another body or another endpoint): <code>409 idempotency_conflict</code>, and nothing runs. Use a new key for every new operation.</li>\n<li><strong>Same key while the first request is still running</strong>: <code>409 idempotency_in_progress</code>. Wait a moment and retry with the same key.</li>\n<li><strong>The first request failed</strong> (any <code>4xx</code> or <code>5xx</code>): nothing is stored, so you can retry with the same key.</li>\n<li>Keys are scoped to your account <strong>and environment</strong>: the same value used with your test key and with your production key are two different keys.</li>\n<li>A successful response is kept for <strong>24 hours</strong>. After that, the key is treated as new.</li>\n<li><code>503 idempotency_store_unavailable</code> means we cannot honour the key right now. The request was <strong>not</strong> executed. Retry shortly with the same key.</li>\n</ul>\n<blockquote>\n<p>Refunds (<code>POST /v1/transactions/{id}/refund</code>) <strong>require</strong> <code>Idempotency-Key</code> and keep it permanently: repeating a key returns that refund's outcome, failure included.</p>\n</blockquote>\n<p>Independently of this header, <code>order_id</code> is unique per account and environment (see <strong>Payment Flow → Idempotency</strong>).</p>\n<h2 id=\"7-authentication-errors\">7. Authentication errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>invalid_request</code></td>\n<td><code>X-Timestamp</code> is not Unix seconds, or <code>X-Signature</code> is not 64 lowercase hex characters</td>\n</tr>\n<tr>\n<td><code>401</code></td>\n<td><code>invalid_authentication</code></td>\n<td>A header is missing, the key is unknown or revoked, or the signature does not match</td>\n</tr>\n<tr>\n<td><code>401</code></td>\n<td><code>timestamp_out_of_window</code></td>\n<td><code>X-Timestamp</code> is more than ±300 seconds from our clock</td>\n</tr>\n</tbody>\n</table>\n</div><pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"error_code\": \"invalid_authentication\",\n  \"message\": \"Invalid authentication\",\n  \"request_id\": \"b31ef3b8-5445-4046-abc1-3292389d6867\"\n}\n</code></pre>\n<p>An unknown key and a bad signature return the <strong>same</strong> error on purpose, so that nobody can probe which keys exist. When a request fails authentication, check in this order: the clock, the path (with <code>/v1</code>, without the query), and the body bytes.</p>\n","_postman_id":"e73419de-30d6-4059-8672-66ec02d790b2"},{"name":"PAYMENT FLOW","item":[],"id":"dee212b1-bb84-4779-8e35-50f05201d8eb","description":"<p>A payment goes through a <strong>session</strong>: you create it from your backend, the buyer pays it, and a webhook tells you the outcome.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>your backend                 CORALIX                      buyer\n     │  POST /v1/sessions        │                            │\n     │──────────────────────────►│                            │\n     │  session + checkout_url   │                            │\n     │◄──────────────────────────│                            │\n     │                                                        │\n     │  Option A: redirect the buyer to checkout_url ────────►│\n     │  Option B: POST /v1/sessions/{id}/pay with card data   │\n     │──────────────────────────►│                            │\n     │                           │  (3-D Secure, if needed)   │\n     │                           │◄──────────────────────────►│\n     │  webhook: payment.success / payment.failed / …         │\n     │◄──────────────────────────│                            │\n</code></pre><h2 id=\"1-create-a-session\">1. Create a session</h2>\n<p><code>POST /v1/sessions</code></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"order_id\": \"order_123\",\n  \"amount\": 15.20,\n  \"currency\": \"USD\",\n  \"customer_id\": \"cust_abc123\",\n  \"success_url\": \"https://yoursite.com/success\",\n  \"failure_url\": \"https://yoursite.com/failure\",\n  \"cancel_url\": \"https://yoursite.com/cancel\",\n  \"metadata\": { \"plan\": \"premium\" }\n}\n</code></pre>\n<p>Only <code>order_id</code>, <code>amount</code> and <code>currency</code> are required. The response (<code>201</code>) is the session in <code>pending</code>, with:</p>\n<ul>\n<li><strong><code>checkout_url</code></strong>: a hosted payment page you can redirect the buyer to.</li>\n<li><strong><code>payment_methods</code></strong>: the methods available to you in this environment. Each has an <code>id</code> you pass to <code>/pay</code> as <code>payment_method_config_id</code>.</li>\n<li><strong><code>expires_at</code></strong>: a session expires <strong>30 minutes</strong> after creation if it is not paid.</li>\n</ul>\n<h2 id=\"2-process-the-payment\">2. Process the payment</h2>\n<p>If you collect card data yourself instead of using <code>checkout_url</code>:</p>\n<p><code>POST /v1/sessions/{sessionId}/pay</code></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"payment_method_config_id\": \"c0658ab5-0592-49e4-a6c4-b469b88091c3\",\n  \"payment_data\": {\n    \"card_number\": \"4242424242424242\",\n    \"exp_month\": 12,\n    \"exp_year\": 2030,\n    \"cvv\": \"123\",\n    \"holder_name\": \"John Doe\"\n  }\n}\n</code></pre>\n<p>Collecting card data yourself puts your systems in PCI DSS scope. If you are not certified, use <code>checkout_url</code>.</p>\n<p><code>/pay</code> answers <strong><code>200</code></strong> with one of these <code>status</code> values:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>status</code></th>\n<th>Meaning</th>\n<th>What to do</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>success</code></td>\n<td>The money moved</td>\n<td>Fulfil the order (and confirm with the webhook)</td>\n</tr>\n<tr>\n<td><code>authorized</code></td>\n<td>Funds are held but not yet captured</td>\n<td>Capture with <code>POST /v1/transactions/{id}/capture</code></td>\n</tr>\n<tr>\n<td><code>requires_action</code></td>\n<td>The buyer must complete a step, usually 3-D Secure</td>\n<td>Follow <code>action</code> (see §3)</td>\n</tr>\n<tr>\n<td><code>failed</code></td>\n<td>Declined</td>\n<td>Show <code>decline_message</code> to the buyer. A declined payment is not an error.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Approved:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"session_id\": \"ec00c38e-e3d7-4943-9ecd-172835c333bf\",\n  \"transaction_id\": \"b6eee137-2b06-4054-be27-3d81a1944249\",\n  \"status\": \"success\",\n  \"provider_ref\": \"PSP-1783967474304\",\n  \"payment_data\": {\n    \"card_number\": \"424242******4242\",\n    \"authorization_code\": \"GD2DA1\",\n    \"descriptor\": \"CORALIX*YOURSHOP\"\n  }\n}\n</code></pre>\n<p>Declined:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"session_id\": \"ec00c38e-e3d7-4943-9ecd-172835c333bf\",\n  \"transaction_id\": \"b6eee137-2b06-4054-be27-3d81a1944249\",\n  \"status\": \"failed\",\n  \"error_code\": \"payment_declined\",\n  \"decline_reason\": \"insufficient_funds\",\n  \"decline_message\": \"Insufficient funds. Please try with another card.\"\n}\n</code></pre>\n<p>Branch on <code>decline_reason</code>, never on <code>decline_message</code>. Send <code>Accept-Language: es</code> to receive <code>decline_message</code> in Spanish. See <strong>Decline Reasons Reference</strong>.</p>\n<p>Some payment methods require more payer data in <code>payment_data</code>: billing address, contact details or the buyer's IP address. If something required is missing, <code>/pay</code> answers <code>422 payer_data_required</code> with <code>missing_fields[]</code>, and the session stays <code>pending</code> so you can retry. See <strong>Endpoints → Pay a session</strong> for every field.</p>\n<h2 id=\"3-3-d-secure-and-other-actions\">3. 3-D Secure and other actions</h2>\n<p>When <code>/pay</code> answers <code>requires_action</code>, the response carries an <code>action</code> object that says what the buyer must do. You have <strong>15 minutes</strong> (<code>action.expires_at</code>).</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"session_id\": \"…\",\n  \"transaction_id\": \"…\",\n  \"status\": \"requires_action\",\n  \"action\": {\n    \"type\": \"redirect\",\n    \"url\": \"https://acs.bank.example/challenge\",\n    \"method\": \"POST\",\n    \"params\": { \"PaReq\": \"eJxVUts…\", \"MD\": \"…\" },\n    \"expires_at\": \"2026-10-06T14:18:11.000Z\"\n  }\n}\n</code></pre>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>action.type</code></th>\n<th>What to do</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>redirect</code></td>\n<td>Send the buyer to <code>url</code> with <code>method</code>. For <code>POST</code>, submit <code>params</code> as a form.</td>\n</tr>\n<tr>\n<td><code>voucher</code></td>\n<td>Show <code>voucher_code</code> (and <code>barcode_url</code> if present) to the buyer</td>\n</tr>\n<tr>\n<td><code>deposit</code></td>\n<td>Show the <code>address</code> (and <code>network</code>, <code>amount</code>, <code>memo</code>) to pay to</td>\n</tr>\n<tr>\n<td><code>otp</code></td>\n<td>Ask the buyer for the code sent to <code>sent_to</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>When the buyer finishes, they come back to your <code>success_url</code> or <code>failure_url</code>. <strong>The redirect does not prove anything</strong>: wait for the webhook, or check <code>GET /v1/sessions/{id}/status</code>, before fulfilling the order.</p>\n<p>If you lose the <code>/pay</code> response, <code>GET /v1/sessions/{id}/status</code> returns the pending <code>action</code> again.</p>\n<h2 id=\"4-know-the-outcome\">4. Know the outcome</h2>\n<p>Two ways, and the first one is the source of truth:</p>\n<ul>\n<li><strong>Webhooks</strong>: we <code>POST</code> a signed event to your endpoint (<code>payment.success</code>, <code>payment.failed</code>, <code>payment.authorized</code>, <code>payment.requires_action</code>, <code>payment.expired</code>, …). See <strong>Webhooks</strong>.</li>\n<li><strong>Polling</strong>: <code>GET /v1/sessions/{sessionId}/status</code> returns the session status, any pending action, and its transactions.</li>\n</ul>\n<h2 id=\"5-session-and-transaction-statuses\">5. Session and transaction statuses</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Status</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>pending</code></td>\n<td>Created, not paid yet</td>\n</tr>\n<tr>\n<td><code>processing</code></td>\n<td>A payment attempt is in progress</td>\n</tr>\n<tr>\n<td><code>requires_action</code></td>\n<td>Waiting for the buyer (3-D Secure, voucher, …)</td>\n</tr>\n<tr>\n<td><code>authorized</code></td>\n<td>Funds held, awaiting capture</td>\n</tr>\n<tr>\n<td><code>success</code></td>\n<td>The money moved</td>\n</tr>\n<tr>\n<td><code>failed</code></td>\n<td>Declined</td>\n</tr>\n<tr>\n<td><code>expired</code></td>\n<td>Not paid within 30 minutes, or the action was not completed in 15</td>\n</tr>\n<tr>\n<td><code>refunded</code></td>\n<td>Fully refunded</td>\n</tr>\n<tr>\n<td><code>chargebacked</code></td>\n<td>The buyer's bank reversed the payment</td>\n</tr>\n</tbody>\n</table>\n</div><p>A session that is no longer <code>pending</code> cannot be paid again (<code>400 session_invalid_state</code>). To retry after a decline, create a new session with a new <code>order_id</code>.</p>\n<h2 id=\"6-idempotency\">6. Idempotency</h2>\n<ul>\n<li><strong><code>order_id</code></strong> is unique per account and environment. Creating a session again with the same <code>order_id</code>, <code>amount</code> and <code>currency</code> returns the existing session: retries are safe and never create a duplicate. The same <code>order_id</code> with a different amount or currency returns <code>409 idempotency_conflict</code> with the existing <code>session_id</code>.</li>\n<li><strong><code>Idempotency-Key</code></strong> protects <code>/pay</code>, <code>/withdrawal</code> and <code>/capture</code> against double execution. See <strong>Authentication → Idempotency-Key</strong>.</li>\n</ul>\n<h2 id=\"7-errors\">7. Errors</h2>\n<p>Errors always have the shape <code>{ \"error_code\", \"message\", \"request_id\" }</code>. Declines are <strong>not</strong> errors: <code>/pay</code> answers them with <code>200</code> and <code>status: \"failed\"</code>. See <strong>Error Codes</strong>.</p>\n","_postman_id":"dee212b1-bb84-4779-8e35-50f05201d8eb"},{"name":"TESTING","item":[],"id":"f806fbc5-df9f-42ca-92c5-4ae16ca9ed13","description":"<p>With a <strong>test</strong> key (<code>pk_test_…</code>), payments go to a simulated provider. The values below force each outcome, so you can test every branch of your integration without real money.</p>\n<blockquote>\n<p>Simulated providers are never reachable with a production key. The test cards below are declined in production, as any invalid card would be. </p>\n</blockquote>\n<h3 id=\"approved-card\">Approved card</h3>\n<p>Use any future expiry date, any 3-digit CVV and any holder name.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Card number</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>4242 4242 4242 4242</code></td>\n<td><code>status: \"success\"</code> (or <code>authorized</code>, if your account captures manually)</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"declined-cards\">Declined cards</h3>\n<p>Each card forces a specific decline. <code>/pay</code> answers <code>200</code> with <code>status: \"failed\"</code>, a <code>decline_reason</code> and a <code>decline_message</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Card number</th>\n<th><code>decline_reason</code></th>\n<th><code>decline_message</code> (en)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>4000 0000 0000 9995</code></td>\n<td><code>insufficient_funds</code></td>\n<td>Insufficient funds. Please try with another card.</td>\n</tr>\n<tr>\n<td><code>4000 0000 0000 0002</code></td>\n<td><code>do_not_honor</code></td>\n<td>Your bank declined the payment. Please contact your bank.</td>\n</tr>\n<tr>\n<td><code>4000 0000 0000 0069</code></td>\n<td><code>expired_card</code></td>\n<td>Your card has expired. Please use a valid card.</td>\n</tr>\n<tr>\n<td><code>4000 0000 0000 0127</code></td>\n<td><code>incorrect_cvv</code></td>\n<td>Incorrect CVV. Please check your security code.</td>\n</tr>\n<tr>\n<td><code>4100 0000 0000 0019</code></td>\n<td><code>fraud_suspected</code></td>\n<td>Transaction declined for security reasons.</td>\n</tr>\n<tr>\n<td><code>4000 0000 0000 4854</code></td>\n<td><code>lost_card</code></td>\n<td>Card reported as lost.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Any other well-formed card number is declined as <code>do_not_honor</code>.</p>\n<h3 id=\"3-d-secure\">3-D Secure</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Card number</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>4000 0000 0000 3220</code></td>\n<td><code>status: \"requires_action\"</code> with a <code>redirect</code> action to a simulated challenge page</td>\n</tr>\n</tbody>\n</table>\n</div><p>On the challenge page, enter one of these codes:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>0000</code></td>\n<td>Authentication passes; the payment becomes <code>success</code> (or <code>authorized</code>)</td>\n</tr>\n<tr>\n<td><code>1111</code></td>\n<td>Declined: <code>3ds_failed</code></td>\n</tr>\n<tr>\n<td><code>2222</code></td>\n<td>Declined: <code>do_not_honor</code></td>\n</tr>\n<tr>\n<td><code>3333</code></td>\n<td>Declined: <code>insufficient_funds</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>The outcome arrives by webhook, and the buyer is redirected to your <code>success_url</code> or <code>failure_url</code>. This is the flow to test that you wait for the webhook rather than trusting the redirect.</p>\n<h3 id=\"invalid-card-data\">Invalid card data</h3>\n<p>Malformed <code>payment_data</code> never reaches the provider. <code>/pay</code> answers <strong><code>422 invalid_payment_data</code></strong>, listing each problem in <code>errors[]</code>, and the session <strong>stays</strong> <strong><code>pending</code></strong>, so you can fix the data and call <code>/pay</code> again.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Condition</th>\n<th>Message in <code>errors[]</code></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Card number not 13 to 19 digits</td>\n<td><code>card_number must be 13 to 19 digits</code></td>\n</tr>\n<tr>\n<td>Month not between 1 and 12</td>\n<td><code>exp_month must be between 1 and 12</code></td>\n</tr>\n<tr>\n<td>Year not a four-digit year</td>\n<td><code>exp_year must be a four-digit year</code></td>\n</tr>\n<tr>\n<td>Expiry in the past</td>\n<td><code>exp_year: card has expired</code></td>\n</tr>\n<tr>\n<td>CVV not 3 or 4 digits</td>\n<td><code>cvv must be 3 or 4 digits</code></td>\n</tr>\n<tr>\n<td>Missing holder name</td>\n<td><code>holder_name is required</code></td>\n</tr>\n</tbody>\n</table>\n</div><pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"error_code\": \"invalid_payment_data\",\n  \"message\": \"payment_data does not match the expected format\",\n  \"request_id\": \"6f1c2e0a-3b5d-4f8e-9a71-2c4d5e6f7a8b\",\n  \"errors\": [\"cvv must be 3 or 4 digits\"]\n}\n\n</code></pre>\n<p>Spaces and hyphens in <code>card_number</code> are accepted and removed. A two-digit <code>exp_year</code> such as <code>30</code> is read as <code>2030</code>.</p>\n<h3 id=\"withdrawals\">Withdrawals</h3>\n<p>In <code>test</code>, the <strong>cents</strong> of the session amount decide the outcome of a withdrawal:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Amount ends in</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>.13</code> (e.g. <code>25.13</code>)</td>\n<td><code>failed</code></td>\n</tr>\n<tr>\n<td><code>.01</code> (e.g. <code>25.01</code>)</td>\n<td>stays <code>processing</code>. Use it to test how you handle a payout that has not resolved yet.</td>\n</tr>\n<tr>\n<td>anything else</td>\n<td><code>success</code></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"example-approved-payment\">Example: approved payment</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-http\">POST /v1/sessions/{sessionId}/pay\n\n</code></pre>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"payment_method_config_id\": \"&lt;id from the session&amp;#x27;s payment_methods&gt;\",\n  \"payment_data\": {\n    \"card_number\": \"4242424242424242\",\n    \"exp_month\": 12,\n    \"exp_year\": 2030,\n    \"cvv\": \"123\",\n    \"holder_name\": \"John Doe\"\n  }\n}\n\n</code></pre>\n<p>Send <code>Accept-Language: es</code> to receive <code>decline_message</code> in Spanish.</p>\n","_postman_id":"f806fbc5-df9f-42ca-92c5-4ae16ca9ed13"},{"name":"WEBHOOKS","item":[],"id":"ab9bd348-84fa-4dd0-acb9-42709f5d8408","description":"<p>Coralix sends a signed <code>POST</code> to your endpoint whenever a payment, refund or withdrawal changes state. <strong>The webhook, not the browser redirect, is the source of truth</strong> for the outcome of a payment: the redirect to your <code>success_url</code> only means the buyer came back.</p>\n<h3 id=\"configure-your-endpoint\">Configure your endpoint</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-http\">POST /v1/webhooks/config\n{ \"endpoint_url\": \"https://yoursite.com/webhooks/coralix\" }\n\n</code></pre>\n<ul>\n<li>Each <strong>environment has its own endpoint</strong>. Configuring with a <code>pk_test_…</code> key sets the test endpoint; configuring with a <code>pk_production_…</code> key sets the production one. Test events never reach your production endpoint.</li>\n<li>The first time you configure an endpoint, the response includes its <strong><code>signing_secret</code></strong> (<code>whsec_…</code>). <strong>It is shown only once.</strong> Store it like a password.</li>\n<li>Changing the URL later keeps the same secret and does not return it.</li>\n</ul>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"0c3f…\",\n  \"merchant_id\": \"25e2…\",\n  \"environment\": \"test\",\n  \"endpoint_url\": \"https://yoursite.com/webhooks/coralix\",\n  \"enabled\": true,\n  \"signing_secret\": \"whsec_4f1c…\"\n}\n\n</code></pre>\n<h3 id=\"events\">Events</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>event_type</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>payment.authorized</code></td>\n<td>Funds are held, awaiting capture</td>\n</tr>\n<tr>\n<td><code>payment.success</code></td>\n<td>The money moved</td>\n</tr>\n<tr>\n<td><code>payment.failed</code></td>\n<td>Declined; carries <code>decline_reason</code> and <code>decline_message</code></td>\n</tr>\n<tr>\n<td><code>payment.requires_action</code></td>\n<td>The buyer must complete a step, such as 3-D Secure</td>\n</tr>\n<tr>\n<td><code>payment.expired</code></td>\n<td>The session expired unpaid</td>\n</tr>\n<tr>\n<td><code>payment.refunded</code></td>\n<td>A refund was completed; carries <code>refund_id</code> and <code>total_refunded</code></td>\n</tr>\n<tr>\n<td><code>payment.refund_failed</code></td>\n<td>A refund was rejected</td>\n</tr>\n<tr>\n<td><code>withdrawal.completed</code> / <code>withdrawal.failed</code> / <code>withdrawal.expired</code></td>\n<td>The outcome of a payout</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"payload\">Payload</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"9b2d…\",\n  \"event_type\": \"payment.success\",\n  \"api_version\": \"2024-01\",\n  \"created_at\": \"2026-10-06T14:03:11.000Z\",\n  \"environment\": \"production\",\n  \"data\": {\n    \"object\": \"payment_session\",\n    \"id\": \"44b45c13-…\",\n    \"merchant_id\": \"25e2…\",\n    \"order_id\": \"order_123\",\n    \"amount\": \"15.20\",\n    \"currency\": \"USD\",\n    \"status\": \"success\",\n    \"provider_ref\": \"…\",\n    \"metadata\": { \"plan\": \"premium\" }\n  }\n}\n\n</code></pre>\n<p><strong>Check</strong> <strong><code>environment</code></strong> <strong>before touching any balance.</strong> A test event must never credit real money.</p>\n<h3 id=\"verifying-the-signature\">Verifying the signature</h3>\n<p>Every delivery carries these headers:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Header</th>\n<th>Content</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>X-Coralix-Signature</code></td>\n<td><code>t=,v1=</code>. After a secret rotation it can carry two <code>v1</code> values.</td>\n</tr>\n<tr>\n<td><code>X-Coralix-Event</code></td>\n<td>The <code>event_type</code></td>\n</tr>\n<tr>\n<td><code>X-Coralix-Delivery</code></td>\n<td>The delivery id. Deduplicate on it: a delivery can arrive more than once.</td>\n</tr>\n</tbody>\n</table>\n</div><p>To verify a delivery:</p>\n<ol>\n<li>Read <code>t</code> and every <code>v1</code> from <code>X-Coralix-Signature</code>.</li>\n<li>Compute <code>HMAC-SHA256(signing_secret, t + \".\" + raw_body)</code> in lowercase hex. Use the <strong>raw request body</strong> exactly as received; do not parse and re-serialize it.</li>\n<li>Accept the delivery if your value equals <strong>any</strong> of the <code>v1</code> values. Compare in constant time.</li>\n<li>Reject it if <code>t</code> is more than 5 minutes from your clock, to stop replays.</li>\n</ol>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-javascript\">const crypto = require('crypto');\nfunction verifyCoralixWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {\n  const parts = signatureHeader.split(',').map((p) =&gt; p.split('='));\n  const t = Number(parts.find(([k]) =&gt; k === 't')?.[1]);\n  const signatures = parts.filter(([k]) =&gt; k === 'v1').map(([, v]) =&gt; v);\n  if (!t || Math.abs(Date.now() / 1000 - t) &gt; toleranceSec) return false;\n  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');\n  return signatures.some(\n    (sig) =&gt;\n      sig.length === expected.length &amp;&amp;\n      crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex')),\n  );\n}\n// Express: keep the raw body for this route\napp.post('/webhooks/coralix', express.raw({ type: 'application/json' }), (req, res) =&gt; {\n  const ok = verifyCoralixWebhook(\n    req.body.toString('utf8'),\n    req.get('X-Coralix-Signature'),\n    process.env.CORALIX_WEBHOOK_SECRET,\n  );\n  if (!ok) return res.status(400).end();\n  const event = JSON.parse(req.body);\n  // … handle event, deduplicating on X-Coralix-Delivery\n  res.status(200).end();\n});\n\n</code></pre>\n<h3 id=\"rotating-the-secret\">Rotating the secret</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-http\">POST /v1/webhooks/config/rotate-secret\n\n</code></pre>\n<p>The response returns the new <code>signing_secret</code>, once, and <code>previous_secret_expires_at</code>. For the next <strong>24 hours</strong> every delivery is signed with <strong>both</strong> secrets, so you can deploy the new one without losing events. After that, only the new one signs.</p>\n<p>Rotation applies to the endpoint of the key's environment.</p>\n<h3 id=\"retries\">Retries</h3>\n<ul>\n<li>Any <code>2xx</code> answer counts as delivered. Answer quickly and process asynchronously; the timeout is 5 seconds.</li>\n<li>A failed delivery is retried up to 5 times in total, after 1 minute, 5 minutes, 30 minutes and 2 hours.</li>\n<li><code>GET /v1/webhooks</code> lists your deliveries, <code>GET /v1/webhooks/{id}</code> shows one, and <code>POST /v1/webhooks/{id}/retry</code> sends one again. Retrying a delivery that already succeeded returns <code>400 webhook_already_delivered</code>.</li>\n</ul>\n","_postman_id":"ab9bd348-84fa-4dd0-acb9-42709f5d8408"},{"name":"REFUNDS","item":[],"id":"156cd8d0-d5e1-4e75-bf78-8f14d7cb103d","description":"<p>A refund gives a captured card payment back to the buyer, in full or in part. <strong>It is accepted when you request it and completed later</strong>: the issuer resolves it, sometimes days afterwards, and a webhook tells you the outcome.</p>\n<h3 id=\"request-a-refund\">Request a refund</h3>\n<p><code>POST /v1/transactions/{transactionId}/refund</code>. The <code>Idempotency-Key</code> header is <strong>required</strong>.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"amount\": \"5.00\",\n  \"reason\": \"Customer returned one item\"\n}\n</code></pre>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Required</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>amount</code></td>\n<td>No</td>\n<td>A <strong>string</strong> with up to two decimals. If you omit it, we refund <strong>everything still available</strong>, which is not the whole payment if part of it was already refunded.</td>\n</tr>\n<tr>\n<td><code>reason</code></td>\n<td>No</td>\n<td>Free text, up to 500 characters, for your records</td>\n</tr>\n</tbody>\n</table>\n</div><p>The answer is <strong><code>202 Accepted</code></strong> with the refund in <code>pending</code>:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"7d1f…\",\n  \"transaction_id\": \"b6eee137-2b06-4054-be27-3d81a1944249\",\n  \"amount\": \"5.00\",\n  \"currency\": \"USD\",\n  \"status\": \"pending\",\n  \"reason\": \"Customer returned one item\",\n  \"provider_ref\": null,\n  \"acquirer_reference_number\": null,\n  \"failure_reason\": null,\n  \"origin\": \"api\",\n  \"resolved_at\": null,\n  \"created_at\": \"2026-10-06T14:03:11.000Z\"\n}\n</code></pre>\n<h3 id=\"statuses\">Statuses</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>status</code></th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>pending</code></td>\n<td>Accepted, not yet sent to the issuer</td>\n</tr>\n<tr>\n<td><code>submitted</code></td>\n<td>Sent to the issuer, awaiting its answer</td>\n</tr>\n<tr>\n<td><code>unknown</code></td>\n<td>We could not confirm whether the issuer received it. It is followed up; do not request it again.</td>\n</tr>\n<tr>\n<td><code>succeeded</code></td>\n<td>The money went back to the buyer</td>\n</tr>\n<tr>\n<td><code>failed</code></td>\n<td>The issuer rejected it. The amount becomes available to refund again.</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"know-the-outcome\">Know the outcome</h3>\n<p>When the refund is resolved we send a webhook:</p>\n<ul>\n<li><strong><code>payment.refunded</code></strong>: <code>data.amount</code> is this refund's amount, <code>data.refund_id</code> identifies it, and <code>data.total_refunded</code> is everything returned on the payment so far. <code>data.acquirer_reference_number</code> is the reference the buyer can give their bank to trace the money, when the issuer provides one.</li>\n<li><strong><code>payment.refund_failed</code></strong>: <code>data.decline_reason</code> carries the issuer's reason.</li>\n</ul>\n<p>A <strong>full</strong> refund moves the payment to <code>refunded</code> when it completes. A <strong>partial</strong> refund does not change the payment's status.</p>\n<h3 id=\"rules\">Rules</h3>\n<ul>\n<li><strong>Only captured card payments</strong> can be refunded. A payment still <code>authorized</code> was never captured: there is nothing to give back.</li>\n<li><strong>You can refund in several parts</strong>, up to the captured amount. Amounts already requested count against that limit while they are pending; if a refund fails, its amount is freed again.</li>\n<li><strong>A payment with a chargeback cannot be refunded</strong>: the buyer's bank already reversed it, and refunding would return the money twice.</li>\n<li><strong>The key identifies one attempt.</strong> Repeating a key returns that refund as it stands, failure included. A new attempt, even for the same amount, needs a new key.</li>\n</ul>\n<h3 id=\"errors\">Errors</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>idempotency_key_required</code></td>\n<td>The <code>Idempotency-Key</code> header is missing</td>\n</tr>\n<tr>\n<td><code>404</code></td>\n<td><code>resource_not_found</code></td>\n<td>The transaction does not exist, is not yours, or is of the other environment</td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>idempotency_conflict</code></td>\n<td>The key was used for another amount, another transaction, or a refund that failed</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_amount_invalid</code></td>\n<td><code>amount</code> is not positive</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_amount_exceeded</code></td>\n<td>More than what is still available, or nothing is left</td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>refund_amount_exceeded</code></td>\n<td>Another refund took the available amount first</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_not_allowed</code></td>\n<td>The payment cannot be refunded; <code>reason</code> says why: <code>not_card</code>, <code>not_captured</code>, <code>invalid_status</code>, <code>acquirer_reversed</code> or <code>chargebacked</code></td>\n</tr>\n</tbody>\n</table>\n</div>","_postman_id":"156cd8d0-d5e1-4e75-bf78-8f14d7cb103d"},{"name":"WITHDRAWALS","item":[],"id":"43b3ba03-828b-41e6-bfc1-5b00744b506f","description":"<p>A withdrawal (payout) sends money <strong>from</strong> your account <strong>to</strong> a card, a bank account or a wallet. It uses the same sessions as payments, with <code>type: \"withdrawal\"</code>, but it has its own endpoint, methods and events.</p>\n<blockquote>\n<p>Withdrawals must be enabled for your account. If no withdrawal method is configured in any environment, creating a withdrawal session returns <code>422 no_withdrawal_rails</code>.</p>\n</blockquote>\n<h3 id=\"1-create-a-withdrawal-session\">1. Create a withdrawal session</h3>\n<p><code>POST /v1/sessions</code></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"order_id\": \"payout_789\",\n  \"type\": \"withdrawal\",\n  \"amount\": 25.00,\n  \"currency\": \"USD\",\n  \"metadata\": { \"beneficiary\": \"user_42\" }\n}\n</code></pre>\n<p>The response has <strong>no <code>checkout_url</code></strong>, because there is no payer to send anywhere. Instead it lists the methods you can pay out through:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"5a0b…\",\n  \"type\": \"withdrawal\",\n  \"status\": \"pending\",\n  \"amount\": \"25.00\",\n  \"currency\": \"USD\",\n  \"withdrawal_methods\": [\n    { \"id\": \"9c7e…\", \"display_name\": \"Bank transfer\", \"withdrawal_method_type\": \"bank\" }\n  ],\n  \"withdrawal_methods_available\": true\n}\n</code></pre>\n<p><code>withdrawal_methods_available: false</code> means no method is configured <strong>in this environment</strong> yet.</p>\n<h3 id=\"2-send-the-payout\">2. Send the payout</h3>\n<p><code>POST /v1/sessions/{sessionId}/withdrawal</code>. Send an <code>Idempotency-Key</code>: a payout repeated by mistake is money sent twice to someone else.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"withdrawal_method_config_id\": \"9c7e…\",\n  \"withdrawal_data\": {\n    \"account_holder_name\": \"Ada Lovelace\",\n    \"account_number\": \"000123456789\",\n    \"bank_code\": \"BANKUS33\",\n    \"country\": \"US\"\n  }\n}\n</code></pre>\n<p><code>withdrawal_data</code> depends on the type of the method you chose:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Type</th>\n<th>Required</th>\n<th>Optional</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>card</code></td>\n<td><code>card_number</code>, <code>exp_month</code>, <code>exp_year</code>, <code>holder_name</code></td>\n<td></td>\n</tr>\n<tr>\n<td><code>bank</code></td>\n<td><code>account_holder_name</code>, <code>account_number</code>, <code>bank_code</code>, <code>country</code></td>\n<td><code>iban</code>, <code>routing_number</code>, <code>branch_code</code></td>\n</tr>\n<tr>\n<td><code>wallet</code></td>\n<td><code>wallet_id</code></td>\n<td><code>provider</code>, <code>phone</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>If something is missing, the answer is <code>422 withdrawal_data_invalid</code> with <code>errors[]</code>, and the session stays <code>pending</code>.</p>\n<p>The response:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"e41c…\",\n  \"session_id\": \"5a0b…\",\n  \"status\": \"success\",\n  \"amount\": \"25.00\",\n  \"currency\": \"USD\",\n  \"provider_ref\": \"PO-88213\",\n  \"error_code\": null,\n  \"created_at\": \"2026-10-06T14:03:11.000Z\"\n}\n</code></pre>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>status</code></th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>success</code></td>\n<td>The money was sent</td>\n</tr>\n<tr>\n<td><code>processing</code></td>\n<td>The provider accepted it and will confirm later. <strong>This is not a failure</strong>; wait for the webhook.</td>\n</tr>\n<tr>\n<td><code>failed</code></td>\n<td>Rejected. <code>error_code</code> carries the provider's code.</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"3-know-the-outcome\">3. Know the outcome</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Event</th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>withdrawal.completed</code></td>\n<td>The money was sent</td>\n</tr>\n<tr>\n<td><code>withdrawal.failed</code></td>\n<td>The payout was rejected</td>\n</tr>\n<tr>\n<td><code>withdrawal.expired</code></td>\n<td>The session was not used within 30 minutes</td>\n</tr>\n</tbody>\n</table>\n</div><p>A withdrawal in <code>processing</code> does not expire: it is waiting for the provider, and its outcome always arrives by webhook.</p>\n<h3 id=\"errors\">Errors</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>422</code></td>\n<td><code>no_withdrawal_rails</code></td>\n<td>Withdrawals are not configured for your account</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>session_type_mismatch</code></td>\n<td><code>/withdrawal</code> on a payment session, or <code>/pay</code> on a withdrawal session</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>withdrawal_method_unavailable</code></td>\n<td>The method does not exist, is disabled, or belongs to the other environment</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>withdrawal_data_invalid</code></td>\n<td><code>withdrawal_data</code> does not match the method's type; see <code>errors[]</code></td>\n</tr>\n<tr>\n<td><code>400</code></td>\n<td><code>session_invalid_state</code></td>\n<td>The session is no longer <code>pending</code></td>\n</tr>\n</tbody>\n</table>\n</div>","_postman_id":"43b3ba03-828b-41e6-bfc1-5b00744b506f"},{"name":"ENDPOINTS","item":[{"name":"1. CREATE PAYMENT SESSION","event":[{"listen":"prerequest","script":{"id":"0d98a74a-f277-4bfa-b3c6-2e64377393c8","exec":["/*"," * Coralix gateway — HMAC request signing (Postman Pre-request Script)"," * ------------------------------------------------------------------"," * Paste this into the \"Pre-request Script\" tab of your Collection (so it runs"," * for every gateway request) or a single request."," *"," * Define these variables (Collection/Environment — keep the secret as type \"secret\"):"," *   coralix_public_key   e.g. pk_test_bet365"," *   coralix_secret_key   e.g. sk_test_bet365_secret"," *"," * It signs METHOD\\nPATH\\nTIMESTAMP\\nSHA256(body) and injects X-API-Key,"," * X-Timestamp and X-Signature automatically — no manual header rows needed."," */","//const CryptoJS = require('crypto-js');","","const publicKey = pm.variables.get('coralix_public_key');","const secretKey = pm.variables.get('coralix_secret_key');","if (!publicKey || !secretKey) {","  throw new Error('Set the coralix_public_key and coralix_secret_key variables');","}","","// METHOD (uppercase)","const method = pm.request.method.toUpperCase();","","// PATH including /v1, WITHOUT query string; resolve any {{vars}} in the path","const path = pm.variables.replaceIn('/' + pm.request.url.path.join('/'));","","// Exact body Postman will send (resolve {{vars}}); empty string for GET/DELETE","const rawBody = pm.request.body && pm.request.body.raw ? pm.request.body.raw : '';","const body = rawBody ? pm.variables.replaceIn(rawBody) : '';","","const timestamp = Math.floor(Date.now() / 1000).toString();","const bodyHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);","const canonical = [method, path, timestamp, bodyHash].join('\\n');","const signature = CryptoJS.HmacSHA256(canonical, secretKey).toString(CryptoJS.enc.Hex);","","pm.request.headers.upsert({ key: 'X-API-Key', value: publicKey });","pm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp });","pm.request.headers.upsert({ key: 'X-Signature', value: signature });","","// Handy while debugging (open the Postman Console):","console.log('[coralix] canonical:\\n' + canonical);","console.log('[coralix] X-Signature: ' + signature);",""],"type":"text/javascript","packages":{}}}],"id":"6cd72966-2f76-4504-8f81-45411c127079","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"auth":{"type":"noauth","isInherited":false},"method":"POST","header":[],"body":{"mode":"raw","raw":"{\n    \"order_id\": \"merchx_order_00001\",\n    \"amount\": 15.20,\n    \"currency\": \"USD\",\n    \"customer_id\": \"cust_abc123\",\n    \"success_url\": \"https://yoursite.com/success\",\n    \"failure_url\": \"https://yoursite.com/failure\",\n    \"cancel_url\": \"https://yoursite.com/cancel\",\n    \"metadata\": {\n        \"plan\": \"premium\",\n        \"user_tier\": \"vip\"\n    }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.coralix.tech/v1/sessions","urlObject":{"protocol":"https","path":["v1","sessions"],"host":["api","coralix","tech"],"query":[],"variable":[]}},"response":[{"id":"637f12a3-e77a-4042-8c07-b4f165e3110a","name":"1. CREATE PAYMENT SESSION - 201","originalRequest":{"method":"POST","header":[],"body":{"mode":"raw","raw":"{\n    \"order_id\": \"merchx_order_00001\",\n    \"amount\": 15.20,\n    \"currency\": \"USD\",\n    \"customer_id\": \"cust_abc123\",\n    \"success_url\": \"https://yoursite.com/success\",\n    \"failure_url\": \"https://yoursite.com/failure\",\n    \"cancel_url\": \"https://yoursite.com/cancel\",\n    \"metadata\": {\n        \"plan\": \"premium\",\n        \"user_tier\": \"vip\"\n    }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.coralix.tech/v1/sessions"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Date","value":"Mon, 13 Jul 2026 18:18:01 GMT"},{"key":"Content-Type","value":"application/json; charset=utf-8"},{"key":"Content-Length","value":"688"},{"key":"Connection","value":"keep-alive"},{"key":"x-powered-by","value":"Express"},{"key":"vary","value":"Origin"},{"key":"x-ratelimit-limit","value":"100"},{"key":"x-ratelimit-remaining","value":"99"},{"key":"x-ratelimit-reset","value":"60"},{"key":"x-trace-id","value":"4973968a-ec1b-45ba-9e7d-94bc7986bd90"},{"key":"etag","value":"W/\"2b0-HDEAqfmEITCkKSwq2X/XDsVKiRc\""},{"key":"Server","value":"cloudflare"},{"key":"x-do-app-origin","value":"c322b499-09e8-4d15-94e7-8d66106abf95"},{"key":"Cache-Control","value":"private"},{"key":"x-do-orig-status","value":"201"},{"key":"cf-cache-status","value":"DYNAMIC"},{"key":"set-cookie","value":"__cf_bm=9ER39WbIDfLB6RodOD.c0U0QAdJZZVzV05dCM4MdAS0-1783966681.3679717-1.0.1.1-ajZAeMf1QLSjpYoUB68CsmbiurW2dI.usJxJJXL1K_kmZcz6uPhR3iDl0fgTFLO5OiPxFyG8Hzr5ERCUIXiaEDbCHen_tw.MDT.jKjCp21AoxOBDAS5GGfzJQsZEOekg; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.coralix.tech; Expires=Mon, 13 Jul 2026 18:48:01 GMT"},{"key":"CF-RAY","value":"a1aa49ae8e3bc646-SJO"},{"key":"alt-svc","value":"h3=\":443\"; ma=86400"}],"cookie":[],"responseTime":null,"body":"{\n    \"id\": \"44b45c13-8b0e-4697-af40-e1ae83d8c1f9\",\n    \"merchant_id\": \"25e2cf6c-21bd-4e5e-bb7d-7f445fe80cbf\",\n    \"order_id\": \"merchx_order_00001\",\n    \"amount\": \"15.20\",\n    \"currency\": \"USD\",\n    \"status\": \"success\",\n    \"customer_id\": \"cust_abc123\",\n    \"success_url\": \"https://yoursite.com/success\",\n    \"failure_url\": \"https://yoursite.com/failure\",\n    \"cancel_url\": \"https://yoursite.com/cancel\",\n    \"metadata\": {\n        \"plan\": \"premium\",\n        \"user_tier\": \"vip\"\n    },\n    \"expires_at\": \"2026-07-13T18:23:59.200Z\",\n    \"created_at\": \"2026-07-13T17:53:59.204Z\",\n    \"checkout_url\": \"http://localhost:3000/checkout/44b45c13-8b0e-4697-af40-e1ae83d8c1f9\",\n    \"payment_methods\": [\n        {\n            \"id\": \"c0658ab5-0592-49e4-a6c4-b469b88091c3\",\n            \"display_name\": \"Tarjeta (MockCard)\",\n            \"payment_method_type\": \"card\"\n        }\n    ]\n}"},{"id":"65b232cb-b903-45d1-8d9a-2b3d13d596a2","name":"1. CREATE PAYMENT SESSION - 401","originalRequest":{"method":"POST","header":[],"body":{"mode":"raw","raw":"{\n    \"order_id\": \"merchx_order_00002\",\n    \"amount\": 15.20,\n    \"currency\": \"USD\",\n    \"customer_id\": \"cust_abc123\",\n    \"success_url\": \"https://yoursite.com/success\",\n    \"failure_url\": \"https://yoursite.com/failure\",\n    \"cancel_url\": \"https://yoursite.com/cancel\",\n    \"metadata\": {\n        \"plan\": \"premium\",\n        \"user_tier\": \"vip\"\n    }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.coralix.tech/v1/sessions"},"status":"Unauthorized","code":401,"_postman_previewlanguage":"json","header":[{"key":"Date","value":"Mon, 13 Jul 2026 18:20:40 GMT"},{"key":"Content-Type","value":"application/json; charset=utf-8"},{"key":"Content-Length","value":"126"},{"key":"Connection","value":"keep-alive"},{"key":"x-powered-by","value":"Express"},{"key":"vary","value":"Origin"},{"key":"x-ratelimit-limit","value":"100"},{"key":"x-ratelimit-remaining","value":"98"},{"key":"x-ratelimit-reset","value":"33"},{"key":"etag","value":"W/\"7e-Gy6ZYenT4Ac+RqTLTJ1Xa1y+Abc\""},{"key":"Server","value":"cloudflare"},{"key":"x-do-app-origin","value":"c322b499-09e8-4d15-94e7-8d66106abf95"},{"key":"Cache-Control","value":"private"},{"key":"x-do-orig-status","value":"401"},{"key":"cf-cache-status","value":"DYNAMIC"},{"key":"CF-RAY","value":"a1aa4d8fb81fc646-SJO"},{"key":"alt-svc","value":"h3=\":443\"; ma=86400"}],"cookie":[],"responseTime":null,"body":"{\n    \"error_code\": \"invalid_authentication\",\n    \"message\": \"Invalid authentication\",\n    \"request_id\": \"b31ef3b8-5445-4046-abc1-3292389d6867\"\n}"}],"_postman_id":"6cd72966-2f76-4504-8f81-45411c127079"},{"name":"2. PAY  PAYMENT SESSION","event":[{"listen":"prerequest","script":{"id":"ab43e383-fe92-40b6-a314-25db2cfc240c","exec":["/*"," * Coralix gateway — HMAC request signing (Postman Pre-request Script)"," * ------------------------------------------------------------------"," * Paste this into the \"Pre-request Script\" tab of your Collection (so it runs"," * for every gateway request) or a single request."," *"," * Define these variables (Collection/Environment — keep the secret as type \"secret\"):"," *   coralix_public_key   e.g. pk_test_bet365"," *   coralix_secret_key   e.g. sk_test_bet365_secret"," *"," * It signs METHOD\\nPATH\\nTIMESTAMP\\nSHA256(body) and injects X-API-Key,"," * X-Timestamp and X-Signature automatically — no manual header rows needed."," */","//const CryptoJS = require('crypto-js');","","const publicKey = pm.variables.get('coralix_public_key');","const secretKey = pm.variables.get('coralix_secret_key');","if (!publicKey || !secretKey) {","  throw new Error('Set the coralix_public_key and coralix_secret_key variables');","}","","// METHOD (uppercase)","const method = pm.request.method.toUpperCase();","","// PATH including /v1, WITHOUT query string; resolve any {{vars}} in the path","const path = pm.variables.replaceIn('/' + pm.request.url.path.join('/'));","","// Exact body Postman will send (resolve {{vars}}); empty string for GET/DELETE","const rawBody = pm.request.body && pm.request.body.raw ? pm.request.body.raw : '';","const body = rawBody ? pm.variables.replaceIn(rawBody) : '';","","const timestamp = Math.floor(Date.now() / 1000).toString();","const bodyHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);","const canonical = [method, path, timestamp, bodyHash].join('\\n');","const signature = CryptoJS.HmacSHA256(canonical, secretKey).toString(CryptoJS.enc.Hex);","","pm.request.headers.upsert({ key: 'X-API-Key', value: publicKey });","pm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp });","pm.request.headers.upsert({ key: 'X-Signature', value: signature });","","// Handy while debugging (open the Postman Console):","console.log('[coralix] canonical:\\n' + canonical);","console.log('[coralix] X-Signature: ' + signature);",""],"type":"text/javascript","packages":{}}}],"id":"6b64dbef-981b-43bf-a968-1e99162eead8","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer pk_test_bet365:sk_test_bet365_secret","type":"text"}],"body":{"mode":"raw","raw":"{\n    \"payment_method_config_id\": \"c0658ab5-0592-49e4-a6c4-b469b88091c3\",\n    \"payment_data\": {\n      \"card_number\": \"4242424242424242\",\n      \"exp_month\": 12,\n      \"exp_year\": 2030,\n      \"cvv\": \"123\",\n      \"holder_name\": \"John Doe\"\n    }\n  }","options":{"raw":{"language":"json"}}},"url":"https://api.coralix.tech/v1/sessions/44b45c13-8b0e-4697-af40-e1ae83d8c1f9/pay","urlObject":{"protocol":"https","path":["v1","sessions","44b45c13-8b0e-4697-af40-e1ae83d8c1f9","pay"],"host":["api","coralix","tech"],"query":[],"variable":[]}},"response":[{"id":"0b98c517-fa5e-4f28-8587-5d865086daf4","name":"2. PAY  PAYMENT SESSION - 200","originalRequest":{"method":"POST","header":[{"key":"Authorization","value":"Bearer pk_test_bet365:sk_test_bet365_secret","type":"text"}],"body":{"mode":"raw","raw":"{\n    \"payment_method_config_id\": \"c0658ab5-0592-49e4-a6c4-b469b88091c3\",\n    \"payment_data\": {\n      \"card_number\": \"4242424242424242\",\n      \"exp_month\": 12,\n      \"exp_year\": 2030,\n      \"cvv\": \"123\",\n      \"holder_name\": \"John Doe\"\n    }\n  }","options":{"raw":{"language":"json"}}},"url":"https://api.coralix.tech/v1/sessions/ec00c38e-e3d7-4943-9ecd-172835c333bf/pay"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Date","value":"Mon, 13 Jul 2026 18:31:14 GMT"},{"key":"Content-Type","value":"application/json; charset=utf-8"},{"key":"Transfer-Encoding","value":"chunked"},{"key":"Connection","value":"keep-alive"},{"key":"x-powered-by","value":"Express"},{"key":"vary","value":"Origin"},{"key":"x-ratelimit-limit","value":"100"},{"key":"x-ratelimit-remaining","value":"99"},{"key":"x-ratelimit-reset","value":"60"},{"key":"x-trace-id","value":"83e63750-dff0-4281-ab73-47cfc029214f"},{"key":"etag","value":"W/\"b1-hsGqSNqcDBBJP3JG75MR04cSduk\""},{"key":"Server","value":"cloudflare"},{"key":"x-do-app-origin","value":"c322b499-09e8-4d15-94e7-8d66106abf95"},{"key":"Cache-Control","value":"private"},{"key":"x-do-orig-status","value":"200"},{"key":"cf-cache-status","value":"DYNAMIC"},{"key":"Content-Encoding","value":"br"},{"key":"CF-RAY","value":"a1aa5d089e17eb6f-SJO"},{"key":"alt-svc","value":"h3=\":443\"; ma=86400"}],"cookie":[],"responseTime":null,"body":"{\n    \"session_id\": \"ec00c38e-e3d7-4943-9ecd-172835c333bf\",\n    \"transaction_id\": \"b6eee137-2b06-4054-be27-3d81a1944249\",\n    \"status\": \"success\",\n    \"provider_ref\": \"MOCKCARD-1783967474304-ec00c38e\"\n}"}],"_postman_id":"6b64dbef-981b-43bf-a968-1e99162eead8"}],"id":"298c6e0f-3b58-490b-a923-225dcc17b0c5","description":"<p>Every endpoint below is under <code>https://api.coralix.tech</code>, must be signed (see <strong>Authentication</strong>), and acts only on your own resources in the environment of your key. A resource of another account or of the other environment answers <code>404</code>, the same as one that does not exist.</p>\n<h2 id=\"sessions\">Sessions</h2>\n<h3 id=\"create-a-session\">Create a session</h3>\n<p><code>POST /v1/sessions</code> → <code>201</code>. Accepts <code>Idempotency-Key</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Required</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>order_id</code></td>\n<td>string (1 to 200)</td>\n<td>Yes</td>\n<td>Your reference. Unique per account and environment.</td>\n</tr>\n<tr>\n<td><code>amount</code></td>\n<td>number</td>\n<td>Yes</td>\n<td>Greater than zero, in major units (<code>15.20</code>)</td>\n</tr>\n<tr>\n<td><code>currency</code></td>\n<td>string</td>\n<td>Yes</td>\n<td>ISO 4217 code</td>\n</tr>\n<tr>\n<td><code>type</code></td>\n<td><code>payment</code> | <code>withdrawal</code></td>\n<td>No</td>\n<td>Default <code>payment</code>. See <strong>Withdrawals</strong>.</td>\n</tr>\n<tr>\n<td><code>customer_id</code></td>\n<td>string</td>\n<td>No</td>\n<td>Your customer reference</td>\n</tr>\n<tr>\n<td><code>success_url</code>, <code>failure_url</code>, <code>cancel_url</code></td>\n<td>URL</td>\n<td>No</td>\n<td>Where the buyer returns after the hosted page or 3-D Secure</td>\n</tr>\n<tr>\n<td><code>metadata</code></td>\n<td>object</td>\n<td>No</td>\n<td>Free key-value data, returned in the session and in webhooks</td>\n</tr>\n</tbody>\n</table>\n</div><p>Response:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"44b45c13-8b0e-4697-af40-e1ae83d8c1f9\",\n  \"merchant_id\": \"25e2cf6c-21bd-4e5e-bb7d-7f445fe80cbf\",\n  \"environment\": \"test\",\n  \"order_id\": \"order_123\",\n  \"type\": \"payment\",\n  \"amount\": \"15.20\",\n  \"currency\": \"USD\",\n  \"status\": \"pending\",\n  \"customer_id\": \"cust_abc123\",\n  \"success_url\": \"https://yoursite.com/success\",\n  \"failure_url\": \"https://yoursite.com/failure\",\n  \"cancel_url\": \"https://yoursite.com/cancel\",\n  \"metadata\": { \"plan\": \"premium\" },\n  \"expires_at\": \"2026-10-06T14:33:11.000Z\",\n  \"created_at\": \"2026-10-06T14:03:11.000Z\",\n  \"checkout_url\": \"https://&lt;checkout-host&gt;/checkout/44b45c13-8b0e-4697-af40-e1ae83d8c1f9\",\n  \"payment_methods\": [\n    { \"id\": \"c0658ab5-0592-49e4-a6c4-b469b88091c3\", \"display_name\": \"Card\", \"payment_method_type\": \"card\" }\n  ]\n}\n</code></pre>\n<p>Errors: <code>400 validation_failed</code>, <code>409 idempotency_conflict</code> (same <code>order_id</code>, different amount or currency; carries <code>session_id</code>), <code>422 no_withdrawal_rails</code>.</p>\n<h3 id=\"get-a-session\">Get a session</h3>\n<p><code>GET /v1/sessions/{sessionId}</code> → <code>200</code>, the same shape as above. Errors: <code>404 session_not_found</code>.</p>\n<h3 id=\"get-a-sessions-status\">Get a session's status</h3>\n<p><code>GET /v1/sessions/{sessionId}/status</code> → <code>200</code></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"status\": \"requires_action\",\n  \"session_id\": \"44b45c13-…\",\n  \"order_id\": \"order_123\",\n  \"action\": { \"type\": \"redirect\", \"url\": \"…\", \"method\": \"POST\", \"params\": { }, \"expires_at\": \"…\" },\n  \"action_expires_at\": \"2026-10-06T14:18:11.000Z\",\n  \"transactions\": [\n    {\n      \"id\": \"b6eee137-…\",\n      \"session_id\": \"44b45c13-…\",\n      \"merchant_id\": \"25e2…\",\n      \"amount\": \"15.20\",\n      \"currency\": \"USD\",\n      \"status\": \"requires_action\",\n      \"provider_id\": \"…\",\n      \"provider_ref\": \"…\",\n      \"settled_at\": null,\n      \"created_at\": \"…\"\n    }\n  ]\n}\n</code></pre>\n<p><code>action</code> is <code>null</code> unless a payment is waiting for the buyer. Use this endpoint to resume a payment whose <code>/pay</code> response you lost.</p>\n<h3 id=\"pay-a-session\">Pay a session</h3>\n<p><code>POST /v1/sessions/{sessionId}/pay</code> → <code>200</code>. Accepts <code>Idempotency-Key</code> and <code>Accept-Language</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Required</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>payment_method_config_id</code></td>\n<td>Yes</td>\n<td>An <code>id</code> from the session's <code>payment_methods</code></td>\n</tr>\n<tr>\n<td><code>payment_data</code></td>\n<td>Yes</td>\n<td>The payment details, by method type (below)</td>\n</tr>\n</tbody>\n</table>\n</div><p><code>payment_data</code> for <code>card</code>:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Required</th>\n<th>Rule</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>card_number</code></td>\n<td>Always</td>\n<td>13 to 19 digits; spaces and hyphens are removed</td>\n</tr>\n<tr>\n<td><code>exp_month</code></td>\n<td>Always</td>\n<td>1 to 12</td>\n</tr>\n<tr>\n<td><code>exp_year</code></td>\n<td>Always</td>\n<td>Four digits (a two-digit year is read as 20xx); not expired</td>\n</tr>\n<tr>\n<td><code>cvv</code></td>\n<td>Always</td>\n<td>3 or 4 digits</td>\n</tr>\n<tr>\n<td><code>holder_name</code></td>\n<td>Always</td>\n<td>1 to 100 characters</td>\n</tr>\n<tr>\n<td><code>first_name</code>, <code>last_name</code>, <code>email</code>, <code>phone</code></td>\n<td>If the method requires <strong>contact</strong> data</td>\n<td><code>phone</code> in E.164 (<code>+34600123456</code>)</td>\n</tr>\n<tr>\n<td><code>address_line1</code>, <code>city</code>, <code>state</code>, <code>postal_code</code>, <code>country</code></td>\n<td>If the method requires <strong>billing</strong> data</td>\n<td><code>country</code> is ISO 3166-1 alpha-2</td>\n</tr>\n<tr>\n<td><code>ip_address</code></td>\n<td>If the method requires <strong>fraud</strong> data</td>\n<td>The buyer's IP address</td>\n</tr>\n<tr>\n<td><code>address_line2</code>, <code>date_of_birth</code></td>\n<td>Never</td>\n<td><code>date_of_birth</code> as <code>YYYY-MM-DD</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>Unknown keys in <code>payment_data</code> are ignored. Which optional groups a method requires depends on its configuration; if one is missing, the answer is <code>422 payer_data_required</code> with <code>missing_fields[]</code>. Sending them always is harmless and avoids that error.</p>\n<p>Responses: see <strong>Payment Flow → Process the payment</strong>.</p>\n<p>Errors: <code>400 session_expired</code>, <code>400 session_invalid_state</code>, <code>422 session_type_mismatch</code>, <code>422 payment_method_unavailable</code>, <code>422 invalid_payment_data</code> (<code>errors[]</code>), <code>422 payer_data_required</code> (<code>missing_fields[]</code>), <code>502 provider_error</code>.</p>\n<h3 id=\"send-a-payout\">Send a payout</h3>\n<p><code>POST /v1/sessions/{sessionId}/withdrawal</code> → <code>200</code>. See <strong>Withdrawals</strong>.</p>\n<h2 id=\"transactions\">Transactions</h2>\n<h3 id=\"list-transactions\">List transactions</h3>\n<p><code>GET /v1/transactions?status=success&amp;page=1&amp;limit=20</code> → <code>200</code>. <code>limit</code> is 1 to 100, default 20. Newest first.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"data\": [\n    {\n      \"id\": \"b6eee137-…\",\n      \"session_id\": \"44b45c13-…\",\n      \"merchant_id\": \"25e2…\",\n      \"amount\": \"15.20\",\n      \"currency\": \"USD\",\n      \"status\": \"success\",\n      \"provider_id\": \"…\",\n      \"provider_ref\": \"…\",\n      \"settled_at\": \"2026-10-06T14:03:12.000Z\",\n      \"created_at\": \"2026-10-06T14:03:11.000Z\"\n    }\n  ],\n  \"page\": 1,\n  \"limit\": 20\n}\n</code></pre>\n<h3 id=\"get-a-transaction\">Get a transaction</h3>\n<p><code>GET /v1/transactions/{transactionId}</code> → <code>200</code>, one element of the list above. Errors: <code>404 resource_not_found</code>.</p>\n<h3 id=\"get-a-transactions-attempts\">Get a transaction's attempts</h3>\n<p><code>GET /v1/transactions/{transactionId}/attempts</code> → <code>200</code>. Accepts <code>Accept-Language</code>.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">[\n  {\n    \"id\": \"…\",\n    \"transaction_id\": \"b6eee137-…\",\n    \"status\": \"failed\",\n    \"error_code\": \"insufficient_funds\",\n    \"latency_ms\": 412,\n    \"created_at\": \"…\",\n    \"decline_reason\": \"insufficient_funds\",\n    \"decline_message\": \"Insufficient funds. Please try with another card.\"\n  }\n]\n</code></pre>\n<h3 id=\"capture-an-authorized-payment\">Capture an authorized payment</h3>\n<p><code>POST /v1/transactions/{transactionId}/capture</code> → <code>200</code>. Accepts <code>Idempotency-Key</code>. Only for accounts that capture manually: it turns an <code>authorized</code> payment into <code>success</code>.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{ \"id\": \"b6eee137-…\", \"session_id\": \"44b45c13-…\", \"status\": \"success\", \"provider_ref\": \"…\", \"settled_at\": \"…\" }\n</code></pre>\n<p>Capturing a payment that is already captured answers <code>200</code> with its current state. Errors: <code>409 capture_not_allowed</code> (with <code>status</code>), <code>409 authorization_expired</code>, <code>502 provider_error</code>.</p>\n<h3 id=\"refund-a-payment\">Refund a payment</h3>\n<p><code>POST /v1/transactions/{transactionId}/refund</code> → <code>202</code>. <code>Idempotency-Key</code> is <strong>required</strong>. See <strong>Refunds</strong>.</p>\n<h2 id=\"your-account\">Your account</h2>\n<h3 id=\"get-your-profile\">Get your profile</h3>\n<p><code>GET /v1/merchants/me</code> → <code>200</code></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"25e2…\",\n  \"name\": \"Your Shop\",\n  \"parent_merchant_id\": null,\n  \"country_code\": [\"US\"],\n  \"timezone\": \"America/New_York\",\n  \"base_currency\": [\"USD\"],\n  \"status\": \"active\",\n  \"created_at\": \"…\",\n  \"environment\": \"test\"\n}\n</code></pre>\n<p><code>environment</code> is the environment of the key you used, which helps when debugging which key is in use.</p>\n<h3 id=\"list-your-payment-methods\">List your payment methods</h3>\n<p><code>GET /v1/merchants/me/payment-methods</code> → <code>200</code>. The methods available in your key's environment, in display order, with the same shape as a session's <code>payment_methods</code>.</p>\n<h3 id=\"api-keys\">API keys</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Request</th>\n<th>Response</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>GET /v1/merchants/me/api-keys</code></td>\n<td><code>200</code>, <code>[{ id, public_key, environment, last_used_at, revoked_at, created_at }]</code></td>\n</tr>\n<tr>\n<td><code>POST /v1/merchants/me/api-keys</code></td>\n<td><code>201</code>, <code>{ id, public_key, environment, created_at, secret_key }</code>. <strong><code>secret_key</code> is shown only this once.</strong></td>\n</tr>\n<tr>\n<td><code>DELETE /v1/merchants/me/api-keys/{id}</code></td>\n<td><code>200</code>, <code>{ id, revoked_at }</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>A key lists, creates and revokes keys <strong>of its own environment only</strong>. To rotate: create a new pair, deploy it, then revoke the old one.</p>\n<h2 id=\"settlements\">Settlements</h2>\n<p>Settlement batches exist only in production. With a test key, the list is empty and a batch answers <code>404</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Request</th>\n<th>Response</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>GET /v1/settlements</code></td>\n<td><code>200</code>, a list of batches, newest first</td>\n</tr>\n<tr>\n<td><code>GET /v1/settlements/{id}</code></td>\n<td><code>200</code>, one batch</td>\n</tr>\n<tr>\n<td><code>GET /v1/settlements/{id}/export</code></td>\n<td><code>200</code>, the same batch (JSON)</td>\n</tr>\n</tbody>\n</table>\n</div><pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"…\",\n  \"merchant_id\": \"25e2…\",\n  \"period_start\": \"2026-09-01\",\n  \"period_end\": \"2026-09-30\",\n  \"gross_amount\": \"10000.00\",\n  \"fees_amount\": \"300.00\",\n  \"refunds_amount\": \"120.00\",\n  \"chargebacks_amount\": \"0.00\",\n  \"net_amount\": \"9580.00\",\n  \"currency\": \"USD\",\n  \"status\": \"paid\",\n  \"paid_at\": \"2026-10-02T00:00:00.000Z\",\n  \"created_at\": \"2026-10-01T00:00:00.000Z\"\n}\n</code></pre>\n<h2 id=\"webhooks\">Webhooks</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Request</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>POST /v1/webhooks/config</code></td>\n<td>Set your endpoint for this environment. The first time, returns <code>signing_secret</code>.</td>\n</tr>\n<tr>\n<td><code>POST /v1/webhooks/config/rotate-secret</code></td>\n<td>New signing secret; the old one keeps signing for 24 hours</td>\n</tr>\n<tr>\n<td><code>GET /v1/webhooks?page=1&amp;limit=20</code></td>\n<td>Your deliveries in this environment</td>\n</tr>\n<tr>\n<td><code>GET /v1/webhooks/{id}</code></td>\n<td>One delivery</td>\n</tr>\n<tr>\n<td><code>POST /v1/webhooks/{id}/retry</code></td>\n<td>Send a delivery again. <code>400 webhook_already_delivered</code> if it already succeeded.</td>\n</tr>\n</tbody>\n</table>\n</div><p>A delivery record:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"id\": \"…\",\n  \"event_type\": \"payment.success\",\n  \"transaction_id\": \"b6eee137-…\",\n  \"endpoint_url\": \"https://yoursite.com/webhooks/coralix\",\n  \"attempts_count\": 2,\n  \"last_response_status\": 503,\n  \"next_retry_at\": \"…\",\n  \"delivered_at\": null,\n  \"created_at\": \"…\",\n  \"payload\": { \"...\": \"the exact body that was sent\" }\n}\n</code></pre>\n<p>See <strong>Webhooks</strong> for events, payloads and signature verification.</p>\n<h2 id=\"rate-limits\">Rate limits</h2>\n<p>Up to <strong>100 requests per minute per IP address</strong>. Beyond that the answer is <code>429 rate_limited</code>, with a <code>Retry-After</code> header saying how many seconds to wait.</p>\n","_postman_id":"298c6e0f-3b58-490b-a923-225dcc17b0c5"},{"name":"ERROR CODES","item":[],"id":"125e4f76-9c67-444d-ad34-9334d0d36cfd","description":"<p>Every error response has the same shape, and every one carries a specific <code>error_code</code>. Branch your logic on <code>error_code</code>, never on <code>message</code>: the code is stable, the wording is not.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"error_code\": \"refund_not_allowed\",\n  \"message\": \"A chargeback is recorded against this payment; refunding it would cost the merchant the amount twice\",\n  \"request_id\": \"6f1c2e0a-3b5d-4f8e-9a71-2c4d5e6f7a8b\",\n  \"reason\": \"chargebacked\"\n}\n</code></pre>\n<ul>\n<li><code>message</code> is always a string, meant for your logs, not for your buyer.</li>\n<li><code>request_id</code> identifies the request in our logs. Quote it when contacting support. If you send an <code>X-Request-Id</code> header, we use your value.</li>\n<li>Some codes carry extra fields, listed below.</li>\n</ul>\n<blockquote>\n<p><strong>Declines are not errors.</strong> <code>POST /v1/sessions/{sessionId}/pay</code> answers a declined payment with <code>200</code> and <code>status: \"failed\"</code>. See <strong>Decline Reasons Reference</strong>.</p>\n</blockquote>\n<h3 id=\"authentication\">Authentication</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>invalid_request</code></td>\n<td><code>X-Timestamp</code> is not Unix seconds, or <code>X-Signature</code> is not 64 lowercase hex characters</td>\n</tr>\n<tr>\n<td><code>401</code></td>\n<td><code>invalid_authentication</code></td>\n<td>A header is missing, the API key is unknown or revoked, or the signature does not match. These cases are deliberately not told apart.</td>\n</tr>\n<tr>\n<td><code>401</code></td>\n<td><code>timestamp_out_of_window</code></td>\n<td><code>X-Timestamp</code> is more than ±300 seconds from server time</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"request-shape-and-limits\">Request shape and limits</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n<th>Extra fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>validation_failed</code></td>\n<td>The body is missing a field, has a wrong type, or contains a field the endpoint does not accept</td>\n<td><code>errors[]</code>: one line per failed constraint</td>\n</tr>\n<tr>\n<td><code>404</code></td>\n<td><code>resource_not_found</code></td>\n<td>The transaction, settlement batch, webhook delivery or API key does not exist, is not yours, or belongs to the other environment</td>\n<td></td>\n</tr>\n<tr>\n<td><code>429</code></td>\n<td><code>rate_limited</code></td>\n<td>Too many requests. The <code>Retry-After</code> header says how many seconds to wait.</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"sessions-and-payments\">Sessions and payments</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n<th>Extra fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>404</code></td>\n<td><code>session_not_found</code></td>\n<td>The session does not exist, is not yours, or belongs to the other environment</td>\n<td></td>\n</tr>\n<tr>\n<td><code>400</code></td>\n<td><code>session_expired</code></td>\n<td>The session passed its 30-minute expiry</td>\n<td></td>\n</tr>\n<tr>\n<td><code>400</code></td>\n<td><code>session_invalid_state</code></td>\n<td>The session is no longer <code>pending</code>, for example because it was already paid</td>\n<td></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>idempotency_conflict</code></td>\n<td>The <code>order_id</code> was already used with a different <code>amount</code> or <code>currency</code></td>\n<td><code>session_id</code>: the session that <code>order_id</code> belongs to</td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>idempotency_conflict</code></td>\n<td>The <code>Idempotency-Key</code> was already used for a different request (another body or endpoint)</td>\n<td></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>idempotency_in_progress</code></td>\n<td>A request with the same <code>Idempotency-Key</code> is still running. Retry shortly with the same key.</td>\n<td></td>\n</tr>\n<tr>\n<td><code>503</code></td>\n<td><code>idempotency_store_unavailable</code></td>\n<td>The <code>Idempotency-Key</code> cannot be honoured right now. The request was <strong>not</strong> executed; retry shortly.</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>session_type_mismatch</code></td>\n<td><code>/pay</code> was called on a withdrawal session, or <code>/withdrawal</code> on a payment session</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>payment_method_unavailable</code></td>\n<td>The <code>payment_method_config_id</code> is not available to you in this environment</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>invalid_payment_data</code></td>\n<td><code>payment_data</code> does not match the format for this payment method</td>\n<td><code>errors[]</code></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>payer_data_required</code></td>\n<td>Payer fields required by this payment method are missing</td>\n<td><code>missing_fields[]</code></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>capture_not_allowed</code></td>\n<td>The transaction is not in <code>authorized</code> and cannot be captured</td>\n<td><code>status</code></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>authorization_expired</code></td>\n<td>The authorization expired before capture</td>\n<td></td>\n</tr>\n<tr>\n<td><code>502</code></td>\n<td><code>provider_error</code></td>\n<td>The payment provider failed</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"withdrawals\">Withdrawals</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n<th>Extra fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>422</code></td>\n<td><code>no_withdrawal_rails</code></td>\n<td>Your account has no withdrawal methods in any environment</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>withdrawal_method_unavailable</code></td>\n<td>The <code>withdrawal_method_config_id</code> is not available to you</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>withdrawal_data_invalid</code></td>\n<td><code>withdrawal_data</code> does not match the selected method</td>\n<td><code>errors[]</code></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"refunds\">Refunds</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n<th>Extra fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>idempotency_key_required</code></td>\n<td>The <code>Idempotency-Key</code> header is missing. It is required on refunds.</td>\n<td></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>idempotency_conflict</code></td>\n<td>The <code>Idempotency-Key</code> was already used for another amount or another transaction, or for a refund that failed. A new attempt needs a new key.</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_amount_invalid</code></td>\n<td><code>amount</code> is not positive</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_amount_exceeded</code></td>\n<td><code>amount</code> is more than what is still available to refund, or nothing is left</td>\n<td></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>refund_amount_exceeded</code></td>\n<td>Another refund took the available amount first</td>\n<td></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>refund_not_allowed</code></td>\n<td>The transaction cannot be refunded at all</td>\n<td><code>reason</code>, one of the values below</td>\n</tr>\n</tbody>\n</table>\n</div><div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>reason</code></th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>not_card</code></td>\n<td>Only card payments can be refunded</td>\n</tr>\n<tr>\n<td><code>not_captured</code></td>\n<td>The payment was authorized and never captured: there is nothing to give back. Release the authorization instead.</td>\n</tr>\n<tr>\n<td><code>invalid_status</code></td>\n<td>The payment did not succeed, so there is no money to return</td>\n</tr>\n<tr>\n<td><code>acquirer_reversed</code></td>\n<td>The acquirer already reversed this payment</td>\n</tr>\n<tr>\n<td><code>chargebacked</code></td>\n<td>A chargeback is recorded against this payment</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"webhooks\">Webhooks</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>400</code></td>\n<td><code>webhook_already_delivered</code></td>\n<td>You asked to retry a delivery that already succeeded</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"server-errors\">Server errors</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n<th>When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>500</code></td>\n<td><code>internal_error</code></td>\n<td>An unexpected error on our side. Retry with the same <code>Idempotency-Key</code>, and quote the <code>request_id</code> if it persists.</td>\n</tr>\n<tr>\n<td><code>500</code></td>\n<td><code>adapter_not_found</code></td>\n<td>A configuration error on our side. Contact support with the <code>request_id</code>.</td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"other-codes\">Other codes</h3>\n<p>These come from rejections that have no more specific code:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>HTTP</th>\n<th><code>error_code</code></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>403</code></td>\n<td><code>forbidden</code></td>\n</tr>\n<tr>\n<td><code>409</code></td>\n<td><code>conflict</code></td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td><code>unprocessable_request</code></td>\n</tr>\n</tbody>\n</table>\n</div>","_postman_id":"125e4f76-9c67-444d-ad34-9334d0d36cfd"},{"name":"DECLINE REASONS REFERENCE","item":[],"id":"ab621510-1780-476f-95ad-66f27b31afbe","description":"<p>When a payment is declined, <code>POST /v1/sessions/{sessionId}/pay</code> answers <strong><code>200</code></strong> with <code>status: \"failed\"</code>, a machine-readable <strong><code>decline_reason</code></strong> and a buyer-facing <strong><code>decline_message</code></strong>. The same two fields appear on <code>GET /v1/transactions/{id}/attempts</code> and in the <code>payment.failed</code> webhook.</p>\n<ul>\n<li><strong>Branch your logic on <code>decline_reason</code></strong>, which is stable. Show <code>decline_message</code> to the buyer, but never parse it: its wording can change.</li>\n<li><strong>Language</strong>: <code>decline_message</code> is in English by default. Send <code>Accept-Language: es</code> (any <code>es-*</code> value works) to receive it in Spanish on <code>/pay</code> and <code>/attempts</code>. <strong>Webhooks always carry the English message.</strong></li>\n<li><strong>Any provider code we do not recognize maps to <code>unknown_decline</code></strong>, so your integration must handle that value.</li>\n</ul>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>decline_reason</code></th>\n<th>Category</th>\n<th><code>decline_message</code> (en)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>insufficient_funds</code></td>\n<td>card</td>\n<td>Insufficient funds. Please try with another card.</td>\n</tr>\n<tr>\n<td><code>invalid_card</code></td>\n<td>card</td>\n<td>Invalid card. Please check your card details and try again.</td>\n</tr>\n<tr>\n<td><code>expired_card</code></td>\n<td>card</td>\n<td>Your card has expired. Please use a valid card.</td>\n</tr>\n<tr>\n<td><code>incorrect_cvv</code></td>\n<td>card</td>\n<td>Incorrect CVV. Please check your security code.</td>\n</tr>\n<tr>\n<td><code>do_not_honor</code></td>\n<td>issuer</td>\n<td>Your bank declined the payment. Please contact your bank.</td>\n</tr>\n<tr>\n<td><code>card_blocked</code></td>\n<td>issuer</td>\n<td>Card blocked. Please contact your bank.</td>\n</tr>\n<tr>\n<td><code>lost_card</code></td>\n<td>issuer</td>\n<td>Card reported as lost.</td>\n</tr>\n<tr>\n<td><code>stolen_card</code></td>\n<td>issuer</td>\n<td>Card reported as stolen.</td>\n</tr>\n<tr>\n<td><code>limit_exceeded</code></td>\n<td>issuer</td>\n<td>Credit limit exceeded.</td>\n</tr>\n<tr>\n<td><code>card_velocity_exceeded</code></td>\n<td>issuer</td>\n<td>Too many recent transactions. Please wait and try again.</td>\n</tr>\n<tr>\n<td><code>invalid_amount</code></td>\n<td>gateway</td>\n<td>Invalid amount.</td>\n</tr>\n<tr>\n<td><code>currency_not_supported</code></td>\n<td>gateway</td>\n<td>Currency not supported by this card.</td>\n</tr>\n<tr>\n<td><code>duplicate_transaction</code></td>\n<td>gateway</td>\n<td>Duplicate transaction.</td>\n</tr>\n<tr>\n<td><code>fraud_suspected</code></td>\n<td>risk</td>\n<td>Transaction declined for security reasons.</td>\n</tr>\n<tr>\n<td><code>3ds_failed</code></td>\n<td>3ds</td>\n<td>Authentication failed. Please try again.</td>\n</tr>\n<tr>\n<td><code>3ds_not_supported</code></td>\n<td>3ds</td>\n<td>Your bank does not support the required authentication.</td>\n</tr>\n<tr>\n<td><code>bank_unreachable</code></td>\n<td>network</td>\n<td>Your bank is currently unavailable. Please try again later.</td>\n</tr>\n<tr>\n<td><code>processing_error</code></td>\n<td>network</td>\n<td>Processing error. Please try again.</td>\n</tr>\n<tr>\n<td><code>country_not_supported</code></td>\n<td>compliance</td>\n<td>Country not supported for this payment method.</td>\n</tr>\n<tr>\n<td><code>card_type_not_supported</code></td>\n<td>compliance</td>\n<td>Card type not accepted.</td>\n</tr>\n<tr>\n<td><code>amount_too_small</code></td>\n<td>limits</td>\n<td>Amount is below the minimum allowed.</td>\n</tr>\n<tr>\n<td><code>unknown_decline</code></td>\n<td>fallback</td>\n<td>Payment not processed. Please contact your bank.</td>\n</tr>\n</tbody>\n</table>\n</div><p><strong>Notes</strong></p>\n<ul>\n<li>The <strong>Category</strong> column groups the reasons for your own reporting. <strong>It is not returned by the API.</strong></li>\n<li>Reasons in the <code>network</code> category are usually temporary: it is reasonable to let the buyer try again. Reasons in <code>card</code>, <code>issuer</code> and <code>risk</code> will not change with a retry of the same card.</li>\n<li>A failed <strong>refund</strong> reports its own reason in the <code>payment.refund_failed</code> webhook. That value comes from the provider and may not be one of the codes above.</li>\n</ul>\n","_postman_id":"ab621510-1780-476f-95ad-66f27b31afbe"},{"name":"GOING LIVE","item":[],"id":"b14ca743-0bce-493a-b0b9-86e22b006734","description":"<p>Before you switch from <code>pk_test_…</code> to <code>pk_production_…</code>, go through this list. Each item is something that works in testing and fails, or costs money, in production.</p>\n<h3 id=\"keys-and-security\">Keys and security</h3>\n<ul>\n<li> Your <strong>production secret key</strong> is stored server-side only, in a secret manager or environment variable. It is never in source control, a mobile app or a browser.</li>\n<li> You call the API <strong>only from your backend</strong>.</li>\n<li> Your servers' clocks are synchronized (NTP). A drift beyond 300 seconds rejects every request.</li>\n<li> You know how to rotate keys: create a new pair, deploy it, revoke the old one.</li>\n</ul>\n<h3 id=\"payments\">Payments</h3>\n<ul>\n<li> Every <code>order_id</code> is unique per purchase attempt. After a decline, a new attempt uses a new <code>order_id</code>.</li>\n<li> Every <code>POST</code> sends an <code>Idempotency-Key</code>, and a retry after a timeout <strong>reuses the same key</strong>.</li>\n<li> You branch on <code>status</code> and <code>decline_reason</code>, never on message text, and you handle <code>unknown_decline</code>.</li>\n<li> You handle <code>requires_action</code>: 3-D Secure is common in production even if your tests rarely hit it.</li>\n<li> If your account captures manually, you capture <code>authorized</code> payments before the authorization expires.</li>\n<li> If you collect card data yourself, you are PCI DSS compliant. Otherwise you use <code>checkout_url</code>.</li>\n</ul>\n<h3 id=\"webhooks\">Webhooks</h3>\n<ul>\n<li> You configured a webhook endpoint <strong>with your production key</strong>. The test endpoint is separate and is not copied over.</li>\n<li> You stored the production <code>signing_secret</code>, and you <strong>verify the signature</strong> of every delivery.</li>\n<li> You check <code>environment</code> in the payload before touching any balance.</li>\n<li> You fulfil orders from the <strong>webhook</strong>, not from the buyer's redirect to <code>success_url</code>.</li>\n<li> You deduplicate on <code>X-Coralix-Delivery</code>: a delivery can arrive more than once.</li>\n<li> Your endpoint answers <code>2xx</code> within 5 seconds and does its work asynchronously.</li>\n</ul>\n<h3 id=\"operations\">Operations</h3>\n<ul>\n<li> You log the <code>request_id</code> of every error, to quote it to support.</li>\n<li> You handle <code>429 rate_limited</code> by waiting <code>Retry-After</code> seconds.</li>\n<li> You retry <code>5xx</code> errors and <code>503 idempotency_store_unavailable</code> with backoff and the same <code>Idempotency-Key</code>.</li>\n<li> You tested a refund and, if you use them, a withdrawal, including the <code>processing</code> case.</li>\n</ul>\n","_postman_id":"b14ca743-0bce-493a-b0b9-86e22b006734"}],"event":[{"listen":"prerequest","script":{"id":"7be3f853-e74a-4edc-9e3d-b1f091241cb9","type":"text/javascript","packages":{},"exec":[""]}},{"listen":"test","script":{"id":"e9e53639-69b5-4e9d-b713-7e34e863026d","type":"text/javascript","packages":{},"exec":[""]}}],"variable":[{"key":"coralix_public_key","value":"pk_test_fecc9278094f9fa2b24197fed9d6647f4","type":"string"},{"key":"coralix_secret_key","value":"sk_test_50cba8238dae1f7211bfbd6d8e165dc85ef96b52b7d17f9f4","type":"string"}]}