Submission of Donation Records (Corppass)
1. Overview and Architecture
The Inland Revenue Authority of Singapore (IRAS) provides application programming interface (API) services to allow application developers and reporting entities (Institutions of a Public Character, or IPCs) to submit and validate tax-deductible donation records directly via secure HTTP JSON requests.
Submissions are validated against strict business rules, donor identification formats, and deductible receipt requirements. Communications are established via server-to-server connections through GovTech’s APEX Cloud platform.
2. Onboarding to APEX and CorpPass
Developers must authenticate to APEX via CorpPass OAuth 2.1 before consuming the Donation-API. All integrations must be tested and verified in the Sandbox environment before requesting production clearance.
-
CorpPass Account Provisioning: Your organization must have an active CorpPass account.
A CorpPass Admin or Sub-Admin must assign access to the
APEX Cloude-Service for non-government users. -
APEX Developer Portal Setup:
- Log in to the APEX Developer Portal and register your Application.
- Generate an
x-apex-apikey. - Create an OAuth 2.1 Client.
- Subscribe your created application to the Submission of Donation Records API.
- Update Notification Contacts: Maintain an updated email address in your APEX profile to receive operational downtime notifications, API deprecations, and schema change advisories.
3. Message Security & Authentication
The API endpoint must be invoked from a server-to-server connection supporting HTTP/2, TLS 1.2, or TLS 1.3. IP Whitelisting is enforced at the gateway layer.
Required HTTP Headers
| Header | Format / Value | Description |
|---|---|---|
x-apex-apikey |
String (UUID) |
API Key generated in APEX and subscribed to the Donation-API. |
Authorization |
Bearer <JWT> |
CorpPass access token. Use OAuth scope DonationSub when requesting the token. |
Content-Type |
application/json |
Request payload format. |
Accept |
application/json |
Accepted response format. |
Transacting Flow: Taxpayer vs. Tax Agent
When invoking the CorpPass OAuth authorization endpoint, you may send the optional tax_agent parameter to indicate whether the transaction is performed directly or by an appointed third party:
tax_agent=n: Entity is transacting for its own business donation records.tax_agent=y: Entity is transacting as an authorized tax agent for its IPC business client.
4. Payload Limits, Chunking & Rate Limits
- Record Limit: Maximum of 5,000 records per request payload.
- Size Limit: Maximum of 2 MB per JSON payload.
- Assembly Rate Limit: 1,000 requests per hour per application. Exceeding this threshold returns HTTP status
429 Too Many Requestswith messageAssembly Rate Limit exceeded.
Payload Chunking Strategy
For submissions exceeding 5,000 records (e.g., 8,000 records), partition data into multiple sequential calls:
- Payload 1: 5,000 donation records (size < 2 MB)
- Payload 2: 3,000 donation records (size < 2 MB)
If Payload 1 passes and Payload 2 fails validation, resolve errors in Payload 2 and resubmit only Payload 2. Do not resend Payload 1 to avoid duplicate processing.
5. Environments & Governance
| Environment | HTTP Verb | Base Endpoint |
|---|---|---|
| Sandbox | POST | https://sandbox.api.gov.sg/iras/donation-records/v1/Submit |
| Production | POST | https://public.api.gov.sg/iras/donation-records/v1/Submit |
6. Interface Rules & Character Whitelist
All data payloads must adhere to these structural constraints:
- Property names use camelCase (e.g.,
orgAndSubmissionInfo,dateOfDonation). - Date strings conform to
ISO-8601or the specifiedYYYYMMDDinteger format. - All string values are restricted to the following approved characters:
- Standard ASCII range: 32 through 127
- Germanic / Umlauts:
ü(Unicode: 252) andÜ(Unicode: 220) - Turkish dotted:
İ(Unicode: 304) - Whitespace & controls: Line Feed (LF, Unicode: 10), Carriage Return (CR, Unicode: 13)
- Typography: En dash (
–), Em dash (—), Smart quotes (“,”,‘,’)
7. Request Data Dictionary
1. Header Object: orgAndSubmissionInfo
| Field | Type | Req | Description & Format Rules |
|---|---|---|---|
validateOnly |
Boolean | M | true = Validation only (no submission); false = Validate and commit to IRAS. |
basisYear |
Number(4) | M | Receipt calendar year (YYYY). Accepts 3 back years, current year, 1 advance year. |
organisationIDType |
String(1) | M | 5: UEN-BUSINESS, 6: UEN-LOCAL CO, U: UEN-OTHERS, A: ASGD, I: ITR |
organisationIDNo |
String(12) | M | Entity registration number formatted according to organization ID type. |
organisationName |
String(60) | M | Registered institution/business name. |
batchIndicator |
String(1) | M | O: Original submission, A: Amendment (overwrites previous submission by donor/receipt). |
authorisedPersonIDNo |
String(12) | M | NRIC (prefix S/T), FIN (prefix F/G/M), ASGD (ANNNNNNNC), or ITR (NNNNNNNNNC). |
authorisedPersonName |
String(30) | M | Name of officer submitting records. |
authorisedPersonDesignation |
String(30) | O | Official title / job designation. |
telephone |
String(20) | M | Minimum 8 digits. |
authorisedPersonEmail |
String(50) | M | Valid email address for submission correspondence. |
numOfRecords |
Number(7) | M | Total count of donor items in donationDonorDtl (max 5,000). |
totalDonationAmount |
Number(12) | M | Sum total of donationAmount values in payload. Integers only. |
2. Item Object: donationDonorDtl[]
| Field | Type | Req | Description & Format Rules |
|---|---|---|---|
recordID |
Number(4) | M | Sequential index starting from 1, unique within payload. |
idType |
String(1) | M | 1: NRIC, 2: FIN, 5: UEN-BUSINESS, 6: UEN-LOCAL CO, A: ASGD, I: ITR, U: UEN-OTHERS |
idNumber |
String(12) | M | Identification matching specified idType. |
individualIndicator |
String(3) | C | Mandatory if idType is A or I. Must be IND (Individual) or NON (Non-individual). |
name |
String(80) | M | Donor's registered name. |
postalCode |
String(6) | O | Numeric 6 digits. Preserve leading zeroes (e.g., "012345"). |
donationAmount |
Number(9) | M | Rounded to nearest dollar (no decimals). Must be > $0 for Original (O) batches: a zero amount voids a donation, which is valid only on an Amendment (A) batch. |
dateOfDonation |
Number(8) | M | Format: YYYYMMDD (e.g., 20250105). |
receiptNum |
String(20) | M | Unique receipt or transaction tracking reference number. |
typeOfDonation |
String(1) | M | O: Outright Cash, S: Share/Unit Trust, A: Artifact, L: Land/Building, P: Public Sculpture, H: OHAS. If S, donor must be Individual. |
namingDonation |
String(1) | M | Z: Non-naming (default), N: Named IPC, F: Named Facility. |
8. Response Schema & Diagnostics
The HTTP status is not the outcome. IRAS answers 200 for a request it processed, and
returnCode in the body says whether the records were accepted: a rejection is still a
200, so reading the status alone reports a rejected batch as a success. Any other status is an
exception error rather than a validation outcome - the request may not have been processed at
all (see 401 unchanged credentials and 429 rate limit above).
| Property | Type | Description |
|---|---|---|
returnCode |
String | "10": Success, "30": Failure / Rejected. Both arrive with HTTP 200. |
data.acknowledgementCode |
String | IRAS system acknowledgment sequence identifier (populated when returnCode = "10"). |
info.messageCode |
String |
Diagnostic code:850300: Request object is null.850301: Arguments error / validation failed.850302: Generic server exception.850303: Service inactive.850304: Unauthorized credentials.850305: Invalid sandbox test user.
|
info.fieldInfoList[].recordID |
String | Affected record identifiers. Identical errors are grouped, e.g. "3,5,7". |
9. OpenAPI 3.0 Specification
This is the same document as docs/iras-donation-records.openapi.json, which the IREMS module
reads at runtime to build and verify its requests. The block below is generated from that file by
npm run spec:embed, and the test suite fails when the two drift apart - so copy it with
confidence for Postman imports, API gateway configuration, or client code generation:
{
"openapi": "3.0.3",
"info": {
"title": "Submission of Donation Records (Donation-API)",
"version": "1.3.0",
"description": "The Inland Revenue Authority of Singapore (IRAS) provides the Submission of Donation Records API (Donation-API) so Institutions of a Public Character (IPCs), Qualifying Grantmakers and government agencies can validate and submit tax-deductible donation information to IRAS directly. Submitted records are auto-included in the donors' tax returns.\n\nIntegration is through APEX (api.gov.sg): the caller signs in with CorpPass using APEX OAuth 2.1, then calls this API with that access token plus the APEX API key. See the security schemes below and the onboarding guide at https://go.gov.sg/apexonboarding-oauth.\n\nThe field-level reference is the API specification PDF: https://file.go.gov.sg/iras-donations-apispecs.pdf. This document is the machine-readable contract, and it is the one the IREMS IRAS module reads at runtime to build and verify its submission requests."
},
"servers": [
{
"url": "https://sandbox.api.gov.sg/iras/donation-records/v1",
"description": "Sandbox environment. Test entities only."
},
{
"url": "https://public.api.gov.sg/iras/donation-records/v1",
"description": "Production gateway. Mock submissions or test cycles here breach the IRAS API Terms of Use."
}
],
"security": [
{
"APEXOAuth2": ["DonationSub"],
"ApexApiKey": []
}
],
"paths": {
"/Submit": {
"post": {
"summary": "Submit or validate donation records",
"operationId": "Submit",
"description": "Submits one payload of donation records, or validates one when validateOnly is true.\n\nThe HTTP status only distinguishes transport outcomes: 200 means IRAS processed the request, and the body field returnCode says whether the records were accepted (10) or rejected (30). Any other status is an exception error rather than a validation outcome.\n\nLimits are declared in x-iras-limits: at most 5000 records and 2 MB per request. Payloads larger than that must be split into sequential requests; if one chunk fails, resolve it and resubmit that chunk only.",
"security": [
{
"APEXOAuth2": ["DonationSub"],
"ApexApiKey": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DonationSubmissionPayload"
}
}
}
},
"responses": {
"200": {
"description": "IRAS processed the request. Read returnCode in the body: 10 is success, 30 is failure or rejection.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DonationResponse"
}
}
}
},
"401": {
"description": "Unauthorized credentials. The body carries info.messageCode 850304. A non-200 status is an exception error, not a validation rejection."
},
"429": {
"description": "Assembly Rate Limit exceeded. At most 1000 requests per hour per application."
},
"default": {
"description": "Any other status is an exception error. Read the body for the upstream message; the payload may not have been processed at all."
}
}
}
}
},
"components": {
"securitySchemes": {
"APEXOAuth2": {
"type": "oauth2",
"description": "APEX CorpPass OAuth 2.1. The caller obtains an access token with the authorization code flow and PKCE (S256), requesting the DonationSub scope, and presents it as a bearer token on every call.\n\nClient authentication is a signed private_key_jwt client assertion in the token request body: iss and sub are the APEX client id, aud is the token endpoint URL being called, and exp is iat plus 300 seconds. There is no client secret.\n\ntax_agent is an optional parameter on the authorization request: y when an appointed tax agent transacts for an IPC business client, n (or omitted) when the entity transacts for its own records.\n\nThe endpoints below are the sandbox pair. Production uses https://public.api.gov.sg/oauth/cp/v3/authorize and https://public.api.gov.sg/oauth/cp/v3/token.",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://sandbox.api.gov.sg/oauth/cp/v3/authorize",
"tokenUrl": "https://sandbox.api.gov.sg/oauth/cp/v3/token",
"scopes": {
"DonationSub": "Submit donation records to IRAS"
}
}
},
"x-axway": {
"accessTokenLocation": "HEADER",
"authorizationHeaderPrefix": "Bearer",
"scopesMatching": "Any",
"typeDisplayName": "OAuth 2.0"
}
},
"ApexApiKey": {
"type": "apiKey",
"in": "header",
"name": "x-apex-apikey",
"description": "API key generated in APEX and subscribed to the Donation-API. Required on every call, alongside the bearer token. Treated as a credential: it belongs in configuration, never in source."
}
},
"schemas": {
"DonationSubmissionPayload": {
"type": "object",
"required": ["orgAndSubmissionInfo", "donationDonorDtl"],
"properties": {
"orgAndSubmissionInfo": {
"type": "object",
"required": ["validateOnly", "basisYear", "organisationIDType", "organisationIDNo", "organisationName", "batchIndicator", "authorisedPersonIDNo", "authorisedPersonName", "telephone", "authorisedPersonEmail", "numOfRecords", "totalDonationAmount"],
"properties": {
"validateOnly": {
"type": "boolean",
"description": "true validates the payload without filing it."
},
"basisYear": {
"type": "integer",
"example": 2025,
"description": "Basis year of the donation records."
},
"organisationIDType": {
"type": "string",
"enum": ["5", "6", "U", "A", "I"],
"x-enum-descriptions": {
"5": "UEN-BUSINESS",
"6": "UEN-LOCAL CO",
"U": "UEN-OTHERS",
"A": "ASGD",
"I": "ITR"
},
"description": "How the organisation's registration number is issued: 5 UEN-BUSINESS, 6 UEN-LOCAL CO, U UEN-OTHERS, A ASGD, I ITR."
},
"organisationIDNo": {
"type": "string",
"maxLength": 12,
"example": "180011032C"
},
"organisationName": {
"type": "string",
"maxLength": 60,
"example": "ABCD Medical Institute"
},
"batchIndicator": {
"type": "string",
"enum": ["O", "A"],
"x-enum-descriptions": {
"O": "original submission",
"A": "amendment"
},
"description": "O is an original submission, A is an amendment. An original submission requires every donationAmount to be greater than zero; a zero amount only voids a donation, which is valid on an amendment batch."
},
"authorisedPersonIDNo": {
"type": "string",
"maxLength": 12,
"example": "S9991257Z"
},
"authorisedPersonName": {
"type": "string",
"maxLength": 30,
"example": "James Tan"
},
"authorisedPersonDesignation": {
"type": "string",
"maxLength": 30,
"example": "Manager"
},
"telephone": {
"type": "string",
"minLength": 8,
"maxLength": 20,
"example": "61234567"
},
"authorisedPersonEmail": {
"type": "string",
"format": "email",
"maxLength": 50,
"example": "[email protected]"
},
"numOfRecords": {
"type": "integer",
"maximum": 5000,
"example": 1,
"description": "Must equal the number of entries in donationDonorDtl."
},
"totalDonationAmount": {
"type": "integer",
"example": 2500,
"description": "Sum of the donationAmount values in this payload, rounded to the nearest dollar."
}
}
},
"donationDonorDtl": {
"type": "array",
"maxItems": 5000,
"items": {
"type": "object",
"required": ["recordID", "idType", "idNumber", "name", "donationAmount", "dateOfDonation", "receiptNum", "typeOfDonation", "namingDonation"],
"properties": {
"recordID": {
"type": "integer",
"example": 1
},
"idType": {
"type": "string",
"enum": ["1", "2", "5", "6", "A", "I", "U"],
"x-enum-descriptions": {
"1": "NRIC",
"2": "FIN",
"5": "UEN-BUSINESS",
"6": "UEN-LOCAL CO",
"A": "ASGD",
"I": "ITR",
"U": "UEN-OTHERS"
},
"description": "How the donor's identification number is issued: 1 NRIC, 2 FIN, 5 UEN-BUSINESS, 6 UEN-LOCAL CO, A ASGD, I ITR, U UEN-OTHERS."
},
"idNumber": {
"type": "string",
"maxLength": 12,
"example": "S1234567Z"
},
"individualIndicator": {
"type": "string",
"enum": ["IND", "NON", ""],
"x-enum-descriptions": {
"IND": "Individual",
"NON": "Non-individual"
},
"description": "Mandatory when idType is A or I. IND is an individual, NON is a non-individual."
},
"name": {
"type": "string",
"maxLength": 80,
"example": "James Zhang"
},
"addressLine1": {
"type": "string",
"maxLength": 30
},
"addressLine2": {
"type": "string",
"maxLength": 30
},
"postalCode": {
"type": "string",
"maxLength": 6,
"example": "640501"
},
"donationAmount": {
"type": "integer",
"example": 2500,
"description": "Rounded to the nearest dollar, no decimals. Greater than zero on an original (O) batch; zero only voids a donation and is valid on an amendment (A) batch."
},
"dateOfDonation": {
"type": "integer",
"example": 20250105,
"description": "Format YYYYMMDD."
},
"receiptNum": {
"type": "string",
"maxLength": 20,
"example": "RECP123",
"description": "Unique receipt or transaction tracking reference."
},
"typeOfDonation": {
"type": "string",
"enum": ["O", "S", "A", "L", "P", "H"],
"x-enum-descriptions": {
"O": "outright cash",
"S": "share or unit trust",
"A": "artifact",
"L": "land or building",
"P": "public sculpture",
"H": "OHAS"
},
"description": "O is outright cash, S is share or unit trust, A is artifact, L is land or building, P is public sculpture, H is OHAS. A share or unit trust donation requires an individual donor."
},
"namingDonation": {
"type": "string",
"enum": ["Z", "N", "F"],
"x-enum-descriptions": {
"Z": "non-naming (default)",
"N": "named IPC",
"F": "named facility"
},
"description": "Z is non-naming (default), N is a named IPC, F is a named facility."
}
}
}
}
},
"DonationResponse": {
"type": "object",
"description": "The body IRAS returns with HTTP 200. returnCode is the outcome; everything else is diagnostic.",
"properties": {
"returnCode": {
"type": "string",
"enum": ["10", "30"],
"description": "10 means the records were accepted, 30 means they were rejected. A rejection still arrives with HTTP 200, so this field - not the status alone - decides success."
},
"data": {
"type": "object",
"properties": {
"acknowledgementCode": {
"type": "string",
"example": "1510010493",
"description": "IRAS acknowledgement sequence identifier, populated when returnCode is 10."
}
}
},
"info": {
"type": "object",
"properties": {
"messageCode": {
"type": "string",
"example": "850301",
"description": "850300 request object is null, 850301 arguments error or validation failed, 850302 generic server exception, 850303 service inactive, 850304 unauthorized credentials, 850305 invalid sandbox test user."
},
"message": {
"type": "string",
"example": "Arguments Error"
},
"warnings": {
"type": "array",
"items": {
"type": "object"
}
},
"fieldInfoList": {
"type": "array",
"description": "Validation failures, grouped by message. One entry can name several records.",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"example": "idNumber"
},
"message": {
"type": "string",
"example": "Incorrect ID Type and/or ID No"
},
"recordID": {
"type": "string",
"example": "3,5,7",
"description": "Comma-separated record ids the message applies to."
}
}
}
}
}
}
}
}
}
}
},
"x-iras-limits": {
"maxRecordsPerRequest": 5000,
"maxPayloadBytes": 2097152,
"maxRequestsPerHour": 1000,
"note": "Split a larger submission into sequential payloads of at most 5000 records, each under 2 MB. If one payload fails validation, resolve it and resubmit that payload only - do not resend the earlier ones, to avoid duplicate processing."
},
"x-provenance": {
"mergedOn": "2026-09-26",
"mergedFrom": [
"docs/Submission of Donation Records (Sandbox) v1.1.json - Swagger 2.0, gateway-generated: sandbox host only, OAuth 2.0 schemes, no request or response schemas",
"docs/iras_donation_api_documentation.html - OpenAPI 3.0.3 version 1.3.0: CorpPass onboarding, full request and response schemas, operational limits"
],
"supersedes": "1.1 (Swagger 2.0)",
"changes": [
"Adopted OpenAPI 3.0.3 and version 1.3.0 from the newer document.",
"Servers now cover both sandbox and production; the 1.1 document described the sandbox host only.",
"Authentication modelled as APEX CorpPass OAuth 2.1 (authorization code, PKCE S256, private_key_jwt, scope DonationSub) with the APEX API key as a second required scheme - replacing the 1.1 client_secret metadata.",
"Added the request and response schemas, which the 1.1 document did not define.",
"Response outcome is returnCode (10 success, 30 rejected) with HTTP 200; a non-200 status is an exception error.",
"typeOfDonation is the letter O for outright cash, not the digit 0.",
"Documented 401 and 429 responses, the payload limits, and the optional tax_agent authorization parameter."
],
"note": "The OpenAPI block embedded in docs/iras_donation_api_documentation.html is asserted against this file byte for byte by scripts/check.js, so the two cannot drift."
},
"x-axway": {
"serviceType": "rest",
"basePaths": [
"https://sandbox.api.gov.sg",
"https://public.api.gov.sg"
],
"expired": false,
"customProperties": {
"upgradeFrom": "371f8bca-1ab7-485c-b400-5c5dee427bcb",
"upgradeFromCompletedOn": "1770036810672",
"apiType": "sandbox+production"
},
"retirementDate": 0,
"corsEnabled": true,
"deprecated": false,
"availableSDK": {
"ios-swift": "/discovery/sdk/f4540e83-b3f5-47aa-8af3-452eb4e9d2a1/ios-swift",
"android": "/discovery/sdk/f4540e83-b3f5-47aa-8af3-452eb4e9d2a1/android",
"nodejs": "/discovery/sdk/f4540e83-b3f5-47aa-8af3-452eb4e9d2a1/nodejs"
},
"id": "f4540e83-b3f5-47aa-8af3-452eb4e9d2a1",
"state": "published",
"accessGrantedDate": 1783071053749
}
}
10. Payload Examples
Sample Request
{
"orgAndSubmissionInfo": {
"validateOnly": false,
"basisYear": 2025,
"organisationIDType": "6",
"organisationIDNo": "180011032C",
"organisationName": "ABCD Medical Institute",
"batchIndicator": "O",
"authorisedPersonIDNo": "S9991257Z",
"authorisedPersonName": "James Tan",
"authorisedPersonDesignation": "Manager",
"telephone": "61234567",
"authorisedPersonEmail": "[email protected]",
"numOfRecords": 2,
"totalDonationAmount": 5000
},
"donationDonorDtl": [
{
"recordID": 1,
"idType": "1",
"idNumber": "S1234567Z",
"individualIndicator": "",
"name": "James Zhang",
"addressLine1": "123 Orchard Road",
"addressLine2": "#10-01",
"postalCode": "640501",
"donationAmount": 2500,
"dateOfDonation": 20250105,
"receiptNum": "RECP123",
"typeOfDonation": "O",
"namingDonation": "Z"
},
{
"recordID": 2,
"idType": "A",
"idNumber": "A1234567C",
"individualIndicator": "IND",
"name": "Andrew Chan",
"addressLine1": "",
"addressLine2": "",
"postalCode": "640501",
"donationAmount": 2500,
"dateOfDonation": 20250505,
"receiptNum": "RECP128",
"typeOfDonation": "O",
"namingDonation": "Z"
}
]
}
Sample Error Response (Lumped Field Validation Errors)
{
"returnCode": "30",
"info": {
"messageCode": "850301",
"message": "Arguments Error",
"fieldInfoList": [
{
"field": "idNumber",
"message": "Incorrect ID Type and/or ID No",
"recordID": "3,5,7"
},
{
"field": "name",
"message": "Must not be blank",
"recordID": "3,5,7"
}
]
}
}