UU PDP Compliance Checklist for AI Features in Indonesia

Photo by Leonid Dzhepko via Wikimedia Commons (CC BY 3.0)
Yes. Article 16 paragraph 1 of Law 27 of 2022 defines processing to include analysis, transfer, dissemination and disclosure, and a prompt containing a customer record is all three at once. Sending it to a provider outside Indonesia is also a cross-border transfer under Article 56, so it needs both a lawful basis and a transfer route you can describe.
Treat it as a separate question, because it is a separate purpose. Article 28 requires processing to be carried out in accordance with the purpose it was collected for, and improving a shared model is not the same purpose as handling one customer's ticket. In most designs that second purpose has to rest on explicit consent, and Article 21 sets out what the consent notice must tell the data subject.
Not on its own, because storage region and processing region are different settings. OpenAI's data residency table lists Singapore with regional storage supported and regional processing not supported, and on Azure a Global deployment type may be processed in any geography where the model runs. Read the deployment type first, then decide which tier of Article 56 you are actually relying on.
Write one erasure function that knows every store the data reached: your tables, your prompt and response logs, the vector collection, and any files or stored objects you created at the provider. Deleting the source row does not remove a derived embedding, and the payload beside a vector usually still holds the original text. Provider-side abuse-monitoring copies and the weights of a fine-tuned model are outside that function, which is why you record them rather than pretend they were reached.
Very often yes. Article 34 requires an assessment where processing carries high potential risk and lists triggers including automated decision-making with significant effect, large-scale processing, systematic evaluation or scoring, combining datasets, and the use of new technology. A feature that sends customer records to a model usually matches at least one of those, so plan the assessment as part of shipping rather than as a response to a complaint.

Photo by Leonid Dzhepko via Wikimedia Commons (CC BY 3.0)
Key Takeaway
Under Indonesia's UU PDP, Law 27 of 2022, sending a customer record to a model provider is processing and often a cross-border transfer, so every AI feature needs its own lawful basis, a documented transfer route under Article 56, redaction at the prompt boundary, and a deletion path that reaches provider logs and vector embeddings.
The feature was three lines. Pull the body of a support ticket out of the tickets table, ask a model to summarise it, write the summary back beside the ticket. It passed code review in a morning because there was nothing to review: no new table, no new endpoint, no new dependency. The question that stopped it came from someone who had not read the diff at all. Which country does that complaint text end up in, and who keeps a copy of it?
I am an engineer, not a lawyer, and none of this is legal advice - the decisions that carry risk belong with counsel. What follows is the checklist I now run before an AI feature touches Indonesian personal data, written against the text of Law 27 of 2022 and against the retention documentation the providers actually publish. It assumes you already know the shape of UU PDP. The general developer guide on this site covers consent management, encryption and breach notification, and I am not going to re-teach them; this post is only about the surface that AI features add.
Article 16 paragraph 1 defines processing as a list, and the list is longer than most engineers assume. It covers acquisition and collection, processing and analysis, storage, correction and updating, display, publication, transfer, dissemination or disclosure, and deletion or destruction. Two entries on that list are the reason an AI feature is not an ordinary CRUD feature.
That is the whole difference. A database compliance checklist has rows for tables, backups, encryption at rest and access logs. It has no row for an outbound HTTPS request carrying a customer's own words to a company in another jurisdiction, because when those checklists were written, ordinary line-of-business services did not routinely make one. Now they make one per ticket.
Article 20 paragraph 1 says the controller must have a basis for processing, and paragraph 2 lists six: explicit consent for one or more specified purposes, performance of a contract or a request made before entering one, a legal obligation, protection of vital interests, a public task, and other legitimate interests weighed against the data subject's rights. You pick one per purpose, not one per system.
Running inference on a support ticket so the agent handling it gets a summary is plausibly the same purpose as handling the ticket, so the contract or legitimate-interest basis you already rely on may stretch over it. Using that same ticket as training data for a model you fine-tune is a different purpose entirely - the customer's words stop being an input to their own case and become an asset that improves your product for everybody else. Article 28 requires processing to be carried out in accordance with its purpose, so the second use needs its own answer, and in most designs that answer has to be consent. Article 21 then tells you what the consent notice must contain: the legality of the processing, its purpose, the type and relevance of the data, the retention period of the documents holding it, the processing period, and the data subject's rights.
The practical consequence is that we already have consent is almost never a complete answer for a feature that trains. Write the two questions down separately. And notice, when the second one turns out to be hard, that you are allowed to answer it with no - training on customer text is optional in a way that inference usually is not, and declining it removes an obligation rather than deferring one.
Article 56 sets a tiered test for sending personal data outside Indonesian jurisdiction, and the tiers are ordered: you only reach the second because the first failed, and the third because both failed.
| Tier | What Article 56 requires | What it means for an AI feature today |
|---|---|---|
| Paragraph 2 | The recipient's country of domicile has a level of personal data protection equal to or higher than this law | No adequacy list has been published, so there is nothing you can simply point at |
| Paragraph 3 | Failing that, adequate and binding personal data protection must exist | Contractual. A data processing agreement with real transfer terms, which is where nearly every model provider ends up |
| Paragraph 4 | Failing both, the data subject's consent | A consent dialog on every ticket summary is not a design anybody ships twice |
Paragraph 5 leaves the detail to a Government Regulation. The Constitutional Court reviewed the framework in Decision 137 PUU-XXIII 2025, handed down in January 2026, and upheld it, confirming that adequacy assessment sits with the executive rather than with parliament. That settles who decides. It does not produce the list, and until the list exists the honest position for most Indonesian teams is that they are relying on paragraph 3 and had better be able to describe the safeguard out loud.
Describing it means naming where inference happens, and this is where engineers get caught, because the vendor documentation separates two things that sound like one. OpenAI's data residency controls list Singapore as sg.api.openai.com with regional storage supported and regional processing not supported - only the United States and Europe currently support both, and Indonesia is not on the list at all. Azure draws the same line from the other side: prompts and responses are processed within the customer-specified geography unless the deployment type is Global or DataZone, and a Global deployment may be processed in any geography where that model is deployed. Picking the nearest storage region and assuming the tokens stayed there is the mistake.
Read your deployment type before you read your data processing agreement. A Global deployment on Azure, or a request sent without a regional domain prefix on OpenAI, means you cannot answer the question Article 56 paragraph 2 asks, because you do not know which country ran the inference. The configuration takes an afternoon to change. The disclosure cannot be taken back.

