API Documentation

A simple, predictable REST API for accepting payments and issuing refunds. JSON in, JSON out, over HTTPS.

The API is organized around REST. Requests use standard HTTP verbs, return JSON, and use conventional status codes. Every object has a unique, unguessable id with a type prefix.

Base URL

https://onhere.net/api/v1

Every request runs in either test or live mode, decided by the API key you use. Test mode never touches real money and is available immediately; live mode unlocks once your account is approved.

Authentication

Authenticate with your secret API key in the Authorization header.

Find your keys in your dashboard under Developers → API keys. Each account has a publishable key (pk_) and a secret key (sk_), in a test and a live variant.

Send the secret key as a Bearer token on every request. Keep it private — treat it like a password and never expose it in client-side code.

The key prefix decides the mode: sk_test_ hits test mode, sk_live_ hits live mode. Live keys work only after your account is approved.

curl https://onhere.net/api/v1/account \
  -H "Authorization: Bearer sk_test_..."
<?php
$ch = curl_init('https://onhere.net/api/v1/account');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_...'],
]);
$response = curl_exec($ch);
const res = await fetch('https://onhere.net/api/v1/account', {
  headers: { Authorization: 'Bearer sk_test_...' }
});
const account = await res.json();
Never share your secret key or commit it to source control. If it leaks, roll it from the dashboard.

Errors

onHere uses conventional HTTP status codes and a consistent error body.

Codes in the 2xx range mean success, 4xx means the request failed given the information provided, and 5xx means something went wrong on our side. Failed requests return an error object:

{
  "error": {
    "type": "invalid_request_error",
    "message": "The amount must be a positive integer in the smallest currency unit."
  }
}
Status Meaning
400Bad request — a parameter was missing or invalid.
401Unauthorized — no or invalid API key.
403Forbidden — live mode used by an unapproved account.
404Not found — the resource does not exist.
429Too many requests — you hit the rate limit.

Rate limits

Requests are limited per API key, per minute.

Every response includes RateLimit-Limit and RateLimit-Remaining headers. When you exceed the limit you receive a 429 response — back off and retry after a short delay.

RateLimit-Limit: 60
RateLimit-Remaining: 59

Object IDs

Every object has a unique, random id with a type prefix.

IDs are never sequential and never guessable. Use the full string as the identifier; do not parse it.

usr_account
txn_payment
re_refund
key_API key
whe_webhook endpoint
evt_event

The account

Information about the authenticated account.

Retrieve the account

GET /v1/account

Returns the account associated with the API key.

curl https://onhere.net/api/v1/account \
  -H "Authorization: Bearer sk_test_..."
<?php
$ch = curl_init('https://onhere.net/api/v1/account');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_...'],
]);
$account = json_decode(curl_exec($ch), true);
const res = await fetch('https://onhere.net/api/v1/account', {
  headers: { Authorization: 'Bearer sk_test_...' }
});
const account = await res.json();
{
  "id": "usr_x7Kp9fQ2mL4tR8wZ1cVb",
  "object": "account",
  "type": "individual",
  "email": "jane@example.com",
  "approved": true,
  "livemode": false,
  "created": 1783723601
}

Payments

A payment represents money moving through your account.

Create a payment

POST /v1/payments

Creates a payment for the given amount and currency.

Parameters

Parameter Type Description
amount integer Amount in the smallest currency unit (e.g. cents). Required.
currency string Three-letter ISO currency code. Defaults to EUR.
description string An arbitrary description shown in the dashboard.
reference string Your own reference (e.g. an order id) echoed back on the payment and in webhooks. Optional.
card_number string Card number for the internal card scheme. Optional.
card_expiry string Card expiry as MM/YYYY. Optional.
card_cvc string Card verification code. Optional.

Payments start as pending and are confirmed by review; transaction.succeeded (or transaction.failed) fires when the outcome is decided.

Response includes reference and payment_link (the pl_ id the payment came from, if any).

