Convoke Game Submissions
Overview
Send each finished Convoke match to Play Nice. It's recorded as a published game on the submitter's account.
Authenticate with your partner key in an Authorization: Bearer header. It's the same key you use as your Connect with Play Nice client_secret.
POST https://api.playnicemtg.com/v1/convoke
Authorization: Bearer YOUR_PARTNER_KEY
Content-Type: application/jsonSubmit a game
POST/v1/convoke
The body is JSON with two fields.
| Field | Type | Description |
|---|---|---|
userId | string | Required. The submitter's Play Nice user id, from the token exchange. |
gameData | object | Required. The match. See Game fields. |
Resubmitting is safe. The same convoke_match_id returns the game already recorded.
curl -X POST https://api.playnicemtg.com/v1/convoke \
-H 'Authorization: Bearer YOUR_PARTNER_KEY' \
-H 'Content-Type: application/json' \
-d @game.json{
"userId": "Xy12AbCdEfGh34IjKlMn56OpQr78",
"gameData": {
"convoke_user_id": "convoke-user-123",
"convoke_match_id": "match-8841",
"format": "cedh",
"no_of_players": 4,
"isDraw": false,
"winner_player_id": "convoke-user-123",
"ending_turn": "6",
"win_type": "win-first",
"win_condition": "Thassa's Oracle",
"best_card1": "Rhystic Study",
"best_card2": "Ad Nauseam",
"fun_factor": "8",
"notes": "Won through a Silence.",
"tournament": false,
"topdeck_tournament": false,
"league": false,
"needed_one_more_turn": false,
"opening_hand": {
"card1": "Sol Ring",
"card2": "Rhystic Study",
"card3": "Island",
"card4": "Mana Vault",
"card5": "Swamp",
"card6": "Dark Ritual",
"card7": "Demonic Tutor"
},
"seats": [
{
"position": "1",
"commander": "63cda4a0-0dff-4edb-ae67-a2b7e2971350",
"partner_commander": null,
"convoke_user_id": "convoke-user-123",
"play_nice_user_id": "Xy12AbCdEfGh34IjKlMn56OpQr78",
"mulligan": "7 (1st)",
"lands_in_opening_hand": 2
},
{
"position": "2",
"commander": "557fcd17-6cb3-414a-b2b1-ea9ae32e5aec",
"partner_commander": "bc7cbe9b-324e-42b8-94e2-36e91cb32163",
"convoke_user_id": "convoke-user-456",
"play_nice_user_id": null,
"mulligan": "6",
"lands_in_opening_hand": 3
},
{
"position": "3",
"commander": "63cda4a0-0dff-4edb-ae67-a2b7e2971350",
"partner_commander": null,
"convoke_user_id": "convoke-user-789",
"play_nice_user_id": null,
"mulligan": null,
"lands_in_opening_hand": null
},
{
"position": "4",
"commander": "557fcd17-6cb3-414a-b2b1-ea9ae32e5aec",
"partner_commander": null,
"convoke_user_id": "convoke-user-012",
"play_nice_user_id": null,
"mulligan": null,
"lands_in_opening_hand": null
}
]
}
}Game fields
Optional fields with an invalid value are dropped or defaulted, not rejected.
| Field | Type | Description |
|---|---|---|
convoke_user_id | string | Required. The submitter's Convoke id. Must match a seat. |
convoke_match_id | string | Required. Your unique match id. |
no_of_players | integer | Required. 2–5. |
isDraw | boolean | Required. |
winner_player_id | string | Required unless isDraw. The winner's convoke_user_id. |
seats | array | Required. One per player. See Seat fields. |
format | string | See Allowed values. Default casual. |
ending_turn | string | "2"–"99". "1" means not tracked. |
win_type | string | Win reason. Ignored for draws. |
draw_type | string | Draw reason. Draws only. |
win_condition | string | The combo or action that won. Up to 200 characters. |
best_card1 | string | MVP card name. Up to 100 characters. |
best_card2 | string | Second MVP card name. Up to 100 characters. |
fun_factor | string | "1"–"10". |
notes | string | Up to 10,000 characters. |
tournament | boolean | Default false. |
topdeck_tournament | boolean | Default false. |
league | boolean | Default false. |
needed_one_more_turn | boolean | The submitter would have won next turn. Default false. |
opening_hand | object | The submitter's opening hand. See Opening hand. |
Seat fields
seats must hold exactly no_of_players entries.
| Field | Type | Description |
|---|---|---|
position | string | Required. "1" to no_of_players, unique. |
commander | string | Scryfall card id, any printing. Required on the submitter's seat; an opponent seat without one is recorded as an unknown deck. |
convoke_user_id | string | Required. The pilot's Convoke id, unique per game. |
partner_commander | string | null | Scryfall card id of the partner or background. |
play_nice_user_id | string | null | The pilot's Play Nice user id. See Linked accounts. |
mulligan | string | null | See Allowed values. |
lands_in_opening_hand | integer | null | 0–7. |
Opening hand
The submitter's opening hand, as card names in card1–card7. Leave out slots the hand didn't have.
Send names, not ids: Play Nice matches each name to the printing in the user's decklist. Names it can't match are dropped.
"opening_hand": {
"card1": "Sol Ring",
"card2": "Arcane Signet",
"card3": "Command Tower",
"card4": "Rhystic Study",
"card5": "Island"
}Allowed values
format
| Value | Meaning |
|---|---|
casual | Casual |
high-power | High Power |
cedh | cEDH |
mulligan: the hand size kept, exactly as written. 7 (1st) and 7 (2nd) are a seven kept on the first or second hand.
| Value |
|---|
7 (1st) |
7 (2nd) |
6 |
5 |
4 |
3 |
2 |
1 |
win_type
| Value | Meaning |
|---|---|
win-first | Win First |
win-through | Win Through |
win-after | Win After |
win-on-top | Win on Top |
win-because | Win Because |
draw_type
| Value | Meaning |
|---|---|
time | Went to Time |
forced | Forced Draw |
id | Intentional Draw |
infinite | Accidental Infinite Loop |
nobody | Nobody Wanted to Play Anymore |
stax | Stax Player Couldn't Win |
Response
| Field | Type | Description |
|---|---|---|
game_id | string | The Play Nice game id. |
game_url | string | The game on the submitter's profile. |
share_game_url | string | Other players open it to add a copy of the game to their own account. |
convoke_match_id | string | Echoed back. |
{
"success": true,
"game_id": "3f1c9a2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
"game_url": "https://www.playnicemtg.com/users/kinnanfan/games/3f1c9a2e-5b7d-4e8f-9a0b-1c2d3e4f5a6b",
"share_game_url": "https://www.playnicemtg.com/s/aB3dE9",
"convoke_match_id": "match-8841"
}How games are recorded
- Dated when Play Nice receives it, with venue Convoke and marked online.
- Added to the submitter's deck for that commander. The deck is created if it doesn't exist.
- A missing or invalid commander fails the request for the submitter (
400or422), and becomes an unknown deck for an opponent. - A partner pair with one invalid side is recorded as the valid commander.
- The submitter gets a Play Nice notification, unless they've turned notifications off.
Linked accounts
Once enforcement is on, submissions require linked accounts:
userIdmust be linked to the submitter'sconvoke_user_id, or the request fails with403 NOT_LINKED.- A seat's
play_nice_user_idcounts only if that Play Nice account is linked to that seat'sconvoke_user_id. Otherwise the seat is recorded without a Play Nice account. - Ids come from your server, never from a client payload.
On NOT_LINKED,don't retry. Clear the stored Play Nice id and ask the user to link. Enforcement starts on a date we agree with you.
{
"success": false,
"error": {
"code": "NOT_LINKED",
"message": "This Convoke account is not linked to that Play Nice account. Clear the stored Play Nice ID and ask the user to link their account."
}
}Errors
Errors return error.code and error.message, plus error.field when one field is at fault.
| Status | Code | What to do |
|---|---|---|
| 400 | BAD_JSON | Send valid JSON. |
| 400 | VALIDATION_FAILED | Fix the field named in error.field. |
| 401 | UNAUTHORIZED | Check the partner key. |
| 403 | NOT_LINKED | Ask the user to link. Don't retry. |
| 404 | USER_NOT_FOUND | userId isn't a Play Nice user. |
| 409 | CONFLICT | This convoke_match_id was already recorded for another user. |
| 422 | INVALID_COMMANDER | The submitter's commander isn't a legal commander or companion. |
| 500 | INTERNAL_ERROR | Temporary. Retry with the same body. |
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "gameData.winner_player_id is required when isDraw is false.",
"field": "gameData.winner_player_id"
}
}Existing users
Users who pasted their Play Nice id into Convoke don't need to link again:
- Ship the link flow.
- Remove the manual Play Nice id field.
- Send us a one-time export of every
convoke_user_id/play_nice_user_idpair.
We create links from it. A Play Nice id claimed by more than one Convoke account is skipped; those users link through the flow.
[
{ "convoke_user_id": "convoke-user-123", "play_nice_user_id": "Xy12AbCdEfGh34IjKlMn56OpQr78" },
{ "convoke_user_id": "convoke-user-456", "play_nice_user_id": "Pq98RsTuVwXy76ZaBcDe54FgHi32" }
]