Article 27 requires processing to be limited and specific, lawful and transparent. At a prompt boundary that stops being an abstraction and becomes a serialisation choice. The convenient thing is to stringify the whole record, because it is one expression and models do read better with context. The compliant thing is a field allowlist, because it is the only version you can describe honestly in a processing record.
// Wrong: the whole row leaves, including columns nobody asked for.
const prompt = "Summarise this complaint:\n" + JSON.stringify(ticket);
// ticket also carries nik, phone, address, internal notes and an audit trail.
// Right: a field allowlist you can quote in the processing record, plus
// redaction for the free text you genuinely cannot drop.
const AI_SUMMARY_FIELDS = ["subject", "body", "productCode", "createdAt"] as const;
const REDACTIONS: Array<[RegExp, string]> = [
[/\b\d{16}\b/g, "[NIK]"], // 16-digit Indonesian national ID
[/\b(?:\+62|0)8\d{8,11}\b/g, "[PHONE]"], // Indonesian mobile numbers
[/\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g, "[EMAIL]"],
];
const redact = (text: string) =>
REDACTIONS.reduce((s, [re, tag]) => s.replace(re, tag), text);
// The allowlist is the contract. Adding a field is a change to the
// processing record, not a one-line commit.
export function summaryPayload(ticket: Ticket) {
return Object.fromEntries(
AI_SUMMARY_FIELDS.map((f) => {
const v = ticket[f];
return [f, typeof v === "string" ? redact(v) : v];
}),
);
}Two things worth knowing before you rely on this. Redaction is not a substitute for the allowlist - a regular expression that misses an unusual phone format still ships that phone number, whereas a field that was never selected cannot leak at all. And the allowlist is what makes the Article 31 record writable: you can state exactly which categories of personal data leave the system for this feature, which is a sentence nobody can write when the payload is whatever the ORM happened to return.
Put the allowlist in the same module as the provider client, and export nothing else from it. If the only route to the model is a function that accepts an already-narrowed payload, the next developer cannot widen the disclosure by accident, and the diff that widens it deliberately is the one a reviewer will actually see.
Article 43 paragraph 1 requires deletion when data is no longer needed for the purpose, when consent is withdrawn, when the data subject asks, or when it was obtained unlawfully. Article 44 covers destruction, and its official explanation defines destroying as removing, eliminating or wiping personal data so that it can no longer be used to identify the data subject. That is a definition written about an outcome, not about a DELETE statement, and it is the sentence that makes an AI feature's storage awkward. Three stores are usually forgotten.
The awkward part is not that these stores exist. It is that each one is created by a different part of the code, at a different time, often by a different person, while the erasure routine is written once by whoever built the first of them and rarely revisited.
Take the rights first, because the deadlines are short. Article 7 gives the data subject access and a copy of their data, and Article 32 paragraph 2 requires that access to be provided within 3 x 24 hours of the request. Article 30 paragraph 1 gives the same 3 x 24 hours for correcting inaccurate data. Article 8 gives the right to end processing, delete and destroy. Article 10 paragraph 1 gives the right to object to a decision based solely on automated processing, including profiling, that has legal effect or a significant impact - a right that exists precisely because of features like the one you are shipping.
// One function knows every store that ever saw this subject's data.
// The list grows; the function is the only place that has to know.
async function eraseSubject(subjectId: string) {
await db.transaction(async (tx) => {
await tx.delete(tickets).where(eq(tickets.customerId, subjectId));
await tx.delete(aiPromptLog).where(eq(aiPromptLog.subjectId, subjectId));
await tx.delete(aiResponseLog).where(eq(aiResponseLog.subjectId, subjectId));
});
// Embeddings are derived personal data. Deleting the source row does not
// touch them, and the payload stored beside the vector still holds the text.
await qdrant.delete("tickets", {
filter: { must: [{ key: "subjectId", match: { value: subjectId } }] },
});
// Provider-side objects you created yourself: files, vector stores,
// stored responses. These are yours to delete and nobody else's job.
for (const fileId of await listProviderFiles(subjectId)) {
await openai.files.del(fileId);
}
// What this function CANNOT reach. Record it, because Article 45 makes you
// tell the data subject that deletion happened, and this is the footnote.
await eraseAudit.record(subjectId, [
"provider abuse-monitoring copies",
"weights of any model fine-tuned on this text",
]);
}Article 45 then requires you to notify the data subject that the deletion or destruction has happened, which turns out to be a useful forcing function. You cannot send that notice honestly while you know a fine-tuned model still carries their text somewhere in its weights.
Access under Article 7 has the same shape and is worth thinking about separately. You can answer an access request for your database, your logs and your vector store, because all three are queryable by subject. You cannot answer it for a set of weights - a fine-tuned model has no index, no row to return and no reliable way to say whether a given person's text influenced it. That asymmetry is the strongest practical argument against training on personal data at all: it converts a two-hour engineering task into an unanswerable one, permanently.