curl https://onhere.net/api/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"amount": 2500, "currency": "eur", "description": "Order #1001"}'
<?php
$ch = curl_init('https://onhere.net/api/v1/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer sk_test_...',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => 2500,
    'currency' => 'eur',
    'description' => 'Order #1001',
  ]),
]);
$payment = json_decode(curl_exec($ch), true);
const res = await fetch('https://onhere.net/api/v1/payments', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ amount: 2500, currency: 'eur', description: 'Order #1001' }),
});
const payment = await res.json();
{
  "id": "txn_w2UlYqe9ZMoIhvofDhyj",
  "object": "payment",
  "amount": 2500,
  "currency": "EUR",
  "description": "Order #1001",
  "reference": "wc_order_1057",
  "payment_link": "pl_jLdWvwZU5Wj8P1HvgE4F",
  "status": "pending",
  "livemode": false,
  "amount_refunded": 0,
  "refunded": false,
  "created": 1783724733
}

List payments

GET /v1/payments

Returns your payments, most recent first.

Retrieve a payment

GET /v1/payments/{id}

Retrieves a payment by its id.

curl https://onhere.net/api/v1/payments/txn_w2UlYqe9ZMoIhvofDhyj \
  -H "Authorization: Bearer sk_test_..."

Subscriptions

Recurring billing. Create a reusable plan, then subscribe a customer to it with a card. The subscription charges immediately and then on the plan schedule; each charge is a payment you approve like any other.

Create a plan

POST /v1/subscription_plans

A plan holds the amount, interval, optional total duration and minimum term. Subscriptions are created against a plan.

Parameter Type Description
namestringPlan name (required).
amountintegerAmount in the smallest currency unit (e.g. 1999 = 19.99).
currencystringThree-letter ISO currency code. Defaults to EUR.
intervalstringBilling interval: day, week, month or year.
interval_countintegerNumber of intervals between charges. Defaults to 1.
total_cyclesintegerTotal number of charges before the subscription completes. Omit or 0 to run until canceled.
min_cyclesintegerCharges a customer must keep the subscription before they may cancel it themselves. Defaults to 0.
descriptionstringOptional description.
curl https://onhere.net/api/v1/subscription_plans \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro monthly",
    "amount": 1999,
    "currency": "eur",
    "interval": "month",
    "total_cycles": 12,
    "min_cycles": 3
  }'
<?php
$ch = curl_init('https://onhere.net/api/v1/subscription_plans');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer sk_test_...',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'name' => 'Pro monthly',
    'amount' => 1999,
    'currency' => 'eur',
    'interval' => 'month',
    'total_cycles' => 12,
    'min_cycles' => 3,
  ]),
]);
$plan = json_decode(curl_exec($ch), true);
const res = await fetch('https://onhere.net/api/v1/subscription_plans', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Pro monthly',
    amount: 1999,
    currency: 'eur',
    interval: 'month',
    total_cycles: 12,
    min_cycles: 3,
  }),
});
const plan = await res.json();
{
  "id": "plan_9Kd2mZ0pQ7rLtV3xW1cB",
  "object": "subscription_plan",
  "name": "Pro monthly",
  "description": null,
  "amount": 1999,
  "currency": "EUR",
  "interval": "month",
  "interval_count": 1,
  "total_cycles": 12,
  "min_cycles": 3,
  "status": "active",
  "created": 1783724733
}

List plans

GET/v1/subscription_plans

Returns your plans, newest first.

Retrieve a plan

GET/v1/subscription_plans/{id}

Archive a plan

DELETE/v1/subscription_plans/{id}

Archiving stops new subscriptions on the plan; existing subscriptions keep running.

Create a subscription

POST /v1/subscriptions

Subscribe a customer to a plan with a card. The first charge is taken immediately, then on the plan schedule. The customer is e-mailed on signup, on every charge and on cancellation.

