Savings
Create automated savings plans for your customers. Supports contributions, withdrawals, and full lifecycle management.
List Savings Plans
GET /savings/plans/
Retrieve all savings plans for your integration. Results are paginated.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number to fetch. Defaults to 1. |
per_page | integer | No | Number of plans per page. Defaults to 20. |
customer_id | string | No | Filter by customer identifier. |
status | string | No | Filter by plan status (active, paused, cancelled, completed, failed). |
currency | string | No | Filter by NGN or USD. |
q | string | No | Search plan title or customer name. |
Request
- cURL
- Node.js
- Python
- Go
curl -X GET "https://sandbox.payscribe.ng/api/v1/savings/plans/?page=1&per_page=20" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/?page=1&per_page=20', {
method: 'GET',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.get(
'https://sandbox.payscribe.ng/api/v1/savings/plans/?page=1&per_page=20',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodGet, "https://sandbox.payscribe.ng/api/v1/savings/plans/?page=1&per_page=20", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plans fetched.",
"message": {
"details": {
"plans": [{
"id": "svp_abc123",
"title": "Emergency fund",
"type": "fixed",
"currency": "NGN",
"status": "active",
"contribution_amount": 10000,
"frequency": "monthly",
"balance": 140000,
"available_balance": 140000,
"total_contributed": 140000,
"total_withdrawn": 0,
"next_run_at": "2026-09-01 09:00:00",
"created_at": "2026-08-01 09:00:00",
"customer": {"id": "cus_xyz456", "name": "John Doe", "email": "customer@example.com"}
}],
"total": 1,
"page": 1,
"per_page": 20
}
},
"status_code": 200
}
Create Savings Plan
POST /savings/plans/
Create a new automated savings plan for a customer.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
| Content-Type | application/json |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Customer identifier. |
currency | string | Yes | NGN or USD. |
type | string | Yes | target or fixed. A target plan also needs target_amount or target_date. |
title | string | Yes | Human-readable plan title (maximum 100 characters). |
contribution_amount | number | Yes | Amount per contribution. |
frequency | string | Yes | Contribution frequency. One of daily, weekly, monthly. |
start_at | string | Yes | Start date and time, for example 2026-09-01 09:00:00. |
timezone | string | No | IANA timezone; defaults to UTC. |
lock_type | string | No | flexible (default) or locked. |
missed_policy | string | No | retry_until_success (default), skip_and_continue, or pause_on_failure. |
target_amount | number | Conditional | Target savings amount. Required for a target plan if target_date is omitted. Ignored for fixed plans. |
target_date | string | Conditional | Target date (YYYY-MM-DD). Required for a target plan if target_amount is omitted. Ignored for fixed plans. |
early_withdrawal_penalty_bps | integer | No | Penalty in basis points applied to a withdrawal made before maturity. Only used when lock_type is locked. |
retry_max | integer | No | Max retries for a failed contribution charge (0-20, default 3). |
retry_interval_minutes | integer | No | Minutes between retries (5-10080, default 1440). |
Request
- cURL
- Node.js
- Python
- Go
curl -X POST "https://sandbox.payscribe.ng/api/v1/savings/plans/" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cus_xyz456",
"currency": "NGN",
"type": "fixed",
"title": "Emergency fund",
"contribution_amount": 10000,
"frequency": "monthly",
"timezone": "Africa/Lagos",
"start_at": "2026-09-01 09:00:00"
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"customer_id": "cus_xyz456",
"currency": "NGN",
"type": "fixed",
"title": "Emergency fund",
"contribution_amount": 10000,
"frequency": "monthly",
"timezone": "Africa/Lagos",
"start_at": "2026-09-01 09:00:00"
}),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
import json
payload = json.loads(r'''{
"customer_id": "cus_xyz456",
"currency": "NGN",
"type": "fixed",
"title": "Emergency fund",
"contribution_amount": 10000,
"frequency": "monthly",
"timezone": "Africa/Lagos",
"start_at": "2026-09-01 09:00:00"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/savings/plans/',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}", 'Content-Type': 'application/json'},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"strings"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := strings.NewReader(`{
"customer_id": "cus_xyz456",
"currency": "NGN",
"type": "fixed",
"title": "Emergency fund",
"contribution_amount": 10000,
"frequency": "monthly",
"timezone": "Africa/Lagos",
"start_at": "2026-09-01 09:00:00"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/savings/plans/", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan created.",
"message": {
"details": {
"id": 123,
"saving_id": "svp_abc123",
"title": "Emergency fund",
"type": "fixed",
"currency": "NGN",
"frequency": "monthly",
"status": "active",
"next_run_at": "2026-09-01 09:00:00",
"customer": {"id": "cus_xyz456", "name": "John Doe", "email": "customer@example.com"}
}
},
"status_code": 200
}
Get Savings Plan
GET /savings/plans/{id}
Retrieve the details of a single savings plan.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID to retrieve. |
Request
- cURL
- Node.js
- Python
- Go
curl -X GET "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123', {
method: 'GET',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.get(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodGet, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan fetched.",
"message": {
"details": {
"saving_id": "svp_abc123",
"title": "Emergency fund",
"type": "fixed",
"currency": "NGN",
"status": "active",
"contribution_amount": 10000,
"frequency": "monthly",
"timezone": "Africa/Lagos",
"lock_type": "flexible",
"balance": 140000,
"locked_balance": 0,
"available_balance": 140000,
"total_contributed": 140000,
"total_withdrawn": 0,
"target_amount": null,
"target_date": null,
"start_at": "2026-09-01 09:00:00",
"next_run_at": "2026-10-01 09:00:00",
"missed_policy": "retry_until_success",
"early_withdrawal_penalty_bps": null,
"created_at": "2026-08-01 09:00:00",
"updated_at": "2026-09-01 09:00:00",
"customer": {"id": "cus_xyz456", "name": "John Doe", "email": "customer@example.com", "phone": "+2348012345678"}
}
},
"status_code": 200
}
List Savings Plan Transactions
GET /savings/plans/{id}/transactions
Retrieve the contribution and withdrawal history for a single savings plan. Results are paginated.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID. |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number to fetch. Defaults to 1. |
page_size | integer | No | Records per page (1-100). Defaults to 25. |
status | string | No | Filter by success, failed, or pending. |
from | string | No | Start date (YYYY-MM-DD), inclusive. |
to | string | No | End date (YYYY-MM-DD), inclusive. |
- cURL
- Node.js
- Python
- Go
curl -X GET "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/transactions?page=1&page_size=25" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/transactions?page=1&page_size=25', {
method: 'GET',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.get(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/transactions?page=1&page_size=25',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodGet, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/transactions?page=1&page_size=25", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan transactions fetched.",
"message": {
"details": {
"saving_id": "svp_abc123",
"plan_id": 42,
"transactions": [{
"id": 0,
"trans_id": "TRX-20260901-0001",
"description": "Monthly contribution",
"amount": 10000,
"currency": "NGN",
"fee": 0,
"status": "success",
"created_at": "2026-09-01 09:00:05",
"meta": null
}],
"total": 1,
"page": 1,
"per_page": 25
}
},
"status_code": 200
}
transactions[].id is currently always 0 (the endpoint selects the wrong column internally). Use trans_id to identify a transaction until this is fixed server-side.
Pause Savings Plan
POST /savings/plans/{id}/pause
Pause an active savings plan. Contributions will stop until the plan is resumed.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID to pause. |
Request
- cURL
- Node.js
- Python
- Go
curl -X POST "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/pause" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/pause', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/pause',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/pause", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan paused.",
"message": {"details": []},
"status_code": 200
}
Resume Savings Plan
POST /savings/plans/{id}/resume
Resume a paused savings plan. Contributions will restart on the next scheduled cycle.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID to resume. |
Request
- cURL
- Node.js
- Python
- Go
curl -X POST "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/resume" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/resume', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/resume',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/resume", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan resumed.",
"message": {"details": []},
"status_code": 200
}
Cancel Savings Plan
POST /savings/plans/{id}/cancel
Cancel an active or paused savings plan. The plan cannot be reactivated after cancellation. Remaining balance is available for withdrawal.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID to cancel. |
Request
- cURL
- Node.js
- Python
- Go
curl -X POST "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/cancel" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/cancel', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/cancel',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/cancel", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Savings plan cancelled.",
"message": {"details": []},
"status_code": 200
}
Withdraw from Savings Plan
POST /savings/plans/{id}/withdraw
Withdraw funds from an active or cancelled savings plan. Withdrawals are processed instantly to the customer's wallet.
Headers
| Field | Value |
|---|---|
| Authorization | Bearer PAYSCRIBE_API_KEY |
| Content-Type | application/json |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The savings plan ID to withdraw from. |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount to withdraw, in the plan's currency major unit (naira for NGN, dollars for USD). Must not exceed available_balance. |
ref | string | No | Idempotency reference. A random one is generated if omitted. |
Unlike other write endpoints, reusing ref here is a genuine idempotent replay: if the original withdrawal already succeeded, the same withdrawal_id is returned again with status_code: 200 instead of an error. Reusing a ref whose withdrawal is still pending or failed returns 400 Duplicate request.
Request
- cURL
- Node.js
- Python
- Go
curl -X POST "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/withdraw" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 25000, "ref": "WD-20260828-001"}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/withdraw', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"amount": 25000,
"ref": "WD-20260828-001"
}),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
import json
payload = json.loads(r'''{
"amount": 25000,
"ref": "WD-20260828-001"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/withdraw',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}", 'Content-Type': 'application/json'},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"strings"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := strings.NewReader(`{
"amount": 25000,
"ref": "WD-20260828-001"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/savings/plans/svp_abc123/withdraw", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Response
Status: 200 OK
{
"status": true,
"description": "Withdrawal successful.",
"message": {
"details": {
"withdrawal_id": "swd_xyz789",
"amount": 25000,
"currency": "NGN",
"ledger_ref": "PS_WD_ABC123XYZ",
"ref": "WD-20260828-001"
}
},
"status_code": 200
}
Webhooks
| Event | Description |
|---|---|
savings.plans.created | A savings plan was created |
savings.plans.paused | Sent whenever a plan's status changes via pause, resume, or cancel — check the status field in the payload (paused, active, or cancelled) to tell them apart |
savings.contribution.success | Contribution collected successfully |
savings.contribution.failed | Contribution failed |
savings.withdrawal.success | Withdrawal processed |
savings.withdrawal.failed | Withdrawal failed |
Resuming or cancelling a plan currently dispatches savings.plans.paused (not savings.plans.resumed or savings.plans.cancelled) — a naming bug in the current release. Read the status field in the payload rather than relying on the event name to distinguish these three transitions.
See Webhooks for payload format and verification.
Was this page helpful?