Seev PlusDocs
Docs
Get Paid

Checkout API

Create a checkout session from your backend, redirect the payer, and verify the result before you fulfil the order.

POST /api/v1/developer/payments

Requires developer access and an API key

Use the Checkout API when your own application needs to start the payment, rather than a Dashboard tool. Your backend creates a checkout session through SeevPlus, which records the organization and environment against the transaction before the payer is sent to the hosted checkout page. If you only need to send someone a link or sell a product without writing code, use Payment Links, Invoices or the Storefront instead.

To get access, open the Developer Dashboard, accept the developer terms, select Checkout API as a product, and generate a key. The API Keys guide walks through that. If your integration still calls gateway-api.seevcash.com directly, follow Migrate to the SeevPlus Payment Routes first, because a direct gateway call cannot be attributed to an organization.

Trace one payment end to end

Customer
Your server
Seev
Clicks Pay
POST /developer/payments
201 · checkout_url
Redirected to hosted checkout
Collects payment
Lands on redirect_url
Session reference
GET /developer/payments/{ref}
status: completed
Fulfil the order
  1. Your server creates a checkout session with the customer and order details.

  2. Seev returns a checkout URL. Redirect the customer there.

  3. The customer pays with a method enabled on the hosted checkout page.

  4. Seev redirects the customer back to your redirect_url with a session reference.

  5. Your server verifies the session before fulfilling the order.

The redirect to your redirect_url is a hint, not proof. Customers close tabs, networks drop, and anyone who knows the URL can request it. Fulfil only after step 5, a verified status from Seev.

Create sessions from a backend or server function and keep the secret key in a server-side environment variable, because the key authorizes payment creation for your organization. Save your own order record before you request a session, so that you have something to reconcile the returned reference against.

Authenticate a request

Both routes live on one host. Sandbox and production are selected by the key you send, not by a different base URL, so a sample on this page runs against sandbox when you export a sandbox key into SEEV_CHECKOUT_API_KEY.

ActionMethodRoute
Create checkout sessionPOSThttps://api.seevplus.com/api/v1/developer/payments
Verify checkout sessionGEThttps://api.seevplus.com/api/v1/developer/payments/{sessionRef}
AuthorizationReq
header

Bearer <secret key>. Interchangeable with X-API-Key, which Seev reads first.

Idempotency-Key
header

Any string up to 128 characters, one per logical order. Strongly recommended.

Content-TypeReq
header

application/json.

Send the secret as Authorization: Bearer $SEEV_CHECKOUT_API_KEY, or as X-API-Key: $SEEV_CHECKOUT_API_KEY if that suits your HTTP client better. The verify route takes no credentials at all. If you generate keys for several products, use the one created for Checkout API, since product keys are scoped to their own Seev services.

SeevPlus stores only a one-way hash of newly issued checkout secrets and never persists the plaintext. A checkout key created before this route existed has no stored hash to match, so rotate it once before it can authenticate here.

Official SDKs, browser packages, mobile SDKs, and platform plugins such as WordPress or WooCommerce are not available yet. Call the REST API from your backend.

Create a checkout session

Amounts and item prices are integers in the currency's smallest unit, so 10000 means GHS 100.00 and the 800 below means GHS 8.00. Send either amount or items; the gateway calculates the final amount either way.

curl -X POST "https://api.seevplus.com/api/v1/developer/payments" \
  -H "Authorization: Bearer $SEEV_CHECKOUT_API_KEY" \
  -H "Idempotency-Key: order_123_attempt_1" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "checkout",
    "recipient": {
      "name": "Kwame Asante",
      "email": "kwame@example.com"
    },
    "items": [
      {
        "name": "Web Development",
        "description": "Landing page design",
        "quantity": 1,
        "price": 800,
        "image": "https://example.com/web-development.png"
      },
      {
        "name": "Hosting (1 year)",
        "description": "Cloud hosting plan",
        "quantity": 1,
        "price": 200,
        "image": ""
      }
    ],
    "currency": "GHS",
    "channels": ["mobile_money"],
    "redirect_url": "https://yourapp.com/payment/callback",
    "meta": {
      "orderId": "order_123"
    }
  }'