Parameter Type Description
planstringThe plan public id to subscribe to (required).
customer_emailstringCustomer e-mail; subscription notifications are sent here (required).
customer_namestringOptional customer name.
couponstringOptional coupon code (a percentage discount) to apply to the subscription.
card_numberstringCard number (required).
card_expirystringCard expiry as MM/YY or MM/YYYY (required).
card_cvcstringCard security code (required).
curl https://onhere.net/api/v1/subscriptions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_9Kd2mZ0pQ7rLtV3xW1cB",
    "customer_email": "buyer@example.com",
    "card_number": "4242424242424242",
    "card_expiry": "12/2030",
    "card_cvc": "123"
  }'
<?php
$ch = curl_init('https://onhere.net/api/v1/subscriptions');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer sk_test_...',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'plan' => 'plan_9Kd2mZ0pQ7rLtV3xW1cB',
    'customer_email' => 'buyer@example.com',
    'card_number' => '4242424242424242',
    'card_expiry' => '12/2030',
    'card_cvc' => '123',
  ]),
]);
$sub = json_decode(curl_exec($ch), true);
const res = await fetch('https://onhere.net/api/v1/subscriptions', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    plan: 'plan_9Kd2mZ0pQ7rLtV3xW1cB',
    customer_email: 'buyer@example.com',
    card_number: '4242424242424242',
    card_expiry: '12/2030',
    card_cvc: '123',
  }),
});
const sub = await res.json();

The subscription is returned with its status, charge count and next charge date.

{
  "id": "sub_2pQ7rLtV3xW1cB9Kd2mZ",
  "object": "subscription",
  "plan": "plan_9Kd2mZ0pQ7rLtV3xW1cB",
  "amount": 1999,
  "currency": "EUR",
  "description": "Pro monthly",
  "customer_email": "buyer@example.com",
  "interval": "month",
  "interval_count": 1,
  "total_cycles": 12,
  "min_cycles": 3,
  "status": "active",
  "livemode": false,
  "charge_count": 1,
  "next_charge": 1786316733,
  "created": 1783724733
}

List subscriptions

GET/v1/subscriptions

Returns your subscriptions for the current mode, newest first.

Retrieve a subscription

GET/v1/subscriptions/{id}

Pause, resume or cancel

POST/v1/subscriptions/{id}/pause
POST/v1/subscriptions/{id}/resume
POST/v1/subscriptions/{id}/cancel

Pause suspends charging; resume continues it; cancel stops it for good. Canceling from the API is immediate and ignores the plan minimum term (which only restricts the customer self-cancel link).

Refunds

Refund a payment, fully or partially.

Create a refund

POST /v1/refunds

Refunds a payment. Without an amount the remaining balance is refunded.

Parameters

Parameter Type Description
payment string The id of the payment to refund. Required.
amount integer Amount to refund in the smallest currency unit. Defaults to the full remaining amount.
reason string An optional reason stored with the refund.
curl https://onhere.net/api/v1/refunds \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"payment": "txn_w2UlYqe9ZMoIhvofDhyj", "amount": 1000}'
<?php
$ch = curl_init('https://onhere.net/api/v1/refunds');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer sk_test_...',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'payment' => 'txn_w2UlYqe9ZMoIhvofDhyj',
    'amount' => 1000,
  ]),
]);
$refund = json_decode(curl_exec($ch), true);
const res = await fetch('https://onhere.net/api/v1/refunds', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ payment: 'txn_w2UlYqe9ZMoIhvofDhyj', amount: 1000 }),
});
const refund = await res.json();
{
  "id": "re_wfWPRqfVIuGAAtiY4Nup",
  "object": "refund",
  "payment": "txn_w2UlYqe9ZMoIhvofDhyj",
  "amount": 1000,
  "reason": "",
  "status": "succeeded",
  "created": 1783725100
}

List refunds

GET /v1/refunds

Returns your refunds, most recent first.

Refund rules

Only succeeded incoming payments can be refunded. Bonus credits (source "bonus") and payouts are ledger entries, not customer payments — refunding one returns a refund_not_allowed error. The total refunded can never exceed the original payment.

