Verify Payments
Confirm on your server that a payment really succeeded before you fulfil an order or grant access.
API product
Requires developer access and an API key
Seev sends the customer back to your redirect_url when checkout finishes, but that request proves only that someone opened a URL. Anyone who knows your callback URL can open it too, with any query string they like. Verify the payment from your server before you release goods, grant access or mark an order paid.
Fulfilment is hard to reverse. Do not fulfil on the redirect, on a customer
screenshot, or on a pending status. Fulfil on a paid status returned by
the verify route or carried by a signed webhook.
SDK helpers and platform plugins are not available yet, so the REST examples below are the production path today.
Verify a session after the redirect
Look the session up by the gateway reference returned when you created the checkout session, or by the session reference that arrives on the redirect. This route is the SeevPlus wrapper around the gateway session lookup and takes no API key or Authorization header, which lets a callback route check a status without holding a secret.
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)
}Amounts in the response are integers in the currency's smallest unit, so the 10000 below is GHS 100.00.
{
"success": true,
"data": {
"id": "checkout_xyz",
"reference": "PAY-20260701-abc123",
"status": "completed",
"amount": 10000,
"final_amount": 10000,
"currency": "GHS"
}
}| Name | Type | Required | Description |
|---|---|---|---|
data.id | string | Always | Checkout session identifier. |
data.reference | string | Always | Gateway payment reference. Store this against your order and reconcile against it. |
data.status | string | Always | Raw gateway status: completed, success, pending, failed or cancelled. |
data.amount | integer | Always | Amount requested, in the currency's smallest unit, so 10000 is GHS 100.00. |
data.final_amount | integer | Always | Amount confirmed by the session, in the same smallest unit. Compare it against your order total before fulfilling. |
data.currency | string | Always | ISO 4217 code, uppercase. |
Treat completed or success as paid. Treat pending, failed and cancelled as not paid, and treat anything you do not recognise as not paid.
Calling this route also synchronizes the SeevPlus transaction record with the gateway status, and that synchronization is what triggers an outbound payment.succeeded or payment.failed webhook. A session nobody ever verifies may therefore never produce a webhook.
Example callback handler
// GET /payment/callback?sessionRef=PAY-20260701-abc123
export async function GET(req: Request) {
const url = new URL(req.url);
const sessionRef =
url.searchParams.get('sessionRef') ||
url.searchParams.get('session_ref') ||
url.searchParams.get('session_id');
if (!sessionRef) {
return Response.redirect('/order/failed?reason=missing_session');
}
const response = await fetch(
`https://api.seevplus.com/api/v1/developer/payments/${encodeURIComponent(sessionRef)}`,
);
if (!response.ok) {
// 404 or 502 means unknown, not failed. Do not cancel the order here.
return Response.redirect('/order/pending');
}
const result = await response.json();
const status = result?.data?.status;
if (status === 'completed' || status === 'success') {
await fulfillOrder(result.data.reference);
return Response.redirect('/order/confirmed');
}
return Response.redirect(`/order/failed?status=${status || 'unknown'}`);
}Handle a failed lookup
Errors arrive as {"error": "...", "message": "..."} with both fields carrying the same text.
| Status | error text | Cause | What to do |
|---|---|---|---|
| 400 | session reference is required | The path segment was empty or whitespace only. | Check that your callback read a reference before calling, as in the handler above. |
| 404 | failed to fetch payment session: Failed to verify checkout session: session not found or invalid | The reference matches no gateway session, or the session expired. | Do not fulfil and do not cancel. Compare the reference against the one you stored at creation. |
| 502 | failed to fetch payment session: ... | The gateway did not answer the lookup. Its own text is appended. | Treat the payment as unknown, retry with a backoff, and leave the order pending in the meantime. |
A failure to read a status is not a failed payment. Mapping either of these onto a cancelled order is the fastest way to lose money that a customer has already paid.
Reconcile a payment after the fact
Use the Developer Transactions API when you need a payment that was not created by the request you are handling: a status page that polls, a reconciliation job, or a webhook you want to confirm against a second source. See Developer Transactions for the active organization's searchable history.
| Status | Meaning |
|---|---|
completed / success | Payment succeeded. Safe to fulfil. |
pending | Initiated but not confirmed. Do not fulfil. |
failed | Did not go through. The customer can retry. |
cancelled | Cancelled or no longer payable. Create a new request. |
Fulfil from a webhook
The verify call gives the customer an answer in the browser. A webhook is what tells your backend about a payment when the customer closed the tab, lost signal, or completed a USSD prompt with no redirect involved. Use both: the callback drives your UI, the webhook drives fulfilment.
Two events are delivered, payment.succeeded and payment.failed. Each request carries X-Seev-Event-ID, X-Seev-Event-Type, X-Seev-Timestamp and X-Seev-Signature, where the signature is an HMAC-SHA256 over the timestamp and the raw body using the webhook's signing secret. See Webhooks for endpoint setup, delivery logs and signature verification.
Three properties of the current delivery behaviour will affect your handler:
- Delivery is attempted once, with an 8 second timeout. A slow or briefly unavailable endpoint drops the event, and you replay it manually from the Dashboard delivery log.
- Deliveries for an organization are dispatched concurrently and carry no sequence number, so events can arrive out of order. Decide fulfilment from the transaction state in the payload rather than from arrival order, and ignore an event that would move an order backwards.
- Because dispatch is driven by a status synchronization, the same event can be delivered more than once. Key your handler on
X-Seev-Event-IDand make fulfilment idempotent.
Choose a verification method
| Scenario | Method |
|---|---|
| Showing the customer a result right after the redirect | Verify the session reference with the verify route |
| Releasing goods, granting access or sending a receipt | Signed webhook event |
| Looking up a past payment by reference | Developer Transactions API |
| Following a USSD payment that has no redirect | Poll the verify route until the status is final |