const response = await fetch(
    'https://api.seevplus.com/api/v1/developer/payments',
    {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${process.env.SEEV_CHECKOUT_API_KEY}`,
            'Idempotency-Key': 'order_123_attempt_1',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            type: 'checkout',
            recipient: { name: 'Kwame Asante', email: 'kwame@example.com' },
            items: [
                {
                    name: 'Web Development',
                    description: 'Landing page design',
                    quantity: 1,
                    price: 800, // GHS 8.00
                    image: 'https://example.com/web-development.png',
                },
                {
                    name: 'Hosting (1 year)',
                    description: 'Cloud hosting plan',
                    quantity: 1,
                    price: 200, // GHS 2.00
                    image: '',
                },
            ],
            currency: 'GHS',
            channels: ['mobile_money'],
            redirect_url: 'https://yourapp.com/payment/callback',
            meta: { orderId: 'order_123' },
        }),
    },
);

const payload = await response.json();

if (response.status !== 201) {
    throw new Error(payload.error ?? 'checkout creation failed');
}

console.log(payload.data.checkout_url);
import os
import requests

response = requests.post(
    "https://api.seevplus.com/api/v1/developer/payments",
    headers={
        "Authorization": f"Bearer {os.environ['SEEV_CHECKOUT_API_KEY']}",
        "Idempotency-Key": "order_123_attempt_1",
        "Content-Type": "application/json",
    },
    json={
        "type": "checkout",
        "recipient": {"name": "Kwame Asante", "email": "kwame@example.com"},
        "items": [
            {
                "name": "Web Development",
                "description": "Landing page design",
                "quantity": 1,
                "price": 800,  # GHS 8.00
                "image": "https://example.com/web-development.png",
            },
            {
                "name": "Hosting (1 year)",
                "description": "Cloud hosting plan",
                "quantity": 1,
                "price": 200,  # GHS 2.00
                "image": "",
            },
        ],
        "currency": "GHS",
        "channels": ["mobile_money"],
        "redirect_url": "https://yourapp.com/payment/callback",
        "meta": {"orderId": "order_123"},
    },
    timeout=30,
)

payload = response.json()

if response.status_code != 201:
    raise RuntimeError(payload.get("error", "checkout creation failed"))

print(payload["data"]["checkout_url"])
<?php

$body = [
    'type' => 'checkout',
    'recipient' => ['name' => 'Kwame Asante', 'email' => 'kwame@example.com'],
    'items' => [
        [
            'name' => 'Web Development',
            'description' => 'Landing page design',
            'quantity' => 1,
            'price' => 800, // GHS 8.00
            'image' => 'https://example.com/web-development.png',
        ],
        [
            'name' => 'Hosting (1 year)',
            'description' => 'Cloud hosting plan',
            'quantity' => 1,
            'price' => 200, // GHS 2.00
            'image' => '',
        ],
    ],
    'currency' => 'GHS',
    'channels' => ['mobile_money'],
    'redirect_url' => 'https://yourapp.com/payment/callback',
    'meta' => ['orderId' => 'order_123'],
];

$curl = curl_init('https://api.seevplus.com/api/v1/developer/payments');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('SEEV_CHECKOUT_API_KEY'),
        'Idempotency-Key: order_123_attempt_1',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
]);

$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);

$payload = json_decode($response, true);

if ($status !== 201) {
    throw new RuntimeException($payload['error'] ?? 'checkout creation failed');
}

echo $payload['data']['checkout_url'];
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    body, err := json.Marshal(map[string]any{
        "type": "checkout",
        "recipient": map[string]string{
            "name":  "Kwame Asante",
            "email": "kwame@example.com",
        },
        "items": []map[string]any{
            {
                "name":        "Web Development",
                "description": "Landing page design",
                "quantity":    1,
                "price":       800, // GHS 8.00
                "image":       "https://example.com/web-development.png",
            },
            {
                "name":        "Hosting (1 year)",
                "description": "Cloud hosting plan",
                "quantity":    1,
                "price":       200, // GHS 2.00
                "image":       "",
            },
        },
        "currency":     "GHS",
        "channels":     []string{"mobile_money"},
        "redirect_url": "https://yourapp.com/payment/callback",
        "meta":         map[string]string{"orderId": "order_123"},
    })
    if err != nil {
        panic(err)
    }

    req, err := http.NewRequest(
        http.MethodPost,
        "https://api.seevplus.com/api/v1/developer/payments",
        bytes.NewReader(body),
    )
    if err != nil {
        panic(err)
    }

    req.Header.Set("Authorization", "Bearer "+os.Getenv("SEEV_CHECKOUT_API_KEY"))
    req.Header.Set("Idempotency-Key", "order_123_attempt_1")
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var payload struct {
        Error string `json:"error"`
        Data  struct {
            CheckoutURL string `json:"checkout_url"`
        } `json:"data"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
        panic(err)
    }

    if resp.StatusCode != http.StatusCreated {
        panic(payload.Error)
    }

    fmt.Println(payload.Data.CheckoutURL)
}

