Safepay Raast lets you orchestrate Request To Pay flows on behalf of your merchants. Follow this quickstart to get credentials, link a merchant, and send an RTP request.
Prerequisites
{{aggregator_id}} issued by Safepay
{{secret_key}} for the target environment
- A Raast merchant to link during merchant creation
Environments
| Environment | Base URL | Notes |
|---|
| Sandbox | https://dev.api.getsafepay.com/raastwire | Use for integration testing. Data resets periodically. |
| Production | https://api.getsafepay.com/raastwire | Live traffic. Requests require enabled merchants and approved KYC/KYB. |
Request an aggregator account
Secure the aggregator secret key
Store {{secret_key}} in your secret manager and scope access to your backend jobs only. Every Raast API request must send the header X-SFPY-AGGREGATOR-SECRET-KEY: {{secret_key}}.
Link a merchant to a Raast merchant
Create an aggregator merchant with a unique merchant_external_id, settlement iban, and a Raast merchant token. The response returns an aggregator_merchant_identifier you will use in every payment request. Trigger your first RTP payment
Never ship {{secret_key}} in client-side code. Rotate credentials immediately if exposed.
Create your first merchant
Each merchant you manage must be linked to a Raast merchant. Set {{base_url}} to https://dev.api.getsafepay.com/raastwire in Sandbox or https://api.getsafepay.com/raastwire in Production.
curl --request POST "{{base_url}}/v1/aggregators/{{aggregator_id}}/merchants" \
--header "X-SFPY-AGGREGATOR-SECRET-KEY: {{secret_key}}" \
--header "Content-Type: application/json" \
--data '{
"merchant_external_id": "sec_0c3de397-441f-471f-bcd7-b6d948e1c307",
"iban": "PK62ABPA0010000222380013",
"name": "Acme Foods Saddar",
"enabled": true,
"raast_merchant_id": "{{raast_merchant_id}}",
"rate_card": {
"ratecard_kind": "RateCardKind_fixed",
"fixed_rate": 3000,
"tax_region": "PK",
"tax_rate": 0.1
}
}'
const baseUrl = '{{base_url}}';
const aggregatorId = '{{aggregator_id}}';
const secretKey = process.env.SAFEPAY_SECRET_KEY ?? '';
const raastMerchantId = '{{raast_merchant_id}}';
const createMerchantResponse = await fetch(
`${baseUrl}/v1/aggregators/${aggregatorId}/merchants`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SFPY-AGGREGATOR-SECRET-KEY': secretKey,
},
body: JSON.stringify({
merchant_external_id: 'sec_0c3de397-441f-471f-bcd7-b6d948e1c307',
iban: 'PK62ABPA0010000222380013',
name: 'Acme Foods Saddar',
enabled: true,
raast_merchant_id: raastMerchantId,
rate_card: {
ratecard_kind: 'RateCardKind_fixed',
fixed_rate: 3000,
tax_region: 'PK',
tax_rate: 0.1,
},
}),
},
);
if (!createMerchantResponse.ok) {
const errorBody = await createMerchantResponse.json();
throw new Error(`Safepay Raast error: ${createMerchantResponse.status} ${errorBody.message}`);
}
type CreateMerchantPayload = {
api_version: string;
data: {
token: string;
merchant_external_id: string;
enabled: boolean;
name: string;
};
};
const merchantPayload: CreateMerchantPayload = await createMerchantResponse.json();
const aggregatorMerchantIdentifier = merchantPayload.data.token;
console.log('Aggregator merchant identifier', aggregatorMerchantIdentifier);
Persist data.token; Safepay uses it as the aggregator_merchant_identifier in subsequent payment, QR, and payout calls.
Make a payment request
Once Safepay enables the merchant, create a Request-to-Pay (RTP) using the same credentials. Reuse {{base_url}} for the target environment. The example below demonstrates an RTP Now payment that immediately prompts the customer.
curl --request POST "{{base_url}}/v1/aggregators/{{aggregator_id}}/payments" \
--header "X-SFPY-AGGREGATOR-SECRET-KEY: {{secret_key}}" \
--header "Content-Type: application/json" \
--data '{
"request_id": "{{request_id}}",
"amount": 5000,
"aggregator_merchant_identifier": "{{aggregator_merchant_identifier}}",
"order_id": "INV-2025-0001",
"type": "RTP_NOW",
"expiry_in_minutes": 30,
"debitor_iban": "PK36SCBL0000001123456702"
}'
const aggregatorMerchantIdentifier = process.env.SAFEPAY_AGGREGATOR_MERCHANT_IDENTIFIER;
if (!aggregatorMerchantIdentifier) {
throw new Error('Missing SAFEPAY_AGGREGATOR_MERCHANT_IDENTIFIER');
}
const baseUrl = '{{base_url}}';
const aggregatorId = '{{aggregator_id}}';
const secretKey = process.env.SAFEPAY_SECRET_KEY ?? '';
const paymentResponse = await fetch(
`${baseUrl}/v1/aggregators/${aggregatorId}/payments`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SFPY-AGGREGATOR-SECRET-KEY': secretKey,
},
body: JSON.stringify({
request_id: crypto.randomUUID(),
amount: 5000,
aggregator_merchant_identifier: aggregatorMerchantIdentifier,
order_id: 'INV-2025-0001',
type: 'RTP_NOW',
expiry_in_minutes: 30,
debitor_iban: 'PK36SCBL0000001123456702',
}),
},
);
if (!paymentResponse.ok) {
const error = await paymentResponse.json();
throw new Error(`Payment failed (${paymentResponse.status}): ${error.message}`);
}
type CreatePaymentPayload = {
api_version: string;
data: {
token: string;
status: 'P_INITIATED' | 'P_RECEIVED' | 'P_FAILED' | 'P_REJECTED' | 'P_SETTLED';
trace_reference?: string;
msg_id?: string;
request_id: string;
order_id: string;
created_at: string;
};
};
const paymentPayload: CreatePaymentPayload = await paymentResponse.json();
console.log('Payment token', paymentPayload.data.token);
Next, subscribe to webhooks to know when the customer approves or rejects the request, and retry idempotently by reusing the same request_id when network errors occur.
See also