Quickstart
Three requests, create, confirm, read back, prove your key, your headers and your connection; the scripts below are the ones the product's own release suite executes against the live API, so they cannot be simultaneously green there and wrong here.
Written for a developer with a test API key and ten minutes.
You need
- A workspace in the Test environment, with at least one
ACTIVEprovider connection. Connect a provider. - A test API key with
payments:writeandpayments:read, minted on Settings → API keys. It is shown once. Settings. - Your workspace id, shown on the overview and on the Quickstart screen, which renders these same scripts already filled in for your workspace.
Replace pehmen_test_YOUR_KEY_HERE and YOUR_WORKSPACE_ID below. Nothing on this page has ever held a real key.
curl
Bash, curl and grep. No jq, no uuidgen. set -euo pipefail is not decoration: without it a failed request leaves an empty variable and the next request fails with a confusing 404 instead of the real error.
#!/usr/bin/env bash
set -euo pipefail
API="https://pehmen-api.huper.technology"
ORG="YOUR_WORKSPACE_ID"
KEY="pehmen_test_YOUR_KEY_HERE"
RUN="$$-$(date +%s)"
# 1. Create an order and a payment intent. Every /v1 mutation needs an
# Idempotency-Key: the same key replays the same response instead of
# creating a second payment.
INTENT=$(curl -sS -X POST "$API/v1/payment-intents" \
-H "x-api-key: $KEY" \
-H "x-pehmen-organization: $ORG" \
-H "x-pehmen-environment: test" \
-H "idempotency-key: quickstart-create-$RUN" \
-H "content-type: application/json" \
-d '{
"merchant_payment_ref": "quickstart-'"$RUN"'",
"order": {
"merchant_order_ref": "quickstart-order-'"$RUN"'",
"amount_minor": 12800,
"currency": "SGD",
"category": "event_ticket",
"channel": "web_checkout",
"customer": { "merchant_customer_ref": "quickstart-customer" },
"seller_ref": "quickstart-seller"
},
"payment_method": { "family": "CARD" }
}')
# Pull the id out with grep, so this runs on a bare machine with no
# JSON tooling installed.
INTENT_ID=$(printf '%s' "$INTENT" | grep -o '"id":"pi_[0-9a-f]*"' | head -n 1 | cut -d'"' -f4)
echo "payment intent: $INTENT_ID"
# 2. Confirm. This is where the routing engine picks a connection, records the
# candidates it considered, and reserves the attempt.
CONFIRMED=$(curl -sS -X POST "$API/v1/payment-intents/$INTENT_ID/confirm" \
-H "x-api-key: $KEY" \
-H "x-pehmen-organization: $ORG" \
-H "x-pehmen-environment: test" \
-H "idempotency-key: quickstart-confirm-$RUN" \
-H "content-type: application/json" \
-d '{}')
echo "status: $(printf '%s' "$CONFIRMED" | grep -o '"status":"[A-Z_]*"' | head -n 1 | cut -d'"' -f4)"
# 3. Read the payment back, with the decision that routed it.
curl -sS "$API/v1/payment-intents/$INTENT_ID" \
-H "x-api-key: $KEY" \
-H "x-pehmen-organization: $ORG" \
-H "x-pehmen-environment: test"
echo
Save it as pehmen-quickstart.sh and run bash pehmen-quickstart.sh.
Node
Node 18 or newer; fetch is built in. No npm install. This version also reads each route decision back and prints the reason codes and how many candidates the router considered.
#!/usr/bin/env node
// Pehmen quickstart — create, route and read one test payment.
// No dependencies: Node 18+ has fetch built in.
const API = "https://pehmen-api.huper.technology";
const ORG = "YOUR_WORKSPACE_ID";
const KEY = "pehmen_test_YOUR_KEY_HERE";
const RUN = `${Date.now()}`;
const headers = (idempotencyKey) => ({
'x-api-key': KEY,
'x-pehmen-organization': ORG,
'x-pehmen-environment': 'test',
'content-type': 'application/json',
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
});
/** Every non-2xx carries a typed error envelope; print it rather than a stack. */
async function call(method, path, { body, idempotencyKey } = {}) {
const response = await fetch(`${API}${path}`, {
method,
headers: headers(idempotencyKey),
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(`${method} ${path} -> ${response.status} ${JSON.stringify(payload)}`);
}
return payload;
}
// 1. Create an order and a payment intent.
const intent = await call('POST', '/v1/payment-intents', {
idempotencyKey: `quickstart-create-${RUN}`,
body: {
merchant_payment_ref: `quickstart-${RUN}`,
order: {
merchant_order_ref: `quickstart-order-${RUN}`,
amount_minor: 12800,
currency: 'SGD',
category: 'event_ticket',
channel: 'web_checkout',
customer: { merchant_customer_ref: 'quickstart-customer' },
seller_ref: 'quickstart-seller',
},
payment_method: { family: 'CARD' },
},
});
console.log(`payment intent: ${intent.id} ${intent.status}`);
// 2. Confirm — the routing engine runs here.
const confirmed = await call('POST', `/v1/payment-intents/${intent.id}/confirm`, {
idempotencyKey: `quickstart-confirm-${RUN}`,
body: {},
});
console.log(`status: ${confirmed.status}`);
// 3. Read it back, then read the decision that routed it.
const read = await call('GET', `/v1/payment-intents/${intent.id}`);
console.log(`attempts: ${read.attempts?.length ?? 0}`);
for (const decisionId of read.route_decision_ids ?? []) {
const trace = await call('GET', `/v1/route-decisions/${decisionId}`);
console.log(`routed by: ${(trace.decision_reason_codes ?? []).join(', ')}`);
console.log(`candidates considered: ${(trace.candidate_snapshot ?? []).length}`);
}
Save it as pehmen-quickstart.mjs and run node pehmen-quickstart.mjs.
What you should see
payment intent: pi_9e2f70… CREATED
status: PROCESSING
attempts: 1
routed by: ROUTE_ONLY_CANDIDATE
candidates considered: 1
A pi_… id in CREATED; after confirm, PROCESSING and exactly one attempt; a route decision with the winning reason code and one candidate row per connection. Open the payment in the dashboard and the trace on screen is the same record you just read.
When it does not
| You see | Meaning | Do |
|---|---|---|
401 unauthorized | The key is wrong, revoked or expired | Mint a new one; keys are shown once. |
403 environment_mismatch | The header names an environment the key is not bound to | Test keys say test. |
404 not_found on step 2 | The intent id was not captured, or belongs to another workspace | Check step 1's output; the workspace id must be yours. |
422 no_eligible_route | No connection survived the routing filters | details carries the route decision id; read it. Usually no ACTIVE connection, or PayNow with manual capture. |
409 idempotency_conflict | You re-ran with the same RUN value and a different body | Let RUN change, or delete the change. |
Next
Integration guide turns these three calls into a checkout with outcomes by webhook. Verify a webhook is the handler.