A refund is never blocked by the merchant's balance. If there are no funds, the refund still succeeds and the balance goes negative; the deficit is covered automatically by future payments, and nothing can be paid out until the balance is positive again.

Every refund fires refund.created and refund.succeeded to your webhook endpoints, and notifies the merchant in the dashboard.

Webhooks

Receive events when things happen in your account.

Add an endpoint in your dashboard under Developers → Webhooks, or register one via the API (below), and choose which events to receive. We POST a JSON event to your URL for each subscribed event.

Event types

transaction.createdA payment was created.
transaction.succeededA payment succeeded.
transaction.failedA payment failed.
refund.createdA refund was created.
refund.succeededA refund succeeded.
refund.failedA refund failed.
subscription.createdA subscription was created.
subscription.activatedA subscription first charge was approved and it became active.
subscription.chargedA recurring charge was created for a subscription.
subscription.payment_failedA subscription charge was declined; the subscription entered dunning (retries).
subscription.pausedA subscription was paused.
subscription.resumedA paused subscription was resumed.
subscription.canceledA subscription was canceled.
subscription.completedA subscription reached its total payments and completed.

Event payload

{
  "id": "evt_LiDAyYvbbNlr2vluuIpY",
  "type": "transaction.succeeded",
  "created": 1783724733,
  "data": {
    "id": "txn_w2UlYqe9ZMoIhvofDhyj",
    "object": "payment",
    "amount": 2500,
    "currency": "EUR",
    "reference": "wc_order_1057",
    "payment_link": "pl_jLdWvwZU5Wj8P1HvgE4F",
    "status": "succeeded"
  }
}

Verifying signatures

Every request carries an Onhere-Signature header: t is the timestamp and v1 is an HMAC-SHA256 of "{t}.{body}" using your endpoint signing secret (whsec_). Recompute it and compare before trusting the event.

<?php
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_ONHERE_SIGNATURE'] ?? '';
$secret = 'whsec_...'; // your endpoint signing secret

parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $payload, $secret);

if (hash_equals($expected, $parts['v1'])) {
  $event = json_decode($payload, true);
  // handle $event['type']
}
import crypto from 'node:crypto';

function verify(payload, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.' + payload)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Retries

If your endpoint does not return a 2xx status, we retry with increasing delays. After too many failures the endpoint is disabled and we notify you; you can re-enable it and resend deliveries from the dashboard.

Managing endpoints via the API

Create and remove endpoints programmatically — a plugin can self-register its callback URL on setup, with no manual dashboard step.

Create an endpoint

POST /v1/webhooks

Registers an endpoint and returns its whsec_ signing secret.

Parameters

Parameter Type Description
urlstringThe https URL that receives events. Required.
eventsarrayArray of event types to subscribe to. Defaults to all events.
descriptionstringAn optional label for the endpoint.
curl https://onhere.net/api/v1/webhooks \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://shop.example.com/?wc-api=upnow",
    "events": ["transaction.succeeded", "transaction.failed", "refund.succeeded"]
  }'
const res = await fetch('https://onhere.net/api/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://shop.example.com/?wc-api=upnow',
    events: ['transaction.succeeded', 'transaction.failed', 'refund.succeeded'],
  }),
});
const endpoint = await res.json();
// endpoint.secret is returned ONLY here — store it to verify signatures.
{
  "id": "whe_SX5TaMkJL7SQE62q1h9c",
  "object": "webhook_endpoint",
  "url": "https://shop.example.com/?wc-api=upnow",
  "description": null,
  "events": ["transaction.succeeded", "transaction.failed", "refund.succeeded"],
  "status": "enabled",
  "secret": "whsec_...",
  "created": 1783724733
}

The signing secret is returned only once, in this create response — store it securely.

List endpoints

GET /v1/webhooks

Returns your endpoints (without the signing secret).

Delete an endpoint

DELETE /v1/webhooks/{id}

Removes an endpoint by its id.