Article 31 is one sentence long: the controller must keep a record of all personal data processing activities. All of them. Article 47 requires the controller to be responsible for processing and to be able to demonstrate that responsibility, which is the accountability principle with teeth. Article 34 adds a personal data protection impact assessment wherever processing carries high potential risk, and its list of triggers reads like a description of an AI feature - automated decision-making with legal or significant effect, processing at large scale, systematic evaluation, scoring or monitoring, combining or matching datasets, and the use of new technology.
# processing-records/ai-ticket-summary.yaml
# One file per AI feature, in the repo, reviewed in the same pull request
# that changes the prompt.
feature: ai-ticket-summary
purpose: Summarise an inbound support ticket for the agent handling it
lawful_basis: contract # Art 20(2)(b) - handling the customer's own ticket
training_on_this_data: false # a separate purpose, deliberately not taken
data_sent:
fields: [subject, body, productCode, createdAt]
redacted_in_body: [nik, phone, email]
never_sent: [name, address, nik, payment_history]
provider:
endpoint: TODO
storage_region: TODO
inference_region_guaranteed: false # storage region is NOT processing region
transfer_basis: Art 56(3) binding safeguards, DPA signed TODO
provider_retention: abuse-monitoring logs, 30 days by default
our_stores:
prompt_log: 30 days, erased by eraseSubject
response_log: 30 days, erased by eraseSubject
embeddings: qdrant collection tickets, erased by eraseSubject
unreachable_by_erasure:
- provider abuse-monitoring copies
dpia_required: true # Art 34(2)(f) - use of new technologyKeeping that record in the repository rather than in a spreadsheet is the only version that stays true. The prompt changes in a pull request, the record changes in the same pull request, and the reviewer who notices a new field in the allowlist is the same reviewer who notices the record was not updated to match. When somebody eventually asks which personal data this feature sends abroad and on what basis, the answer is a file with a git history behind it rather than a meeting.
The rule I carry now is short enough to say in a review: treat the prompt as the system boundary. Everything crossing it is a disclosure you are choosing to make, to a party you have to name, in a country you have to identify, into logs you mostly cannot reach afterwards. Build the feature that way and the UU PDP questions stop being a gate at the end and become two small files you were going to write anyway - the allowlist and the record.
Sources