The request body is the Seev gateway initiation body, passed through unchanged apart from meta. Do not set organizationId or user_id yourself. SeevPlus writes organizationId, user_id, seev_origin and seev_environment into meta from the authenticated key, overwriting any values you send under those four names, and preserves the rest of your meta untouched.

A successful create returns HTTP 201 and the gateway initiation response, again unchanged, so gateway fields such as env, item IDs, item session IDs, empty values and fields added by the gateway in future all survive the round trip. Amounts in the response follow the same smallest-unit convention, so "amount": 1000 is GHS 10.00.

{
    "success": true,
    "data": {
        "amount": 1000,
        "channels": ["mobile_money"],
        "checkout_url": "https://pay.seevplus.com/PAY-20260718-f23159df-9df8-4e69-8306-c11d71ead351",
        "currency": "GHS",
        "env": "production",
        "expires_at": "2026-07-18T21:08:46.776302Z",
        "items": [
            {
                "id": "f0c72f6b-59fc-4371-b68b-540f54d12f5b",
                "session_id": "d636387f-6872-469e-be31-76a79301fcb1",
                "name": "Web Development",
                "description": "Landing page design",
                "quantity": 1,
                "price": 800,
                "image": "https://example.com/web-development.png"
            },
            {
                "id": "62342bee-7eeb-4f31-984a-96718e2fd997",
                "session_id": "d636387f-6872-469e-be31-76a79301fcb1",
                "name": "Hosting (1 year)",
                "description": "Cloud hosting plan",
                "quantity": 1,
                "price": 200,
                "image": ""
            }
        ],
        "recipient": {
            "email": "kwame@example.com",
            "name": "Kwame Asante"
        },
        "redirect_url": "https://yourapp.com/payment/callback",
        "reference": "PAY-20260718-f23159df-9df8-4e69-8306-c11d71ead351",
        "status": "pending"
    },
    "request_id": "fb4f88c-9429-41d7-b3ca-5e4226a50736",
    "timestamp": "2026-07-18T20:38:46.779595765Z"
}

Store data.reference against your order, then redirect the customer to data.checkout_url. SeevPlus records its own transaction from the gateway's answer, copying:

  • the gateway-calculated amount and currency
  • the status and the reference
  • the checkout URL and the expiry timestamp

Request fields

typeReq
"checkout" | "invoice" | "payment_link"

Payment context recorded on the session. Use checkout for your own app or storefront flow.

amountReq
integer

Total in the currency's smallest unit, so 10000 is GHS 100.00. Mutually exclusive with items, and one of the two is required.

items
array

Objects with name, quantity and price, where price is per unit in the smallest unit. description and image are optional. Use instead of amount.

currencyReq
string

One of GHS, USD, EUR, GBP, NGN, USDC, USDT. Uppercase.

recipient.nameReq
string

Customer's full name, shown on the hosted checkout page.

recipient.emailReq
string

Deliverable email address. Receipts are sent here.

recipient.phone
string

Customer's phone number.

redirect_urlReq
string

HTTP or HTTPS URL the customer returns to after checkout. Treat arrival there as unverified.

channels
string[]

Restricts the methods offered. Omit to offer every method enabled for hosted checkout.

phone
string

Top-level, not inside recipient. Required when channels is ["ussd"].

network
string

MTN, VODAFONE or AIRTEL. Required when channels is ["ussd"].

delay
integer

Seconds to wait before the USSD intent executes, 0 to 120. Ignored outside USSD.

discount
number

A percentage, deducted from the subtotal before tax.

tax
number

A percentage, applied to the amount left after the discount.

subaccount
string

A SUB_ code that routes a percentage of this payment to a subaccount. Cannot be combined with the crypto channel. See Subaccounts.

meta
object

Your own JSON, passed to the gateway. The keys organizationId, user_id, seev_origin and seev_environment are overwritten by SeevPlus.

Handle a failed create

Errors come back with the HTTP status below and a body carrying the same text twice, as {"error": "...", "message": "..."}. There is no machine-readable code field, so branch on the status.

400
Invalid request body.

The body could not be read or is not valid JSON. Check Content-Type: application/json and that you sent a complete body.

400
Not a JSON object.

The body parsed but is an array or a scalar. Send a single JSON object.

400
Idempotency-Key too long.

