Guide · AI and GenAI
EU AI Act Article 50 Deadline Explained: Mark AI Output by 2 December 2026
21 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- EU AI Act Article 50 makes providers of generative AI systems mark each audio, image, video or text output in a machine-readable format. That means a tag software can detect.
- A provider is the business that builds an AI system, or has one developed for it, and offers it under its own name. Article 50 has applied since 2 August 2026.
- Generative systems already on the EU market before 2 August 2026 get until 2 December 2026 for this marking duty only. A July 2026 amendment to the Act added that date.
- The law does not pick one technique. Its explanatory notes list a toolbox: hidden watermarks, metadata inside the file, signed records of origin, logs and fingerprints. A voluntary Commission Code of Practice (10 June 2026) gives practical steps.
- Beginners can stop after the section on Indian SaaS teams. Engineers then get a tested c2pa-python 0.38.1 demo: sign an image, check who signed it, catch tampering, and find the everyday step that silently deletes the mark.
What goes wrong when your AI feature ships without a machine-readable mark?
By 2 December 2026 at the latest, most generative features sold into the EU must tag their output so software can tell AI made it. Under EU AI Act Article 50, an unmarked output is a compliance gap, even if your interface already says "AI-generated". Fines can reach EUR 15 million or 3 percent of worldwide turnover, with lower caps for smaller firms.
A visible label is for people. Article 50 also wants a mark for machines: something a platform, a newsroom tool or a regulator's checker can read without guessing. In practice, it can be a small signed record inside the image file that says generative AI created it.
Picture a common setup. A SaaS team in Pune sells a marketing tool to small shops in Germany and Spain, and its image generator has been live since March 2026. The team is made up, but the deadline is real.
The team's images leave the server as plain JPEG files. Nothing inside them says how they were made. This guide follows that one image feature from the legal text to a signed, checkable file.
Which words do you need before reading EU AI Act Article 50?
You need five words: provider, deployer, placing on the market, metadata and watermark. Each gets a plain meaning and one example from the image feature.
- Provider: the person or company that develops an AI system, or has one developed, and puts it on the market under its own name, paid or free. Example: the Pune team, for its image generator.
- Deployer: a business that uses an AI system under its own authority. Example: a shop in Madrid that uses the tool to make banners.
- Placing on the market: the first time an AI system is made available in the EU. Example: March 2026, when the first EU customer could use the feature.
- Metadata: extra information stored inside a file next to the picture, like the date and camera details in a phone photo.
- Watermark: a signal hidden in the content itself, such as tiny pixel changes a detector can find but your eyes cannot.
What does EU AI Act Article 50 require?
EU AI Act Article 50 sets transparency duties for four kinds of AI use, split between providers and deployers. For generated content, providers mark outputs for machines and deployers label certain outputs for people.
Paragraph 2 is the one with the December deadline. It says outputs must be "marked in a machine-readable format and detectable as artificially generated or manipulated". The technical solutions must be effective, interoperable, robust and reliable, as far as technically feasible.
The words "as far as technically feasible" are weighed against the type of content, the cost of implementation and the generally acknowledged state of the art. So the law sets the outcome and leaves the exact technique open.
| Paragraph | Who | Duty in plain words | Applies from |
|---|---|---|---|
| 50(1) | Provider | Tell people they are talking to an AI system, unless that is obvious | 2 August 2026 |
| 50(2) | Provider | Mark generated audio, image, video or text in a machine-readable, detectable way | 2 August 2026, or 2 December 2026 for systems on the market before 2 August 2026 |
| 50(3) | Deployer | Inform people exposed to emotion recognition or biometric categorisation | 2 August 2026 |
| 50(4) | Deployer | Disclose deepfakes, and AI text published to inform the public on matters of public interest | 2 August 2026 |
| 50(5) | Both | Give the information clearly, at the latest at first interaction or exposure, and accessibly | 2 August 2026 |
Paragraph 2 has three carve-outs. It does not apply where the system performs an assistive function for standard editing. It also does not apply where the system does not substantially alter the input or its meaning, or where law authorises it to detect, prevent, investigate or prosecute crime. The Pune generator makes new banners from a text prompt, so none of these looks like a fit; confirm with counsel.
What changed with the 2 December 2026 deadline?
The deadline gives older generative systems four extra months, for machine-readable marking only. The Pune team launched in March 2026, so 2 December 2026 is its date.
The date comes from a new Article 111(4), added by Regulation (EU) 2026/1744, the Digital Omnibus on AI, in force since 27 July 2026. It covers systems placed on the market before 2 August 2026. Recital 38, part of the explanatory text placed before the articles, gives the reason: providers already on the market need time to adapt.
Read the scope closely. The extra time covers paragraph 2 only. Chatbot disclosure, deepfake labels and the other paragraphs have applied since 2 August 2026 for everyone.
A system first placed on the market on or after 2 August 2026 gets no extra time. It had to mark output from launch. If you rebuilt or relaunched a feature after 2 August, ask counsel which date applies.
Which Article 50(2) date applies to your feature?
Drag to exploreFollow your feature from the top. Notice that only one path reaches 2 December 2026; every other in-scope path is already live.
Diagram connections:
- Generates content? to Only a carve-out use? (Yes)
- Generates content? to Paragraph 2 not engaged (No)
- Only a carve-out use? to Paragraph 2 not engaged (Yes)
- Only a carve-out use? to On EU market before Aug 2? (No)
- On EU market before Aug 2? to Mark by 2 Dec 2026 (Yes)
- On EU market before Aug 2? to Mark from launch (No)
- Generates content?: Synthetic audio, image, video or text
- Only a carve-out use?: Standard editing aid, or no substantial change
- On EU market before Aug 2?: Placed on the market before 2 August 2026
- Mark by 2 Dec 2026: Article 111(4) transition, paragraph 2 only
- Mark from launch: Article 50(2) applies from 2 August 2026
- Paragraph 2 not engaged: Other Article 50 duties may still apply
The same date also starts two new Article 5 bans, on AI systems for non-consensual sexual deepfakes and child sexual abuse material. That is a separate rule from marking. The next question is whether an EU law reaches a team in India at all.
Does Article 50 apply to an Indian SaaS team selling into the EU?
Very likely, if you put a generative feature on the EU market under your own name. Article 2(1)(a) covers providers placing AI systems on the EU market, wherever they are established.
Article 2(1)(c) also covers providers in other countries where the output of their AI system is used in the EU. Being based in Pune or Bengaluru does not take you out of scope.
Article 3(3) covers a business that develops an AI system or has one developed, and places it on the market "under its own name or trademark". If you call another company's model over an API but sell the feature as yours, you may well be the provider.
Fines for breaking Article 50 come under Article 99(4). The ceiling is EUR 15 million or 3 percent of worldwide annual turnover, whichever is higher. For SMEs (small and medium-sized enterprises), start-ups and small mid-cap companies, it is whichever is lower. This guide is engineering help, not legal advice, so confirm your role with counsel.
Which marking techniques does the law accept, and where does the Code fit?
The law accepts watermarks, metadata, cryptographic provenance, logging and fingerprints, and leaves the choice to you. Recital 133 of the AI Act lists watermarks, metadata identifications, cryptographic methods for proving provenance and authenticity, logging methods and fingerprints.
It also says techniques can be combined, and can work at the level of the AI system or of the model. Like recital 38, it is explanatory text that guides how the article is read.
| Technique | What it is | Where it shows up in this guide |
|---|---|---|
| Metadata identification | Information stored inside the file that says how it was made | The C2PA manifest we sign and read |
| Cryptographic provenance | A digital signature that proves who made the record and that nobody changed it | The signature checked as Valid or Trusted |
| Watermark | A hidden signal woven into the pixels, audio or words | The second layer that can outlast a re-save |
| Fingerprint | A stored, approximate identifier of the content, matched later | A way to find a stripped file's record again |
| Logging | The provider's own record of what it generated | A lookup you run on your side |
The Code of Practice is the Commission's voluntary rulebook for this. Its official FAQ says Section 1 sets out commitments on marking and detecting AI-generated audio, image, video and text content.
That FAQ also asks for machine-readable solutions that are "effective, interoperable, robust and reliable, as far as technically feasible". For signatories, the Commission says enforcement will focus on monitoring adherence to the Code.
Teams that do not sign must show compliance through other adequate means. Article 50(7), as amended, lets the Commission judge whether codes are adequate and adopt common rules if they are not.
None of the Commission pages we opened names C2PA, and Article 50 does not either. C2PA is one open standard for signed provenance metadata, and it is what the rest of this guide uses.
How text and image watermarks work inside is a separate topic with its own maths. Here we test the metadata layer end to end, and show why it needs a partner.
How does a C2PA manifest carry a machine-readable mark?
C2PA, the Coalition for Content Provenance and Authenticity, publishes an open standard for this. A C2PA manifest is a signed record, usually stored inside the file, that states what created it. For an AI image, it can say the content was created with a digital source type of trainedAlgorithmicMedia.
That value comes from the IPTC digital source type list, where it means "Created using Generative AI". If your feature edits an upload instead, the list has compositeWithTrainedAlgorithmicMedia, "Edited using Generative AI".
The C2PA 2.2 specification calls the signed part a claim. The claim lists assertions, which are the individual statements. One of them is a hard binding: cryptographic hashes of the file's bytes.
Think of a courier parcel with a sealed, signed slip taped to it. The slip lists who packed it and what went in. The seal covers both the slip and an exact checksum of the contents.
If someone swaps the contents, the checksum no longer matches. If someone forges the slip, the signature fails. Where the picture breaks: anyone can peel the whole slip off, and the parcel still looks normal.
Signing and checking one generated image
Drag to exploreNotice the two places a check can fail: the hash compare catches edited bytes, and the trust check catches an unknown signer.
Diagram connections:
- Image generator to Manifest builder (raw output)
- Manifest builder to Signer (claim + assertions)
- Signer to Signed JPEG (embed manifest)
- Signed JPEG to Reader or verifier (uploaded or shared)
- Trust anchors to Reader or verifier (who to trust)
- Reader or verifier to Validation state (verdict)
- Image generator: Your feature's output, a plain JPEG
- Manifest builder: Adds c2pa.created + trainedAlgorithmicMedia
- Signer: Hashes bytes, signs claim with private key
- Signed JPEG: Manifest stored inside the file
- Reader or verifier: Rehashes bytes, checks signature
- Trust anchors: Root certificates this verifier accepts
- Validation state: Invalid, Valid or Trusted
How do you sign an AI-generated image with c2pa-python?
You install c2pa-python, make a test certificate, describe the output in a manifest and sign the file. We ran every step below on 26 October 2026 with c2pa-python 0.38.1, its native SDK 0.91.3, Pillow 13.0.0 and Python 3.13.17.
Make a clean environment
Create a virtual environment, then run: pip install c2pa-python==0.38.1 pillow==13.0.0 cryptography==50.0.2. The package needs Python 3.10 or later.
Create a throwaway certificate
Save Script 1 as make_test_certs.py and run: python make_test_certs.py. It writes a test root certificate, a signing certificate and a private key into a folder called work.
Sign a stand-in output
Save Script 2 as sign_and_read.py and run: python sign_and_read.py. It draws a plain 256 by 256 JPEG as a stand-in for your model's output, then signs it.
Run the three checks
Save Script 3 as trust_tamper_strip.py and run: python trust_tamper_strip.py. It reads the file with and without a trust anchor, flips ten bytes, and re-saves the image.
Compare with our output
Your manifest ID and serial number will differ. The states and failure codes should match ours exactly. Byte counts can differ by a few bytes, because each signature differs.
Script 1 (make_test_certs.py): a test certificate chain
Two words first. A digital signature is a code made with a private key; it proves who signed the data and that nobody changed it. A certificate is a file that ties that key to a named signer.
You can copy this script as is; its only idea is a test root that signs a signer. Never use these certificates in production, because no outside verifier trusts them.
"""Create a throwaway test CA and a signing certificate for C2PA demos.
TEST ONLY: these certificates are self-made, so no validator trusts them.
Writes: test_ca.pem, signer_chain.pem (signer cert + CA), signer_key.pem
"""
import datetime
import sys
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
out = Path(sys.argv[1] if len(sys.argv) > 1 else "work")
out.mkdir(parents=True, exist_ok=True)
now = datetime.datetime.now(datetime.timezone.utc)
# 1. Test root CA
ca_key = ec.generate_private_key(ec.SECP256R1())
ca_name = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "Test Root CA (demo only)"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Demo"),
])
ca_cert = (
x509.CertificateBuilder()
.subject_name(ca_name).issuer_name(ca_name)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(days=1))
.not_valid_after(now + datetime.timedelta(days=30))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=False, content_commitment=False, key_encipherment=False,
data_encipherment=False, key_agreement=False, key_cert_sign=True,
crl_sign=True, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False)
.sign(ca_key, hashes.SHA256())
)
# 2. Signing (end-entity) certificate issued by the test CA
key = ec.generate_private_key(ec.SECP256R1())
name = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "Demo content signer"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Demo"),
])
cert = (
x509.CertificateBuilder()
.subject_name(name).issuer_name(ca_name)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(days=1))
.not_valid_after(now + datetime.timedelta(days=30))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=True, content_commitment=False, key_encipherment=False,
data_encipherment=False, key_agreement=False, key_cert_sign=False,
crl_sign=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), critical=False)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False)
.sign(ca_key, hashes.SHA256())
)
pem = serialization.Encoding.PEM
(out / "test_ca.pem").write_bytes(ca_cert.public_bytes(pem))
(out / "signer_chain.pem").write_bytes(cert.public_bytes(pem) + ca_cert.public_bytes(pem))
(out / "signer_key.pem").write_bytes(key.private_bytes(
pem, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
print("wrote test_ca.pem, signer_chain.pem, signer_key.pem to", out)Script 2 (sign_and_read.py): sign the image and read it back
The manifest has one assertion: a created action with the generative AI source type. The signer takes the certificate chain and key as bytes.
import json
import sys
from pathlib import Path
import c2pa
from PIL import Image
work = Path(sys.argv[1] if len(sys.argv) > 1 else "work")
print("c2pa-python", c2pa.__version__, "| native SDK", c2pa.sdk_version())
# 1. A stand-in for your model's output: a plain 256x256 JPEG
src = work / "generated.jpg"
Image.new("RGB", (256, 256), (40, 120, 200)).save(src, quality=90)
# 2. The manifest: what made this file and how (IPTC digital source type)
manifest = {
"claim_generator_info": [{"name": "demo-image-feature", "version": "0.1.0"}],
"title": "generated.jpg",
"assertions": [{
"label": "c2pa.actions",
"data": {"actions": [{
"action": "c2pa.created",
"digitalSourceType":
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
}]},
}],
}
# 3. Signer: certificate chain and private key as BYTES (str raises TypeError)
info = c2pa.C2paSignerInfo(
alg=c2pa.C2paSigningAlg.ES256,
sign_cert=(work / "signer_chain.pem").read_bytes(),
private_key=(work / "signer_key.pem").read_bytes(),
ta_url=None, # no time-stamp authority in this offline demo
)
signer = c2pa.Signer.from_info(info)
dst = work / "generated_signed.jpg"
with c2pa.Builder(manifest) as builder:
builder.sign_file(src, dst, signer)
print("bytes before:", src.stat().st_size, "| after:", dst.stat().st_size)
# 4. Read the manifest back and check it
with c2pa.Reader(str(dst)) as reader:
active = reader.get_active_manifest()
action = active["assertions"][0]["data"]["actions"][0]
print("assertion label:", active["assertions"][0]["label"])
print("action:", action["action"])
print("digitalSourceType:", action["digitalSourceType"].rsplit("/", 1)[-1])
print("signed by:", active["signature_info"]["common_name"])
print("validation_state:", reader.get_validation_state())
failures = (reader.get_validation_results() or {}).get("activeManifest", {}).get("failure", [])
print("failure codes:", sorted({f["code"] for f in failures}))
(work / "manifest_store.json").write_text(json.dumps(json.loads(reader.json()), indent=2))Script 3 (trust_tamper_strip.py): trust, tamper and re-save checks
This script makes four reads: the signed file twice, then a tampered copy and a re-saved copy. It only adds test files to your work folder.
"""Three checks on the signed image from sign_and_read.py.
A. Add the test CA as a trust anchor: does the state become Trusted?
B. Flip bytes in the image data: does validation catch it?
C. Re-save the image with Pillow, as a resize step would: what does the reader see?
"""
import json
import sys
from pathlib import Path
import c2pa
from PIL import Image
work = Path(sys.argv[1] if len(sys.argv) > 1 else "work")
signed = work / "generated_signed.jpg"
def report(label, path, context=None):
try:
kwargs = {"context": context} if context is not None else {}
with c2pa.Reader(str(path), **kwargs) as r:
res = (r.get_validation_results() or {}).get("activeManifest", {})
print(f"{label}: state={r.get_validation_state()} "
f"failures={sorted({f['code'] for f in res.get('failure', [])})}")
except c2pa.C2paError as e:
print(f"{label}: {type(e).__name__}: {str(e)[:120]}")
# Baseline: default settings (no trust list)
report("baseline (no trust list)", signed)
# A. Trust our own test CA
settings = c2pa.Settings.from_json(json.dumps({
"trust": {"trust_anchors": (work / "test_ca.pem").read_text()},
"verify": {"verify_trust": True},
}))
ctx = c2pa.Context(settings)
report("A. test CA added as trust anchor", signed, ctx)
# B. Tamper: change bytes near the end of the file (inside the image data)
data = bytearray(signed.read_bytes())
for i in range(len(data) - 200, len(data) - 190):
data[i] ^= 0xFF
tampered = work / "tampered.jpg"
tampered.write_bytes(bytes(data))
report("B. 10 image bytes flipped", tampered)
# C. Re-save through Pillow (decode + re-encode), as a resize/compress step would
resaved = work / "resaved.jpg"
Image.open(signed).save(resaved, quality=85)
report("C. re-saved with Pillow", resaved)How do you check that the mark worked?
Read the file back and look at the validation state and failure codes. Here is the full output from our run of all three scripts.
c2pa-python 0.38.1 | native SDK 0.91.3
bytes before: 1654 | after: 34319
assertion label: c2pa.actions.v2
action: c2pa.created
digitalSourceType: trainedAlgorithmicMedia
signed by: Demo content signer
validation_state: Valid
failure codes: ['signingCredential.untrusted']
baseline (no trust list): state=Valid failures=['signingCredential.untrusted']
A. test CA added as trust anchor: state=Trusted failures=[]
B. 10 image bytes flipped: state=Invalid failures=['assertion.dataHash.mismatch', 'signingCredential.untrusted']
C. re-saved with Pillow: _C2paManifestNotFound: ManifestNotFound: no JUMBF data found| Check | Result | What it tells you |
|---|---|---|
| Read with default settings | Valid, signingCredential.untrusted | Hashes and signature are intact, but the reader does not know your signer |
| Read with the test root as trust anchor | Trusted, no failures | Trust depends on the verifier's list of root certificates, not on the file alone |
| Flip 10 bytes of image data | Invalid, assertion.dataHash.mismatch | Any change to the hashed bytes is caught, as long as the manifest is still in the file |
| Decode and re-save with Pillow | ManifestNotFound | The mark is gone, and the file gives no sign it ever had one |
The byte counts are worth checking too. Our 1,654-byte test JPEG became about 34,300 bytes after signing; across several runs we saw 34,319 to 34,323. With the automatic thumbnail turned off, it became about 4,800 bytes.
The thumbnail setting is one line of JSON passed through a context. In Script 2, replace the signing block with this:
settings = c2pa.Settings.from_json(json.dumps({"builder": {"thumbnail": {"enabled": False}}}))
with c2pa.Builder(manifest, context=c2pa.Context(settings)) as builder:
builder.sign_file(src, dst, signer)So most of the growth on this tiny image was the thumbnail the SDK adds by default. Measure with your own image sizes before you plan storage or bandwidth.
Why can a metadata mark disappear, and what covers the gap?
Metadata lives beside the pixels, so any step that decodes and re-encodes the image can drop it. Our Pillow re-save did exactly that, with no error and no warning.
This is why recital 133 allows a combination of techniques. A watermark lives inside the pixels or words, so it can outlast a re-save that throws metadata away.
The C2PA specification has a name for this link. A soft binding, such as a fingerprint or invisible watermark, can match a stripped asset back to a manifest kept in a store elsewhere.
Check your own pipeline first. A resize worker, an image CDN or a thumbnail job you wrote may be the first thing that strips your mark.
Copying the old manifest onto resized bytes does not help. We tested it: the copy read Invalid with assertion.dataHash.mismatch, because the hashes describe the old bytes.
What worked was signing a new manifest for the resized file, with the signed original attached as its parent ingredient. That read Valid, and the original's record travelled inside it.
Here is that step as Script 4 (resign_resized.py). Run it after Scripts 1 and 2; it prints state: Valid, ingredients: ['parentOf'], the actions c2pa.opened and c2pa.resized, and two manifests in the store. Valid here still means untrusted, because it uses the same test certificate.
import json
import sys
from pathlib import Path
import c2pa
from PIL import Image
work = Path(sys.argv[1] if len(sys.argv) > 1 else "work")
signed = work / "generated_signed.jpg"
signer = c2pa.Signer.from_info(c2pa.C2paSignerInfo(
alg=c2pa.C2paSigningAlg.ES256,
sign_cert=(work / "signer_chain.pem").read_bytes(),
private_key=(work / "signer_key.pem").read_bytes(),
ta_url=None,
))
# The resize step: decode, shrink, re-encode (this drops the old manifest)
resized = work / "resized.jpg"
Image.open(signed).resize((128, 128)).save(resized, quality=85)
# Sign the resized file with a new manifest; the signed original is its parent
resize_manifest = {
"claim_generator_info": [{"name": "demo-resize-step", "version": "0.1.0"}],
"title": "resized.jpg",
"assertions": [{"label": "c2pa.actions",
"data": {"actions": [{"action": "c2pa.resized"}]}}],
}
resigned = work / "resized_signed.jpg"
with c2pa.Builder(resize_manifest) as builder:
builder.set_intent(c2pa.C2paBuilderIntent.EDIT) # SDK adds c2pa.opened for the parent
with open(signed, "rb") as f:
builder.add_ingredient(
{"title": "generated_signed.jpg", "relationship": "parentOf"}, "image/jpeg", f)
builder.sign_file(resized, resigned, signer)
with c2pa.Reader(str(resigned)) as reader:
active = reader.get_active_manifest()
print("state:", reader.get_validation_state())
print("ingredients:", [i["relationship"] for i in active["ingredients"]])
print("actions:", [a["action"] for a in active["assertions"][0]["data"]["actions"]])
print("manifests in store:", len(json.loads(reader.json())["manifests"]))What should your marking pipeline look like before 2 December 2026?
Sign at the moment of generation on your server, keep the mark through every later step, and add a watermark layer. Then prove it with a test that runs on every deploy.
List every generative output
Write down each feature that returns audio, image, video or text to an EU user, and its first EU launch date.
Settle your role and carve-outs
With counsel, confirm whether you are the provider and whether a carve-out such as standard editing applies.
Sign on the server, never the browser
Keep the private key in a key store or hardware module. A key shipped to the client can sign anything.
Use a real certificate and a time stamp
Get a signing certificate that the verifiers you care about accept. Set a time-stamp authority URL, the ta_url field, so the record shows when it was signed.
Protect the mark downstream
Make resize, compress and CDN steps keep the bytes, or sign a new manifest with the original as parent ingredient. A copied manifest on changed bytes reads Invalid.
Plan a layer that survives re-saves
Metadata alone can be stripped, as the demo shows. Choose a second technique from recital 133, such as a watermark, and check what Section 1 of the Code expects.
Offer detection
Publish how others can check your marks. The Code's Section 1 covers detection as well as marking, and Article 50(2) asks for marks that are detectable.
For the Pune team, the image feature launched in March 2026, so its outputs must be marked for EU users by 2 December 2026. The other steps are how it keeps that mark effective, robust and detectable.
How do you explain EU AI Act Article 50 marking in a system design interview?
Frame it as a provenance pipeline with a legal requirement on one side and a verifier on the other. Interviewers listen for where keys live, where marks get lost and how you test it.
State the requirement
Generated outputs must carry a machine-readable, detectable mark. Older EU-facing systems have until 2 December 2026; newer ones must mark from launch.
Draw the write path
Generator, manifest builder, signer with a key in a key store, then object storage. Sign once, at creation.
Name the second layer
Say metadata can be stripped by re-encoding, so a watermark layer and a fingerprint lookup are the backup.
Draw the read path
A verifier rehashes the bytes, checks the signature and compares the signer's chain with its trust anchors.
Cover failure and scale
Key rotation, certificate expiry, time stamps, and a CI test that fails when any pipeline step drops the manifest.
Questions people ask
- Is a visible "AI-generated" label enough for EU AI Act Article 50?
- No, not for providers under paragraph 2. That paragraph asks for a machine-readable mark that software can detect. A visible label helps people, and deployers must disclose deepfakes under paragraph 4. But a badge in your interface does not travel with the file, so software downstream cannot detect it.
- Does the 2 December 2026 deadline cover chatbot disclosure too?
- No. The new Article 111(4) gives extra time only for Article 50(2), the machine-readable marking duty. It covers providers whose generative systems were placed on the market before 2 August 2026. Telling users they are talking to an AI system, under paragraph 1, has applied since 2 August 2026.
- Is C2PA required by the EU AI Act?
- No. Article 50 does not name C2PA, and neither do the Commission pages on the Code that we opened. Article 50(2) asks for solutions that are "effective, interoperable, robust and reliable", and recital 133 lists watermarks, metadata, cryptographic provenance methods, logging and fingerprints as examples. C2PA is one open standard for signed provenance metadata, which is why this guide uses it for the metadata layer.
- Why did my signed file show Valid instead of Trusted?
- Because the reader does not trust your signer's root certificate. In our c2pa-python 0.38.1 test, a self-made certificate gave Valid with the code signingCredential.untrusted. Adding that root as a trust anchor gave Trusted. Public verifiers keep their own trust lists, so production signing needs a certificate they accept.
- What about AI-generated text, which has no file metadata?
- Article 50(2) covers generated text too. Plain text has no file wrapper to carry a manifest, so the other techniques in recital 133, such as watermarks, matter more. The recital notes the limits of different content types. Check Section 1 of the Code for its text measures; text watermarking is a separate topic.
- Does signing the Code of Practice make us compliant?
- Not automatically. The Code is voluntary and imposes no duties beyond the AI Act. For signatories, the Commission says enforcement will focus on monitoring adherence to the Code. Teams that do not sign must show compliance through other adequate means. Ask your counsel which route fits your product.
Sources and further reading
- AI Act Service Desk (European Commission): Article 50, consolidated text as at 27 July 2026
- AI Act Service Desk: Article 111, new paragraph 4 (2 December 2026 transition)
- AI Act Service Desk: implementation timeline (2 August and 2 December 2026 entries)
- EUR-Lex: Regulation (EU) 2026/1744 (Digital Omnibus on AI), OJ L of 24 July 2026, recital 38
- European Commission: Commission publishes Code of Practice on marking and labelling AI-generated content (10 June 2026)
- European Commission press release IP/26/1328 (10 June 2026), which links the Code of Practice policy page
- European Commission: Code of Practice on AI-generated content, policy page
- European Commission FAQ: Code of Practice on transparency of AI-generated content
- European Commission FAQ: Transparency obligations under Article 50 AI Act
- AI Act Service Desk: Recital 133 (marking techniques)
- AI Act Service Desk: Article 99 (penalties)
- AI Act Service Desk: Article 2 (scope)
- AI Act Service Desk: Article 3 (definitions of provider, deployer, placing on the market)
- European Commission: Guidelines on the transparency obligations under Article 50 (20 July 2026)
- AI Act Service Desk FAQ: Digital Omnibus (describes the proposal's six-month transition)
- C2PA Technical Specification 2.2
- IPTC NewsCodes: Digital Source Type vocabulary
- PyPI: c2pa-python 0.38.1