WooCommerce

Add Here as a payment method in your WooCommerce store. Setup is a single field — paste your API key and the plugin does the rest.

Download the plugin WordPress 5.8+ · WooCommerce 6.0+ · PHP 7.4+

1. Install

  1. Download the plugin ZIP above.
  2. In WordPress admin open Plugins → Add New → Upload Plugin, choose the ZIP, and click Install Now.
  3. Click Activate.

Prefer a manual install? Unzip the folder into wp-content/plugins/ and activate it from the Plugins screen.

2. Connect your key

  1. Open WooCommerce → Settings → Payments and enable Here (or click Manage).
  2. Paste your API secret key (sk_live_… or sk_test_…) and click Save changes.

That is the only field you need. On save the plugin validates your key and registers its webhook automatically — test or live mode is detected from the key prefix.

Where is my key? Sign in to your account and open Developers → API keys to copy your secret key.

3. How payments flow

Once connected, every order runs through Here:

Every webhook is verified with an HMAC-SHA256 signature, so only genuine Here events change your orders.

OpenCart

Add Here as a payment extension in your OpenCart store. Paste your API key and the extension registers its own webhook.

Download the extension OpenCart 3.0.x · PHP with cURL

1. Install

  1. Download the extension ZIP above and install it under Extensions → Installer (or copy the folders into your store root).
  2. Open Extensions → Extensions, choose Payments, find Here and click Install (+).
  3. Click Edit (the pencil) to open its settings.

Manual install? Copy the admin/, catalog/ and system/ folders into your OpenCart root.

2. Connect your key

  1. Set Status to Enabled (optionally pick the order statuses used for paid and failed orders).
  2. Paste your API secret key (sk_live_… or sk_test_…) and click Save.

That is the only required field. On save the extension validates your key and registers its webhook automatically. Your store must be reachable over public HTTPS with cURL enabled.

Where is my key? Sign in to your account and open Developers → API keys to copy your secret key.

3. How payments flow

Once connected, every order runs through Here:

Every webhook is verified with an HMAC-SHA256 signature, so only genuine Here events change your orders.

PrestaShop

Add Here as a payment module in your PrestaShop store. One API key — the module registers its webhook and manages order states for you.

Download the module PrestaShop 1.7 / 8 · PHP with cURL

1. Install

  1. In the Module Manager click Upload a module and select the module ZIP (or copy the upnow folder into /modules).
  2. Install the Here module.
  3. Click Configure.

Manual install? Copy the upnow folder into your shop's /modules directory, then install it from the Module Manager.

2. Connect your key

  1. Paste your API secret key (sk_live_… or sk_test_…) and click Save.

That is the only field you need. On save the module validates your key, registers its webhook, and creates the “Awaiting Here payment” order state automatically.

Where is my key? Sign in to your account and open Developers → API keys to copy your secret key.

3. How payments flow

Once connected, every order runs through Here:

Every webhook is verified with an HMAC-SHA256 signature, so only genuine Here events change your orders.

Magento 2

Add Here as a payment method in your Magento 2 store. Paste your API key and the module self-registers its webhook.

Download the module Magento 2.4.x · PHP with cURL

1. Install

  1. Copy the module to app/code/Here/Payments in your Magento root.
  2. From the Magento root run: bin/magento module:enable Here_Payments && bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush
  3. In production mode also run bin/magento setup:static-content:deploy.

Hosting the module in your own repository? Require it with Composer and run the same setup commands.

2. Connect your key

  1. Open Stores → Configuration → Sales → Payment Methods → Here and set Enabled to Yes.
  2. Paste your API secret key (sk_live_… or sk_test_…) and click Save config.

That is the only required field. Saving validates your key and registers the webhook automatically; the signing secret is stored for you.

Where is my key? Sign in to your account and open Developers → API keys to copy your secret key.

3. How payments flow

Once connected, every order runs through Here:

Every webhook is verified with an HMAC-SHA256 signature, so only genuine Here events change your orders.