The header exceeds 128 characters. Derive a shorter key, such as a hash of your order ID.

400
Organization is not active.

Check organization verification before retrying.

401
API key is required.

Neither Authorization: Bearer nor X-API-Key was sent.

401
Invalid API key.

The secret does not match a stored hash, or predates this route. Rotate the key once in the Developer Dashboard, then use the new secret.

401
Key is not linked to an organization.

Regenerate the key from inside the organization that should receive the payment.

403
Request IP is not allowed.

The key has an IP allowlist and your server's address is not on it. Add the calling server's egress IP, or turn the allowlist off.

403
Payments are paused for this business.

Seev has paused live payments for your business. No session was created and a retry will not help. Contact support to have them turned back on. Sandbox keys keep working in the meantime.

502
Upstream rejected or did not answer.

The gateway's own status code and body are appended to message. No session was created, so a retry is safe. A conflicting idempotency key and an upstream rate limit both arrive this way.

A 502 means no checkout session exists, so retry with a fresh idempotency key rather than reusing the one that failed.

Retry safely with an idempotency key

Send Idempotency-Key on every create. The header is optional, and without it every request creates a new payment, which is how a network timeout turns into a customer being charged twice. Generate one stable key per logical order and reuse it for retries of that order rather than generating a new random key on each attempt.

SeevPlus forwards Idempotency-Key upstream rather than deduplicating on its own. An identical retry within 24 hours replays the original response, and the same key sent with a different body is rejected and reaches you as a 502 carrying the upstream conflict. Keys are limited to 128 characters. See Idempotency for the full behaviour.

Treat this as a safety net rather than a guarantee, and keep your own handler safe to run twice.

Same key, same body

The original response is replayed. Nothing new is created.

Same key, different body

Rejected upstream and returned as 502. Nothing is created.

Key lifetime

24 hours. After that the key is forgotten and a new session is created.

SeevPlus stores the key and a SHA-256 hash of the outgoing request body on the transaction it records, so a duplicate is at least visible during reconciliation even where it was not prevented.

Initiate a direct USSD payment

USSD returns a dial intent instead of a page to redirect to, which makes it a server-driven flow rather than a hosted one. It must be the only entry in channels, and phone and network go at the top level of the body rather than inside recipient.

curl -X POST "https://api.seevplus.com/api/v1/developer/payments" \
  -H "Authorization: Bearer $SEEV_CHECKOUT_API_KEY" \
  -H "Idempotency-Key: order_456_ussd_1" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "checkout",
    "recipient": {
      "name": "Ama Mensah",
      "email": "ama@example.com"
    },
    "amount": 10000,
    "currency": "GHS",
    "channels": ["ussd"],
    "phone": "233241234567",
    "network": "MTN",
    "delay": 0,
    "redirect_url": "https://yourapp.com/payment/callback",
    "meta": {
      "orderId": "order_456"
    }
  }'

The amount of 10000 above is GHS 100.00, in the currency's smallest unit. A successful USSD response carries phone, network, delay and an intent object describing the lifecycle, and carries no checkout_url:

{
    "success": true,
    "data": {
        "reference": "PAY-20260825-abc123",
        "status": "pending",
        "phone": "233241234567",
        "network": "MTN",
        "delay": 0,
        "intent": {
            "type": "ussd",
            "status": "pending",
            "delay": 0,
            "execute_at": "2026-08-25T12:00:00Z",
            "requires_action": false
        }
    }
}

Because there is no redirect to bring the customer back, store the returned reference and poll the verify route until the session reaches a final status. Verification responses for a USSD session can also include ussd_intent_status, ussd_execute_at, ussd_initiated_at and ussd_failure_reason, which is where to look when a customer says no prompt arrived.

Split a payment with a subaccount

Pass a subaccount code as subaccount and Seev splits the payment between your main balance and that subaccount, each settling with its own payout record. Fees come off the top, then both sides take their percentage.

{
    "type": "checkout",
    "amount": 10000,
    "currency": "GHS",
    "subaccount": "SUB_9f2k1p7qw4r8c3e1",
    "recipient": { "name": "Ama Mensah", "email": "ama@example.com" },
    "redirect_url": "https://yourapp.com/payment/callback"
}

The code is resolved before the session is created. It has to belong to your organization in the same environment as the key, be active, be verified, and hold the same currency as the payment. If any of those fails the create is rejected rather than the split being quietly dropped, and it reaches you as a 502 carrying the upstream reason.

The share comes from the subaccount's default and is fixed onto the session at creation, so changing it later does not rewrite payments already made. A successful session carries a split object with the code, the business name and the share applied.

A subaccount has to exist before you can attach it. Create one from the dashboard or through the API, then pass its code here. Subaccounts covers setting one up and what happens to the money once a split payment succeeds.

Choose the channels you offer

Omit channels to offer every method enabled for hosted checkout, or pass a subset to restrict the hosted page. The three values below are accepted today.

mobile_money
Mobile money

MTN MoMo, Telecel Cash and AT Money, on a Ghana cedi checkout.

ussd
USSD

A server-side intent rather than a button on the hosted page. Do not combine it with another value.

Coming soon
Card

Card issuing is live so your team can spend from a funded virtual card. Collecting from a customer by card is not.

Coming soon
Bank transfer

Appears in dashboard previews as upcoming.

crypto
USDC

The customer pays in USDC on Stellar, and the money lands in your USDC account. Production only, and not available on a payment with a subaccount.

Before you offer USDC

A customer paying in USDC is paying into your USDC account, so you need one. You do not get one by default.

Automatically. The first live request that includes crypto opens your USDC account for you. That one request can take a few seconds longer than usual.

Ahead of time. In the dashboard, open Accounts and choose Add USDC. Do this first if you would rather not have the wait land on a customer's checkout.

Your organization has to be verified to hold USDC. If the account cannot be opened, Seev leaves crypto out and the checkout offers your other channels. Read channels in the response to see what the customer will be shown.

Do not offer a customer a method marked coming soon until it is selectable on the live checkout page they opened. Dashboards and previews show unavailable methods as upcoming, which is easy to read as available. Payment Methods covers current availability and what to tell a customer whose payment stalls.

Verify the payment before you fulfil

The redirect to your redirect_url is not proof of payment. Anyone who knows the URL can request it. Fulfil only after a server-side check returns a paid status.

Look the session up by the gateway reference you stored at creation, or by the session reference you receive on the redirect. The route takes no API key.

curl -X GET \
  "https://api.seevplus.com/api/v1/developer/payments/$SESSION_REF"
const response = await fetch(
    `https://api.seevplus.com/api/v1/developer/payments/${sessionRef}`,
);

const payload = await response.json();

if (!response.ok) {
    throw new Error(payload.error ?? 'verification failed');
}

const paid = ['completed', 'success'].includes(payload.data.status);
import requests

response = requests.get(
    f"https://api.seevplus.com/api/v1/developer/payments/{session_ref}",
    timeout=30,
)

payload = response.json()

if not response.ok:
    raise RuntimeError(payload.get("error", "verification failed"))

paid = payload["data"]["status"] in ("completed", "success")
<?php

$curl = curl_init(
    'https://api.seevplus.com/api/v1/developer/payments/' . rawurlencode($sessionRef)
);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);

$payload = json_decode($response, true);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException($payload['error'] ?? 'verification failed');
}

$paid = in_array($payload['data']['status'], ['completed', 'success'], true);
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
)

func main() {
    sessionRef := "SESSION_REFERENCE"

    resp, err := http.Get(
        "https://api.seevplus.com/api/v1/developer/payments/" + url.PathEscape(sessionRef),
    )
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var payload struct {
        Error string `json:"error"`
        Data  struct {
            Status string `json:"status"`
        } `json:"data"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
        panic(err)
    }

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        panic(payload.Error)
    }

    paid := payload.Data.Status == "completed" || payload.Data.Status == "success"
    fmt.Println(paid)
}

Session status

pending

Not yet confirmed. Do not fulfil.

completed

Funds confirmed. Safe to fulfil.

success

Also paid. Treat it the same as completed.

failed

The payment did not go through.

cancelled

Stopped before payment.

Treat completed or success as paid, and everything else as not fulfilled.

Handle a failed verify

400
Session reference is required.

The path segment was empty or whitespace.

404
Session not found or invalid.

The reference does not match a session.

502
Upstream did not answer.

The gateway did not answer the verification call.

Never map a 404 or a 502 onto a failure in your own order record. Neither one says the customer was not charged.

Fix common integration problems

What this route does not do yet
  • Recurring or saved-card payments. Each session is a single charge.

  • Refunds through this API.
  • Durable gateway-event processing. Outbound webhooks fire from a status synchronization rather than an independent gateway event stream, which Verify Payments covers.

  • Official SDKs, browser and mobile packages, and platform plugins such as WordPress or WooCommerce.

Need one of these? Tell us, it shapes what gets built next.

On this page